diff --git a/Cargo.lock b/Cargo.lock index 7472d58a48..46a59ae0d0 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -143,9 +143,9 @@ dependencies = [ [[package]] name = "anyhow" -version = "1.0.102" +version = "1.0.104" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7f202df86484c868dbad7eaa557ef785d5c66295e41b460ef922eca0723b842c" +checksum = "330a5ed07fa54e4702c9d6c4174f74427fc0ef6e214bbd677ae50a5099946470" [[package]] name = "approx" @@ -158,9 +158,9 @@ dependencies = [ [[package]] name = "arc-swap" -version = "1.9.1" +version = "1.9.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6a3a1fd6f75306b68087b831f025c712524bcb19aad54e557b1129cfa0a2b207" +checksum = "c049c0be4daef0b145cb3555416b3b8ef5b7888a38aea1a3a155801fe7b0810b" dependencies = [ "rustversion", ] @@ -281,9 +281,9 @@ checksum = "c08606f8c3cbf4ce6ec8e28fb0014a2c086708fe954eaa885384a6165172e7e8" [[package]] name = "aws-config" -version = "1.8.16" +version = "1.10.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "50f156acdd2cf55f5aa53ee416c4ac851cf1222694506c0b1f78c85695e9ca9d" +checksum = "701418aa459dac33e50a0f8e818e5662a16bc018a6ac7423659b70f3799d67a8" dependencies = [ "aws-credential-types", "aws-runtime", @@ -295,6 +295,7 @@ dependencies = [ "aws-smithy-json", "aws-smithy-runtime", "aws-smithy-runtime-api", + "aws-smithy-schema", "aws-smithy-types", "aws-types", "bytes", @@ -311,9 +312,9 @@ dependencies = [ [[package]] name = "aws-credential-types" -version = "1.2.14" +version = "1.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8f20799b373a1be121fe3005fba0c2090af9411573878f224df44b42727fcaf7" +checksum = "e93964ffdaf57857f544be3666a5f57570bb699e934700f11b49708f61bb556e" dependencies = [ "aws-smithy-async", "aws-smithy-runtime-api", @@ -346,9 +347,9 @@ dependencies = [ [[package]] name = "aws-runtime" -version = "1.7.3" +version = "1.9.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5dcd93c82209ac7413532388067dce79be5a8780c1786e5fae3df22e4dee2864" +checksum = "a6b50a43f3ccdf331521c6d6c68b7cc9668b6e09d439ebda9569df5722324d76" dependencies = [ "aws-credential-types", "aws-sigv4", @@ -371,10 +372,11 @@ dependencies = [ [[package]] name = "aws-sdk-ec2" -version = "1.224.0" +version = "1.241.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a12b9fba5a25f668638edc00be062da3b123e3bf9ea07f13b681a19de097f2d8" +checksum = "36d274d82dfee2c5f8c43c023a32605521ff86a0342ed2279c6c02ed5401cefc" dependencies = [ + "arc-swap", "aws-credential-types", "aws-runtime", "aws-smithy-async", @@ -384,6 +386,7 @@ dependencies = [ "aws-smithy-query", "aws-smithy-runtime", "aws-smithy-runtime-api", + "aws-smithy-schema", "aws-smithy-types", "aws-smithy-xml", "aws-types", @@ -396,10 +399,11 @@ dependencies = [ [[package]] name = "aws-sdk-sso" -version = "1.98.0" +version = "1.104.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d69c77aafa20460c68b6b3213c84f6423b6e76dbf89accd3e1789a686ffd9489" +checksum = "b53416d16c278234845392e38d93bd4481d2f09daa0f005a2277f0aa91f59c22" dependencies = [ + "arc-swap", "aws-credential-types", "aws-runtime", "aws-smithy-async", @@ -408,6 +412,7 @@ dependencies = [ "aws-smithy-observability", "aws-smithy-runtime", "aws-smithy-runtime-api", + "aws-smithy-schema", "aws-smithy-types", "aws-types", "bytes", @@ -420,10 +425,11 @@ dependencies = [ [[package]] name = "aws-sdk-ssooidc" -version = "1.100.0" +version = "1.106.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1c7e7b09346d5ca22a2a08267555843a6a0127fb20d8964cb6ecfb8fdb190225" +checksum = "cc9b706c3305ed0285d5b1b696c747aa34950f830fb03e3e6c76890f99b9f188" dependencies = [ + "arc-swap", "aws-credential-types", "aws-runtime", "aws-smithy-async", @@ -432,6 +438,7 @@ dependencies = [ "aws-smithy-observability", "aws-smithy-runtime", "aws-smithy-runtime-api", + "aws-smithy-schema", "aws-smithy-types", "aws-types", "bytes", @@ -444,10 +451,11 @@ dependencies = [ [[package]] name = "aws-sdk-sts" -version = "1.103.0" +version = "1.109.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c2249b81a2e73a8027c41c378463a81ec39b8510f184f2caab87de912af0f49b" +checksum = "32d214cdfa5bbe17f117e76a7643fadf32a5234fb597322ef8b1fb4b2f17dbbd" dependencies = [ + "arc-swap", "aws-credential-types", "aws-runtime", "aws-smithy-async", @@ -457,6 +465,7 @@ dependencies = [ "aws-smithy-query", "aws-smithy-runtime", "aws-smithy-runtime-api", + "aws-smithy-schema", "aws-smithy-types", "aws-smithy-xml", "aws-types", @@ -469,9 +478,9 @@ dependencies = [ [[package]] name = "aws-sigv4" -version = "1.4.3" +version = "1.5.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "68dc0b907359b120170613b5c09ccc61304eac3998ff6274b97d93ee6490115a" +checksum = "723c2234ad7511ceef63eab016b7ba6ff7c55590fefb96fa8467af014a07309f" dependencies = [ "aws-credential-types", "aws-smithy-http", @@ -491,9 +500,9 @@ dependencies = [ [[package]] name = "aws-smithy-async" -version = "1.2.14" +version = "1.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2ffcaf626bdda484571968400c326a244598634dc75fd451325a54ad1a59acfc" +checksum = "f02e407fb3b54891734224b9ffac8a71fdd35f542500fa1af95754a6b2beb316" dependencies = [ "futures-util", "pin-project-lite", @@ -502,9 +511,9 @@ dependencies = [ [[package]] name = "aws-smithy-http" -version = "0.63.6" +version = "0.64.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ba1ab2dc1c2c3749ead27180d333c42f11be8b0e934058fb4b2258ee8dbe5231" +checksum = "37843d9add67c3aff5856f409c6dc315d3cdff60f9c0cb5b670dab1e9920306d" dependencies = [ "aws-smithy-runtime-api", "aws-smithy-types", @@ -523,9 +532,9 @@ dependencies = [ [[package]] name = "aws-smithy-http-client" -version = "1.1.12" +version = "1.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6a2f165a7feee6f263028b899d0a181987f4fa7179a6411a32a439fba7c5f769" +checksum = "635d23afda0a6ab48d666c4d447c4873e8d1e83518a2be2093122397e50b838e" dependencies = [ "aws-smithy-async", "aws-smithy-runtime-api", @@ -542,7 +551,7 @@ dependencies = [ "hyper-util", "pin-project-lite", "rustls 0.21.12", - "rustls 0.23.40", + "rustls 0.23.42", "rustls-native-certs", "rustls-pki-types", "tokio", @@ -553,43 +562,49 @@ dependencies = [ [[package]] name = "aws-smithy-json" -version = "0.62.5" +version = "0.63.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9648b0bb82a2eedd844052c6ad2a1a822d1f8e3adee5fbf668366717e428856a" +checksum = "3dc65a121adb4b33729919fcfa14fa36fb33c1555a8f06bb0e2188dbfdc1d9ef" dependencies = [ + "aws-smithy-runtime-api", + "aws-smithy-schema", "aws-smithy-types", ] [[package]] name = "aws-smithy-observability" -version = "0.2.6" +version = "0.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a06c2315d173edbf1920da8ba3a7189695827002e4c0fc961973ab1c54abca9c" +checksum = "8e86338c869539a581bf161247762a6e87f92c5c075060057b5ed6d06632ed0c" dependencies = [ "aws-smithy-runtime-api", ] [[package]] name = "aws-smithy-query" -version = "0.60.15" +version = "0.62.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1a56d79744fb3edb5d722ef79d86081e121d3b9422cb209eb03aea6aa4f21ebd" +checksum = "512346c7212ab7436df2d77a16d976a468ae44a418835511d2a69269810aaf62" dependencies = [ + "aws-smithy-runtime-api", + "aws-smithy-schema", "aws-smithy-types", + "aws-smithy-xml", "urlencoding", ] [[package]] name = "aws-smithy-runtime" -version = "1.11.1" +version = "1.12.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0504b1ab12debb5959e5165ee5fe97dd387e7aa7ea6a477bfd7635dfe769a4f5" +checksum = "07505b34e8f4b3591a4fa69e9792b52289b95488dbbc68c3c0075b7bedb245e1" dependencies = [ "aws-smithy-async", "aws-smithy-http", "aws-smithy-http-client", "aws-smithy-observability", "aws-smithy-runtime-api", + "aws-smithy-schema", "aws-smithy-types", "bytes", "fastrand", @@ -606,9 +621,9 @@ dependencies = [ [[package]] name = "aws-smithy-runtime-api" -version = "1.12.0" +version = "1.14.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b71a13df6ada0aafbf21a73bdfcdf9324cfa9df77d96b8446045be3cde61b42e" +checksum = "3b98f2e1fd67ec06618f9c291e5e495a468e60519e44c9c1979cd0521f3affdb" dependencies = [ "aws-smithy-async", "aws-smithy-runtime-api-macros", @@ -624,20 +639,31 @@ dependencies = [ [[package]] name = "aws-smithy-runtime-api-macros" -version = "1.0.0" +version = "1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8d7396fd9500589e62e460e987ecb671bad374934e55ec3b5f498cc7a8a8a7b7" +checksum = "221eaa237ddf1ca79b60d1372aad77e47f9c0ea5b3ce5099da8c61d027dc77b3" dependencies = [ "proc-macro2", "quote", "syn 2.0.117", ] +[[package]] +name = "aws-smithy-schema" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7d56e0a4e53127a632224e43633b0fe045fa9e1e3cfc68b9830f1115e103f910" +dependencies = [ + "aws-smithy-runtime-api", + "aws-smithy-types", + "http 1.4.0", +] + [[package]] name = "aws-smithy-types" -version = "1.4.7" +version = "1.6.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9d73dbfbaa8e4bc57b9045137680b958d274823509a360abfd8e1d514d40c95c" +checksum = "d6dc683efb34b9e755675b37fedbe0103141e5b6df7bdc9eb6967756a8c167d8" dependencies = [ "base64-simd", "bytes", @@ -661,22 +687,26 @@ dependencies = [ [[package]] name = "aws-smithy-xml" -version = "0.60.15" +version = "0.62.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0ce02add1aa3677d022f8adf81dcbe3046a95f17a1b1e8979c145cd21d3d22b3" +checksum = "ce84f71c72fee2cbbadde6e7d082f5fb466e3a84733855295fa7aafd1b31b7d8" dependencies = [ + "aws-smithy-runtime-api", + "aws-smithy-schema", + "aws-smithy-types", "xmlparser", ] [[package]] name = "aws-types" -version = "1.3.15" +version = "1.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2f4bbcaa9304ea40902d3d5f42a0428d1bd895a2b0f6999436fb279ffddc58ac" +checksum = "eec1cd5469f328c782dc3e33d4153cf118a54e33cbb3356d60d16f89883e1f94" dependencies = [ "aws-credential-types", "aws-smithy-async", "aws-smithy-runtime-api", + "aws-smithy-schema", "aws-smithy-types", "rustc_version", "tracing", @@ -712,7 +742,7 @@ dependencies = [ "sha1 0.10.6", "sync_wrapper", "tokio", - "tokio-tungstenite", + "tokio-tungstenite 0.29.0", "tower", "tower-layer", "tower-service", @@ -786,7 +816,7 @@ dependencies = [ "hyper 1.9.0", "hyper-util", "pin-project-lite", - "rustls 0.23.40", + "rustls 0.23.42", "rustls-pki-types", "tokio", "tokio-rustls 0.26.4", @@ -846,9 +876,9 @@ dependencies = [ [[package]] name = "bcrypt" -version = "0.19.1" +version = "0.19.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "24ae5479c93d3720e4c1dbd6b945b97457c50cb672781104768190371df1a905" +checksum = "7f3c067aa24dd4ed5c79cf222a38f260c8f23d3b82a062fba3f28c6fe563b753" dependencies = [ "base64 0.22.1", "blowfish", @@ -942,7 +972,7 @@ dependencies = [ "serde_derive", "serde_json", "serde_urlencoded", - "thiserror 2.0.18", + "thiserror 2.0.19", "tokio", "tokio-util", "tower-service", @@ -1056,9 +1086,9 @@ checksum = "8f1fe948ff07f4bd06c30984e69f5b4899c516a3ef74f34df92a2df2ab535495" [[package]] name = "bytes" -version = "1.11.1" +version = "1.12.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1e748733b7cbc798e1434b6ac524f0c1ff2ab456fe201501e6497c8417a4fc33" +checksum = "fc652a48c352aef3ea3aed32080501cf3ef6ed5da78602a020c991775b0aff04" dependencies = [ "serde", ] @@ -1082,7 +1112,7 @@ dependencies = [ "find-msvc-tools", "jobserver", "libc", - "shlex", + "shlex 1.3.0", ] [[package]] @@ -1140,9 +1170,9 @@ dependencies = [ [[package]] name = "chrono" -version = "0.4.44" +version = "0.4.45" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c673075a2e0e5f4a1dde27ce9dee1ea4558c7ffe648f576438a20ca1d2acc4b0" +checksum = "1aa79e62e7697b8e29b513a68abacf485adcd1fe8284a4316c5ae868e6633327" dependencies = [ "iana-time-zone", "js-sys", @@ -1174,7 +1204,7 @@ dependencies = [ "mogh_error", "mogh_pki", "mogh_resolver", - "reqwest 0.13.3", + "reqwest 0.13.4", "serde", "serde_json", "strum 0.28.0", @@ -1220,9 +1250,9 @@ dependencies = [ [[package]] name = "clap" -version = "4.6.1" +version = "4.6.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1ddb117e43bbf7dacf0a4190fef4d345b9bad68dfc649cb349e7d17d28428e51" +checksum = "d91e0c145792ef73a6ad36d27c75ac09f1832222a3c209689d90f534685ee5b7" dependencies = [ "clap_builder", "clap_derive", @@ -1230,9 +1260,9 @@ dependencies = [ [[package]] name = "clap_builder" -version = "4.6.0" +version = "4.6.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "714a53001bf66416adb0e2ef5ac857140e7dc3a0c48fb28b2f10762fc4b5069f" +checksum = "f09628afdcc538b57f3c6341e9c8e9970f18e4a481690a64974d7023bd33548b" dependencies = [ "anstream", "anstyle", @@ -1242,14 +1272,14 @@ dependencies = [ [[package]] name = "clap_derive" -version = "4.6.1" +version = "4.6.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f2ce8604710f6733aa641a2b3731eaa1e8b3d9973d5e3565da11800813f997a9" +checksum = "d012d2b9d65aca7f18f4d9878a045bc17899bba951561ba5ec3c2ba1eed9a061" dependencies = [ "heck", "proc-macro2", "quote", - "syn 2.0.117", + "syn 3.0.3", ] [[package]] @@ -1311,12 +1341,14 @@ dependencies = [ [[package]] name = "command" -version = "2.2.0" +version = "2.3.0-dev-48" dependencies = [ "komodo_client", - "shlex", + "libc", + "shlex 2.0.1", "svi", "tokio", + "tokio-util", ] [[package]] @@ -1684,7 +1716,7 @@ checksum = "a4ae5f15dda3c708c0ade84bfee31ccab44a3da4f88015ed22f63732abe300c8" [[package]] name = "database" -version = "2.2.0" +version = "2.3.0-dev-48" dependencies = [ "anyhow", "async-compression", @@ -1855,6 +1887,16 @@ dependencies = [ "ctutils", ] +[[package]] +name = "dispatch2" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1e0e367e4e7da84520dedcac1901e4da967309406d1e51017ae1abfb97adbd38" +dependencies = [ + "bitflags 2.11.1", + "objc2", +] + [[package]] name = "displaydoc" version = "0.2.5" @@ -1976,7 +2018,7 @@ dependencies = [ [[package]] name = "encoding" -version = "2.2.0" +version = "2.3.0-dev-48" dependencies = [ "anyhow", "bytes", @@ -2004,21 +2046,9 @@ dependencies = [ "regex", ] -[[package]] -name = "enum-as-inner" -version = "0.6.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a1e6a265c649f3f5979b601d26f1d05ada116434c87741c9493cb56218f76cbc" -dependencies = [ - "heck", - "proc-macro2", - "quote", - "syn 2.0.117", -] - [[package]] name = "environment" -version = "2.2.0" +version = "2.3.0-dev-48" dependencies = [ "anyhow", "formatting", @@ -2170,7 +2200,7 @@ dependencies = [ [[package]] name = "formatting" -version = "2.2.0" +version = "2.3.0-dev-48" dependencies = [ "mogh_error", ] @@ -2214,9 +2244,9 @@ dependencies = [ [[package]] name = "futures-channel" -version = "0.3.32" +version = "0.3.33" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "07bbe89c50d7a535e539b8c17bc0b49bdb77747034daa8087407d655f3f7cc1d" +checksum = "262590f4fe6afeb0bc83be1daa64e52657fe185690a958af7f3ad0e92085c5ae" dependencies = [ "futures-core", "futures-sink", @@ -2224,9 +2254,9 @@ dependencies = [ [[package]] name = "futures-core" -version = "0.3.32" +version = "0.3.33" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7e3450815272ef58cec6d564423f6e755e25379b217b0bc688e295ba24df6b1d" +checksum = "2cd50c473c80f6d7c3670a752354b8e569b1a7cbfdc0419ec88e5edad85e0dc7" [[package]] name = "futures-executor" @@ -2241,15 +2271,15 @@ dependencies = [ [[package]] name = "futures-io" -version = "0.3.32" +version = "0.3.33" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cecba35d7ad927e23624b22ad55235f2239cfa44fd10428eecbeba6d6a717718" +checksum = "4577ecaa3c4f96589d473f679a71b596316f6641bc350038b962a5daf0085d7a" [[package]] name = "futures-macro" -version = "0.3.32" +version = "0.3.33" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e835b70203e41293343137df5c0664546da5745f82ec9b84d40be8336958447b" +checksum = "2d6d3cde68c518367be28956066ddfef33813991b77a55005a69dae04bf3b10b" dependencies = [ "proc-macro2", "quote", @@ -2258,15 +2288,15 @@ dependencies = [ [[package]] name = "futures-sink" -version = "0.3.32" +version = "0.3.33" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c39754e157331b013978ec91992bde1ac089843443c49cbc7f46150b0fad0893" +checksum = "e34418ac499d6305c2fb5ad0ed2f6ac998c5f8ca209b4510f7f94242c647e307" [[package]] name = "futures-task" -version = "0.3.32" +version = "0.3.33" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "037711b3d59c33004d3856fbdc83b99d4ff37a24768fa1be9ce3538a1cde4393" +checksum = "b231ed28831efb4a61a08580c4bc233ec56bc009f4cd8f52da2c3cb97df0c109" [[package]] name = "futures-timer" @@ -2276,9 +2306,9 @@ checksum = "f288b0a4f20f9a56b5d1da57e2227c661b7b16168e2f72365f57b63326e29b24" [[package]] name = "futures-util" -version = "0.3.32" +version = "0.3.33" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "389ca41296e6190b48053de0321d02a77f32f8a5d2461dd38762c0593805c6d6" +checksum = "a77a90a256fce34da66415271e30f94ee91c57b04b8a2c042d9cf3220179deaa" dependencies = [ "futures-channel", "futures-core", @@ -2420,7 +2450,7 @@ dependencies = [ [[package]] name = "git" -version = "2.2.0" +version = "2.3.0-dev-48" dependencies = [ "anyhow", "command", @@ -2643,37 +2673,12 @@ dependencies = [ "futures-channel", "futures-io", "futures-util", - "hickory-proto 0.26.1", + "hickory-proto", "idna", "ipnet", "jni", - "rand 0.10.1", - "thiserror 2.0.18", - "tinyvec", - "tokio", - "tracing", - "url", -] - -[[package]] -name = "hickory-proto" -version = "0.25.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f8a6fe56c0038198998a6f217ca4e7ef3a5e51f46163bd6dd60b5c71ca6c6502" -dependencies = [ - "async-trait", - "cfg-if", - "data-encoding", - "enum-as-inner", - "futures-channel", - "futures-io", - "futures-util", - "idna", - "ipnet", - "once_cell", - "rand 0.9.4", - "ring", - "thiserror 2.0.18", + "rand 0.10.2", + "thiserror 2.0.19", "tinyvec", "tokio", "tracing", @@ -2692,35 +2697,14 @@ dependencies = [ "jni", "once_cell", "prefix-trie", - "rand 0.10.1", + "rand 0.10.2", "ring", - "thiserror 2.0.18", + "thiserror 2.0.19", "tinyvec", "tracing", "url", ] -[[package]] -name = "hickory-resolver" -version = "0.25.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "dc62a9a99b0bfb44d2ab95a7208ac952d31060efc16241c87eaf36406fecf87a" -dependencies = [ - "cfg-if", - "futures-util", - "hickory-proto 0.25.2", - "ipconfig", - "moka", - "once_cell", - "parking_lot", - "rand 0.9.4", - "resolv-conf", - "smallvec", - "thiserror 2.0.18", - "tokio", - "tracing", -] - [[package]] name = "hickory-resolver" version = "0.26.1" @@ -2730,7 +2714,7 @@ dependencies = [ "cfg-if", "futures-util", "hickory-net", - "hickory-proto 0.26.1", + "hickory-proto", "ipconfig", "ipnet", "jni", @@ -2738,11 +2722,11 @@ dependencies = [ "ndk-context", "once_cell", "parking_lot", - "rand 0.10.1", + "rand 0.10.2", "resolv-conf", "smallvec", "system-configuration", - "thiserror 2.0.18", + "thiserror 2.0.19", "tokio", "tracing", ] @@ -2941,7 +2925,7 @@ dependencies = [ "http 1.4.0", "hyper 1.9.0", "hyper-util", - "rustls 0.23.40", + "rustls 0.23.42", "rustls-native-certs", "tokio", "tokio-rustls 0.26.4", @@ -3240,7 +3224,7 @@ dependencies = [ [[package]] name = "interpolate" -version = "2.2.0" +version = "2.3.0-dev-48" dependencies = [ "anyhow", "komodo_client", @@ -3329,7 +3313,7 @@ dependencies = [ "jni-sys", "log", "simd_cesu8", - "thiserror 2.0.18", + "thiserror 2.0.19", "walkdir", "windows-link", ] @@ -3390,9 +3374,9 @@ dependencies = [ [[package]] name = "jsonwebtoken" -version = "10.3.0" +version = "10.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0529410abe238729a60b108898784df8984c87f6054c9c4fcacc47e4803c1ce1" +checksum = "eba32bfb4ffdeaca3e34431072faf01745c9b26d25504aa7a6cf5684334fc4fc" dependencies = [ "aws-lc-rs", "base64 0.22.1", @@ -3403,11 +3387,12 @@ dependencies = [ "serde_json", "signature", "simple_asn1", + "zeroize", ] [[package]] name = "komodo_cli" -version = "2.2.0" +version = "2.3.0-dev-48" dependencies = [ "anyhow", "bcrypt", @@ -3425,7 +3410,7 @@ dependencies = [ "mogh_logger", "mogh_pki", "mogh_secret_file", - "rustls 0.23.40", + "rustls 0.23.42", "serde", "serde_json", "serde_qs", @@ -3437,7 +3422,7 @@ dependencies = [ [[package]] name = "komodo_client" -version = "2.2.0" +version = "2.3.0-dev-48" dependencies = [ "anyhow", "async_timing_util", @@ -3458,16 +3443,18 @@ dependencies = [ "mogh_server", "mongo_indexed", "partial_derive2", - "rand 0.10.1", - "reqwest 0.13.3", + "rand 0.10.2", + "regex", + "reqwest 0.13.4", "schemars 1.2.1", "serde", "serde_json", "serde_qs", "strum 0.28.0", - "thiserror 2.0.18", + "svi", + "thiserror 2.0.19", "tokio", - "tokio-tungstenite", + "tokio-tungstenite 0.30.0", "tokio-util", "tracing", "typeshare", @@ -3477,7 +3464,7 @@ dependencies = [ [[package]] name = "komodo_core" -version = "2.2.0" +version = "2.3.0-dev-48" dependencies = [ "anyhow", "arc-swap", @@ -3523,8 +3510,8 @@ dependencies = [ "partial_derive2", "periphery_client", "regex", - "reqwest 0.13.3", - "rustls 0.23.40", + "reqwest 0.13.4", + "rustls 0.23.42", "serde", "serde_json", "serde_qs", @@ -3550,7 +3537,7 @@ dependencies = [ [[package]] name = "komodo_periphery" -version = "2.2.0" +version = "2.3.0-dev-48" dependencies = [ "anyhow", "arc-swap", @@ -3569,7 +3556,7 @@ dependencies = [ "formatting", "futures-util", "git", - "hickory-resolver 0.26.1", + "hickory-resolver", "interpolate", "komodo_client", "mogh_cache", @@ -3582,13 +3569,14 @@ dependencies = [ "mogh_server", "periphery_client", "portable-pty", - "rustls 0.23.40", + "rustls 0.23.42", "serde", "serde_json", "serde_yaml_ng", "shell-escape", - "shlex", + "shlex 2.0.1", "strum 0.28.0", + "svi", "sysinfo", "tokio", "tokio-stream", @@ -3616,9 +3604,9 @@ checksum = "09edd9e8b54e49e587e4f6295a7d29c3ea94d469cb40ab8ca70b288248a81db2" [[package]] name = "libc" -version = "0.2.186" +version = "0.2.189" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "68ab91017fe16c622486840e4c83c9a37afeff978bd239b5293d61ece587de66" +checksum = "3eaf3ede3fee6db1a4c2ee091bf8a8b4dccdc6d17f656fb07896ee72867612f2" [[package]] name = "libm" @@ -3781,14 +3769,14 @@ dependencies = [ [[package]] name = "mogh_auth_client" -version = "1.5.0" +version = "1.7.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8b6b46f9e9317ea68ddf608ebd187d0d3ed6adb6d12c15dcc84ea70428e8745e" +checksum = "d4b22cf7d0f27783c7bb10c7f87fd7de3952bda65e2166d34c3285c698b00d89" dependencies = [ "anyhow", "mogh_error", "mogh_resolver", - "reqwest 0.13.3", + "reqwest 0.13.4", "serde", "serde_json", "strum 0.28.0", @@ -3799,9 +3787,9 @@ dependencies = [ [[package]] name = "mogh_auth_server" -version = "1.5.0" +version = "1.7.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4d58d23b444f9f1a591f1fd44641b894ca3068a9ce3db3ea99896a5f757fa2fe" +checksum = "a97971cfe27a593a432d773b37764d37819984fbfaff57c11d55730467f2a6ba" dependencies = [ "anyhow", "arc-swap", @@ -3817,8 +3805,8 @@ dependencies = [ "mogh_resolver", "mogh_validations", "openidconnect", - "rand 0.10.1", - "reqwest 0.13.3", + "rand 0.10.2", + "reqwest 0.13.4", "serde", "serde_json", "strum 0.28.0", @@ -3855,7 +3843,7 @@ dependencies = [ "serde", "serde_json", "serde_yaml_ng", - "thiserror 2.0.18", + "thiserror 2.0.19", "toml", "wildcard", ] @@ -3875,15 +3863,16 @@ dependencies = [ [[package]] name = "mogh_logger" -version = "1.3.3" +version = "1.3.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b0aac766e2f34066511e67bf330a88874a9ac9d58732c9c4eb0687546c7a206e" +checksum = "363551f4b80f4a3340d77d8a6a0b7d983d7cf64be58f85b2281266877c787c93" dependencies = [ "anyhow", "opentelemetry", "opentelemetry-otlp", "opentelemetry-semantic-conventions", "opentelemetry_sdk", + "serde", "tracing", "tracing-opentelemetry", "tracing-subscriber", @@ -3962,7 +3951,7 @@ version = "1.0.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "713d8731c78a3b867d712e394cef4be52b278e45aeca4b80ceb41b08d1c32334" dependencies = [ - "thiserror 2.0.18", + "thiserror 2.0.19", "tokio", ] @@ -4013,21 +4002,21 @@ dependencies = [ [[package]] name = "mongo_indexed" -version = "2.0.2" +version = "2.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "180b469d21b4cdb686c1e98050ec8e1168232f4b54022086668c93dc9839f9ab" +checksum = "d3c4b3c63eba1b673060ac84cd0557c9552cd9cf20a8cca1fff3b79ed84179d2" dependencies = [ "mongo_indexed_derive", "mongodb", "serde", - "thiserror 2.0.18", + "thiserror 2.0.19", ] [[package]] name = "mongo_indexed_derive" -version = "2.0.2" +version = "2.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cf8405357bf431a2e73d83057f2960e9349ab946389dd78af655832921434949" +checksum = "5adbea6b2e44191849ebe42cbc6f185e45be7dd27a693978ed21676696167613" dependencies = [ "proc-macro2", "quote", @@ -4054,9 +4043,9 @@ checksum = "224484c5d09285a7b8cb0a0c117e847ebd14cb6e4470ecf68cdb89c503b0edb9" [[package]] name = "mongodb" -version = "3.6.0" +version = "3.7.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1ef2c933617431ad0246fb5b43c425ebdae18c7f7259c87de0726d93b0e7e91b" +checksum = "276ba0cd571553d1f6936c6f180964776ece6ab7507dc8765f8a9c9c49d8cd00" dependencies = [ "base64 0.22.1", "bitflags 2.11.1", @@ -4067,8 +4056,9 @@ dependencies = [ "futures-io", "futures-util", "hex", - "hickory-proto 0.25.2", - "hickory-resolver 0.25.2", + "hickory-net", + "hickory-proto", + "hickory-resolver", "hmac 0.12.1", "macro_magic", "md-5", @@ -4078,8 +4068,7 @@ dependencies = [ "percent-encoding", "rand 0.9.4", "rustc_version_runtime", - "rustls 0.23.40", - "rustversion", + "rustls 0.23.42", "serde", "serde_bytes", "serde_with", @@ -4089,7 +4078,7 @@ dependencies = [ "stringprep", "strsim", "take_mut", - "thiserror 2.0.18", + "thiserror 2.0.19", "tokio", "tokio-rustls 0.26.4", "tokio-util", @@ -4100,9 +4089,9 @@ dependencies = [ [[package]] name = "mongodb-internal-macros" -version = "3.6.0" +version = "3.7.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9e5758dc828eb2d02ec30563cba365609d56ddd833190b192beaee2b475a7bb3" +checksum = "99ceb1a9a1018e470077ec94cf3a8c2d0e6da542b2c05ea95a59a0a627147375" dependencies = [ "macro_magic", "proc-macro2", @@ -4122,9 +4111,9 @@ dependencies = [ [[package]] name = "mungos" -version = "3.2.2" +version = "3.7.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f77d8548efe9a10b49e1c92fa719d2e4e27cc6b40936f05fbb0f6a7ac31d3c0f" +checksum = "585b5da25f7186bfe37d661ce9c4c28b6f284a4ac911ec1ca092e365617d1441" dependencies = [ "anyhow", "envy", @@ -4263,6 +4252,15 @@ dependencies = [ "url", ] +[[package]] +name = "objc2" +version = "0.6.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3a12a8ed07aefc768292f076dc3ac8c48f3781c8f2d5851dd3d98950e8c5a89f" +dependencies = [ + "objc2-encode", +] + [[package]] name = "objc2-core-foundation" version = "0.3.2" @@ -4270,6 +4268,24 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2a180dd8642fa45cdb7dd721cd4c11b1cadd4929ce112ebd8b9f5803cc79d536" dependencies = [ "bitflags 2.11.1", + "dispatch2", + "objc2", +] + +[[package]] +name = "objc2-encode" +version = "4.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ef25abbcd74fb2609453eb695bd2f860d389e457f67dc17cafc8b8cbc89d0c33" + +[[package]] +name = "objc2-foundation" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e3e0adef53c21f888deb4fa59fc59f7eb17404926ee8a6f59f5df0fd7f9f3272" +dependencies = [ + "bitflags 2.11.1", + "objc2", ] [[package]] @@ -4282,6 +4298,17 @@ dependencies = [ "objc2-core-foundation", ] +[[package]] +name = "objc2-open-directory" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bb82bed227edf5201dfedf072bba4015a33d3d4a98519837295a90f0a23f676d" +dependencies = [ + "objc2", + "objc2-core-foundation", + "objc2-foundation", +] + [[package]] name = "oid-registry" version = "0.7.1" @@ -4397,7 +4424,7 @@ dependencies = [ "futures-sink", "js-sys", "pin-project-lite", - "thiserror 2.0.18", + "thiserror 2.0.19", "tracing", ] @@ -4427,7 +4454,7 @@ dependencies = [ "opentelemetry_sdk", "prost", "reqwest 0.12.28", - "thiserror 2.0.18", + "thiserror 2.0.19", "tonic", "tracing", ] @@ -4463,7 +4490,7 @@ dependencies = [ "opentelemetry", "percent-encoding", "rand 0.9.4", - "thiserror 2.0.18", + "thiserror 2.0.19", "tokio", "tokio-stream", ] @@ -4627,7 +4654,7 @@ checksum = "9b4f627cb1b25917193a259e49bdad08f671f8d9708acfd5fe0a8c1455d87220" [[package]] name = "periphery_client" -version = "2.2.0" +version = "2.3.0-dev-48" dependencies = [ "anyhow", "encoding", @@ -4929,9 +4956,9 @@ dependencies = [ "quinn-proto", "quinn-udp", "rustc-hash", - "rustls 0.23.40", + "rustls 0.23.42", "socket2 0.6.3", - "thiserror 2.0.18", + "thiserror 2.0.19", "tokio", "tracing", "web-time", @@ -4950,10 +4977,10 @@ dependencies = [ "rand 0.9.4", "ring", "rustc-hash", - "rustls 0.23.40", + "rustls 0.23.42", "rustls-pki-types", "slab", - "thiserror 2.0.18", + "thiserror 2.0.19", "tinyvec", "tracing", "web-time", @@ -5023,9 +5050,9 @@ dependencies = [ [[package]] name = "rand" -version = "0.10.1" +version = "0.10.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d2e8e8bcc7961af1fdac401278c6a831614941f6164ee3bf4ce61b7edb162207" +checksum = "c7f5fa3a058cd35567ef9bfa5e75732bee0f9e4c55fa90477bef2dfcdbc4be80" dependencies = [ "chacha20 0.10.0", "getrandom 0.4.2", @@ -5127,9 +5154,9 @@ dependencies = [ [[package]] name = "regex" -version = "1.12.3" +version = "1.13.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e10754a14b9137dd7b1e3e5b0493cc9171fdd105e0ab477f51b72e7f3ac0e276" +checksum = "f020237b6c8eed93db2e2cb53c00c60a8e1bc73da7d073199a1180401450218d" dependencies = [ "aho-corasick", "memchr", @@ -5139,9 +5166,9 @@ dependencies = [ [[package]] name = "regex-automata" -version = "0.4.14" +version = "0.4.16" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6e1dd4122fc1595e8162618945476892eefca7b88c52820e74af6262213cae8f" +checksum = "8fcfdb36bda0c880c5931cdc7a2bcdc8ba4556847b9d912bca70bc94708711ad" dependencies = [ "aho-corasick", "memchr", @@ -5156,9 +5183,9 @@ checksum = "cab834c73d247e67f4fae452806d17d3c7501756d98c8808d7c9c7aa7d18f973" [[package]] name = "regex-syntax" -version = "0.8.10" +version = "0.8.11" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "dc897dd8d9e8bd1ed8cdad82b5966c3e0ecae09fb1907d58efaa013543185d0a" +checksum = "d6f6ff9a378485b298a5286656da665ba74413d36db0979633275d2e708145d4" [[package]] name = "relative-path" @@ -5200,7 +5227,7 @@ dependencies = [ "percent-encoding", "pin-project-lite", "quinn", - "rustls 0.23.40", + "rustls 0.23.42", "rustls-native-certs", "rustls-pki-types", "serde", @@ -5221,9 +5248,9 @@ dependencies = [ [[package]] name = "reqwest" -version = "0.13.3" +version = "0.13.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "62e0021ea2c22aed41653bc7e1419abb2c97e038ff2c33d0e1309e49a97deec0" +checksum = "219c5811de6525e5416c7d5d53bb656d3afdbc6c5af816e0802bcfa42dbdc1c3" dependencies = [ "base64 0.22.1", "bytes", @@ -5241,7 +5268,7 @@ dependencies = [ "percent-encoding", "pin-project-lite", "quinn", - "rustls 0.23.40", + "rustls 0.23.42", "rustls-pki-types", "rustls-platform-verifier", "serde", @@ -5514,9 +5541,9 @@ dependencies = [ [[package]] name = "rustls" -version = "0.23.40" +version = "0.23.42" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ef86cd5876211988985292b91c96a8f2d298df24e75989a43a3c73f2d4d8168b" +checksum = "3c54fcab019b409d04215d3a17cb438fd7fbf192ee61461f20f4fe18704bc138" dependencies = [ "aws-lc-rs", "log", @@ -5561,7 +5588,7 @@ dependencies = [ "jni", "log", "once_cell", - "rustls 0.23.40", + "rustls 0.23.42", "rustls-native-certs", "rustls-platform-verifier-android", "rustls-webpki 0.103.13", @@ -5744,9 +5771,9 @@ checksum = "8a7852d02fc848982e0c167ef163aaff9cd91dc640ba85e263cb1ce46fae51cd" [[package]] name = "serde" -version = "1.0.228" +version = "1.0.229" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9a8e94ea7f378bd32cbbd37198a4a91436180c5bb472411e48b5ec2e2124ae9e" +checksum = "4148590afebada386688f18773da617792bf2ef03ffc1e4cbd2b1d45b023e0ba" dependencies = [ "serde_core", "serde_derive", @@ -5784,22 +5811,22 @@ dependencies = [ [[package]] name = "serde_core" -version = "1.0.228" +version = "1.0.229" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "41d385c7d4ca58e59fc732af25c3983b67ac852c1a25000afe1175de458b67ad" +checksum = "67dca2c9c51e58a4791a4b1ed58308b39c64224d349a935ab5039aa360942a48" dependencies = [ "serde_derive", ] [[package]] name = "serde_derive" -version = "1.0.228" +version = "1.0.229" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d540f220d3187173da220f885ab66608367b6574e925011a9353e4badda91d79" +checksum = "e7a5d71263a5a7d47b41f6b3f06ba276f10cc18b0931f1799f710578e2309348" dependencies = [ "proc-macro2", "quote", - "syn 2.0.117", + "syn 3.0.3", ] [[package]] @@ -5815,9 +5842,9 @@ dependencies = [ [[package]] name = "serde_json" -version = "1.0.149" +version = "1.0.151" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "83fc039473c5595ace860d8c4fafa220ff474b3fc6bfdb4293327f1a37e94d86" +checksum = "c841b55ecdae098c80dcae9cf767f6f8a0c2cdb3416bbef72181df4d0fe73f14" dependencies = [ "indexmap 2.14.0", "itoa", @@ -5849,9 +5876,9 @@ dependencies = [ [[package]] name = "serde_qs" -version = "1.1.1" +version = "1.1.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c2316d01592c3382277c5062105510e35e0a6bfb2851e30028485f7af8cf1240" +checksum = "67d525c8ff68aa99e5818302259bdd02d86d0303710616f39c0f44846ff6d332" dependencies = [ "itoa", "percent-encoding", @@ -6027,6 +6054,12 @@ version = "1.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0fda2ff0d084019ba4d7c6f371c95d8fd75ce3524c3cb8fb653a3023f6323e64" +[[package]] +name = "shlex" +version = "2.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f8fadd59c855ef2080decdef8ff161eb6661b86933c9d82e5ba29dc602a55aba" + [[package]] name = "sif-itree" version = "0.4.1" @@ -6104,7 +6137,7 @@ checksum = "0d585997b0ac10be3c5ee635f1bab02d512760d14b7c468801ac8a01d9ae5f1d" dependencies = [ "num-bigint", "num-traits", - "thiserror 2.0.18", + "thiserror 2.0.19", "time", ] @@ -6343,7 +6376,7 @@ version = "1.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "91bb902c47385644774ba89f7e7982d3a32cf1da4e8870bf633cdece450eced1" dependencies = [ - "thiserror 2.0.18", + "thiserror 2.0.19", ] [[package]] @@ -6368,6 +6401,17 @@ dependencies = [ "unicode-ident", ] +[[package]] +name = "syn" +version = "3.0.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "53e9bae58849f64dfa4f5d5ae372c8341f7305f82a3868709269343628b659a3" +dependencies = [ + "proc-macro2", + "quote", + "unicode-ident", +] + [[package]] name = "sync_wrapper" version = "1.0.2" @@ -6390,15 +6434,16 @@ dependencies = [ [[package]] name = "sysinfo" -version = "0.38.4" +version = "0.39.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "92ab6a2f8bfe508deb3c6406578252e491d299cbbf3bc0529ecc3313aee4a52f" +checksum = "d2071df9448915b71c4fe6d25deaf1c22f12bd234f01540b77312bb8e41361e6" dependencies = [ "libc", "memchr", "ntapi", "objc2-core-foundation", "objc2-io-kit", + "objc2-open-directory", "windows", ] @@ -6452,11 +6497,11 @@ dependencies = [ [[package]] name = "thiserror" -version = "2.0.18" +version = "2.0.19" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4288b5bcbc7920c07a1149a35cf9590a2aa808e0bc1eafaade0b80947865fbc4" +checksum = "09a43598840e33d5b0331f38c5e30d13bb11c11210a4b58f0d9b18a5a5eefcd9" dependencies = [ - "thiserror-impl 2.0.18", + "thiserror-impl 2.0.19", ] [[package]] @@ -6472,13 +6517,13 @@ dependencies = [ [[package]] name = "thiserror-impl" -version = "2.0.18" +version = "2.0.19" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ebc4ee7f67670e9b64d05fa4253e753e016c6c95ff35b89b7941d6b856dec1d5" +checksum = "43cbfe0cf76104d42a574802844187e84a305e531ed54455f11fbde0f10541cd" dependencies = [ "proc-macro2", "quote", - "syn 2.0.117", + "syn 3.0.3", ] [[package]] @@ -6557,9 +6602,9 @@ checksum = "1f3ccbac311fea05f86f61904b462b55fb3df8837a366dfc601a0161d0532f20" [[package]] name = "tokio" -version = "1.52.2" +version = "1.53.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "110a78583f19d5cdb2c5ccf321d1290344e71313c6c37d43520d386027d18386" +checksum = "202caea871b69668250d242070849eb495be178ed697a3e98aebce5bc81a0bed" dependencies = [ "bytes", "libc", @@ -6599,15 +6644,15 @@ version = "0.26.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1729aa945f29d91ba541258c8df89027d5792d85a8841fb65e8bf0f4ede4ef61" dependencies = [ - "rustls 0.23.40", + "rustls 0.23.42", "tokio", ] [[package]] name = "tokio-stream" -version = "0.1.18" +version = "0.1.19" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "32da49809aab5c3bc678af03902d4ccddea2a87d028d86392a4b1560c6906c70" +checksum = "a3d06f0b082ba57c26b79407372e57cf2a1e28124f78e9479fe80322cf53420b" dependencies = [ "futures-core", "pin-project-lite", @@ -6623,33 +6668,47 @@ checksum = "8f72a05e828585856dacd553fba484c242c46e391fb0e58917c942ee9202915c" dependencies = [ "futures-util", "log", - "rustls 0.23.40", + "tokio", + "tungstenite 0.29.0", +] + +[[package]] +name = "tokio-tungstenite" +version = "0.30.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "17a073bfed563fa236697a068031408a93cd9522e08abf9933ead3e73411bd71" +dependencies = [ + "futures-util", + "log", + "rustls 0.23.42", "rustls-native-certs", "rustls-pki-types", "tokio", "tokio-rustls 0.26.4", - "tungstenite", + "tungstenite 0.30.0", ] [[package]] name = "tokio-util" -version = "0.7.18" +version = "0.7.19" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9ae9cec805b01e8fc3fd2fe289f89149a9b66dd16786abd8b19cfa7b48cb0098" +checksum = "494815d09bf52b5548659851081238f0ca39ff638363907596da739561c62c52" dependencies = [ "bytes", "futures-core", "futures-io", "futures-sink", + "futures-util", + "libc", "pin-project-lite", "tokio", ] [[package]] name = "toml" -version = "1.1.2+spec-1.1.0" +version = "1.1.3+spec-1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "81f3d15e84cbcd896376e6730314d59fb5a87f31e4b038454184435cd57defee" +checksum = "53c96ecdfa941c8fc4fcaed14f99ada8ebed502eef533015095a07e3301d4c3c" dependencies = [ "indexmap 2.14.0", "serde_core", @@ -6699,14 +6758,14 @@ dependencies = [ "ordered_hash_map", "serde", "serde_json", - "thiserror 2.0.18", + "thiserror 2.0.19", ] [[package]] name = "toml_writer" -version = "1.1.1+spec-1.1.0" +version = "1.1.2+spec-1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "756daf9b1013ebe47a8776667b466417e2d4c5679d441c26230efd9ef78692db" +checksum = "7d56353a2a665ad0f41a421187180aab746c8c325620617ad883a99a1cbe66d2" [[package]] name = "tonic" @@ -6749,9 +6808,9 @@ dependencies = [ [[package]] name = "totp-rs" -version = "5.7.1" +version = "5.7.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a2b36a9dd327e9f401320a2cb4572cc76ff43742bcfc3291f871691050f140ba" +checksum = "50e69a15e21b2ff22c415446983978bded3244195f17d59cb113551c1e806f91" dependencies = [ "base32", "constant_time_eq", @@ -6871,7 +6930,7 @@ dependencies = [ "rand 0.9.4", "serde", "serde_json", - "thiserror 2.0.18", + "thiserror 2.0.19", "time", "tokio", "tracing", @@ -6978,7 +7037,7 @@ dependencies = [ [[package]] name = "transport" -version = "2.2.0" +version = "2.3.0-dev-48" dependencies = [ "anyhow", "axum", @@ -6990,13 +7049,13 @@ dependencies = [ "mogh_pki", "periphery_client", "pin-project-lite", - "rand 0.10.1", - "rustls 0.23.40", + "rand 0.10.2", + "rustls 0.23.42", "serde", "sha1 0.11.0", "sha2 0.11.0", "tokio", - "tokio-tungstenite", + "tokio-tungstenite 0.30.0", "tokio-util", "tracing", "url", @@ -7021,10 +7080,26 @@ dependencies = [ "httparse", "log", "rand 0.9.4", - "rustls 0.23.40", - "rustls-pki-types", "sha1 0.10.6", - "thiserror 2.0.18", + "thiserror 2.0.19", +] + +[[package]] +name = "tungstenite" +version = "0.30.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e48ac77174b19c110a50ab2128b24215ac9cb40e0e12e093fb602d175c569d22" +dependencies = [ + "bytes", + "data-encoding", + "http 1.4.0", + "httparse", + "log", + "rand 0.10.2", + "rustls 0.23.42", + "rustls-pki-types", + "sha1 0.11.0", + "thiserror 2.0.19", ] [[package]] @@ -7232,13 +7307,13 @@ dependencies = [ [[package]] name = "uuid" -version = "1.23.1" +version = "1.24.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ddd74a9687298c6858e9b88ec8935ec45d22e8fd5e6394fa1bd4e99a87789c76" +checksum = "bf3923a6f5c4c6382e0b653c4117f48d631ea17f38ed86e2a828e6f7412f5239" dependencies = [ "getrandom 0.4.2", "js-sys", - "rand 0.10.1", + "rand 0.10.2", "serde_core", "wasm-bindgen", ] @@ -7531,7 +7606,7 @@ version = "0.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f9b0540e91e49de3817c314da0dd3bc518093ceacc6ea5327cb0e1eb073e5189" dependencies = [ - "thiserror 2.0.18", + "thiserror 2.0.19", ] [[package]] @@ -7994,7 +8069,7 @@ checksum = "66fee0b777b0f5ac1c69bb06d361268faafa61cd4682ae064a171c16c433e9e4" [[package]] name = "xtask" -version = "2.2.0" +version = "2.3.0-dev-48" dependencies = [ "anyhow", "clap", @@ -8071,6 +8146,20 @@ name = "zeroize" version = "1.8.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b97154e67e32c85465826e8bcc1c59429aaaf107c1e4a9e53c8d8ccd5eff88d0" +dependencies = [ + "zeroize_derive", +] + +[[package]] +name = "zeroize_derive" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3c50655cbb0fe3fc43170059e702f1ce5e19b84cec58dc87b037a09935c2f328" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.117", +] [[package]] name = "zerotrie" diff --git a/Cargo.toml b/Cargo.toml index 5b0b7e3b43..286aa7dac3 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -9,9 +9,8 @@ members = [ ] [workspace.package] -version = "2.2.0" +version = "2.3.0-dev-48" edition = "2024" -authors = ["mbecker20 "] license = "GPL-3.0-or-later" repository = "https://github.com/moghtech/komodo" homepage = "https://komo.do" @@ -34,34 +33,34 @@ slack = { version = "2.0.0", package = "slack_client_rs", default-features = fal mogh_error = { version = "1.0.4", default-features = false } derive_default_builder = "0.1.8" async_timing_util = "1.1.0" -mogh_auth_client = "1.5.0" -mogh_auth_server = "1.5.0" +mogh_auth_client = "1.7.0" +mogh_auth_server = "1.7.0" mogh_secret_file = "1.0.1" mogh_validations = "1.0.1" mogh_rate_limit = "1.0.1" partial_derive2 = "0.5.0" -mongo_indexed = "2.0.2" +mongo_indexed = "2.1.0" mogh_resolver = "1.0.0" mogh_config = "1.1.0" -mogh_logger = "1.3.3" +mogh_logger = "1.3.4" mogh_server = "1.5.0" toml_pretty = "2.0.0" mogh_cache = "1.1.2" mogh_pki = "1.1.3" -mungos = "3.2.2" +mungos = "3.7.0" svi = "1.2.0" # ASYNC -reqwest = { version = "0.13.3", default-features = false, features = ["json", "stream", "form", "query", "rustls"] } -tokio = { version = "1.52.2", features = ["full"] } -tokio-util = { version = "0.7.18", features = ["io", "codec"] } -tokio-stream = { version = "0.1.18", features = ["sync"] } +reqwest = { version = "0.13.4", default-features = false, features = ["json", "stream", "form", "query", "rustls"] } +tokio = { version = "1.53.1", features = ["full"] } +tokio-util = { version = "0.7.19", features = ["io", "codec"] } +tokio-stream = { version = "0.1.19", features = ["sync"] } pin-project-lite = "0.2.17" -futures-util = "0.3.32" -arc-swap = "1.9.0" +futures-util = "0.3.33" +arc-swap = "1.9.2" # SERVER -tokio-tungstenite = { version = "0.29.0", features = ["rustls-tls-native-roots"] } +tokio-tungstenite = { version = "0.30.0", features = ["rustls-tls-native-roots"] } axum = { version = "0.8.9", features = ["ws", "json", "macros"] } axum-extra = { version = "0.12.6", features = ["typed-header"] } @@ -72,37 +71,40 @@ utoipa = "5.5.0" # SER/DE ipnetwork = { version = "0.21.1", features = ["serde"] } indexmap = { version = "2.14.0", features = ["serde"] } -serde = { version = "1.0.227", features = ["derive"] } +serde = { version = "1.0.229", features = ["derive"] } strum = { version = "0.28.0", features = ["derive"] } bson = { version = "2.15.0" } # must keep in sync with mongodb version -toml = "1.1.2" +toml = "1.1.3" serde_yaml_ng = "0.10.0" -serde_json = "1.0.149" -serde_qs = "1.1.0" +serde_json = "1.0.151" +serde_qs = "1.1.2" url = "2.5.8" # ERROR -anyhow = "1.0.102" -thiserror = "2.0.18" +anyhow = "1.0.104" +thiserror = "2.0.19" + +# SYSTEM +libc = "0.2.189" # LOGGING tracing = "0.1.44" # CONFIG -clap = { version = "4.6.1", features = ["derive"] } +clap = { version = "4.6.4", features = ["derive"] } dotenvy = "0.15.7" envy = "0.4.2" # CRYPTO / AUTH -uuid = { version = "1.23.1", features = ["v4", "fast-rng", "serde"] } -rustls = { version = "0.23.40", features = ["aws-lc-rs"] } -data-encoding = "2.10.0" +uuid = { version = "1.24.0", features = ["v4", "fast-rng", "serde"] } +rustls = { version = "0.23.42", features = ["aws-lc-rs"] } +data-encoding = "2.11.0" urlencoding = "2.1.3" -bcrypt = "0.19.0" +bcrypt = "0.19.2" hmac = "0.13.0" sha1 = "0.11.0" sha2 = "0.11.0" -rand = "0.10.1" +rand = "0.10.2" hex = "0.4.3" # SYSTEM @@ -111,18 +113,18 @@ portable-pty = "0.9.0" shell-escape = "0.1.5" crossterm = "0.29.0" bollard = "0.21.0" -sysinfo = "0.38.4" -shlex = "1.3.0" +sysinfo = "0.39.6" +shlex = "2.0.1" # CLOUD -aws-config = "1.8.16" -aws-sdk-ec2 = "1.224.0" -aws-credential-types = "1.2.14" +aws-config = "1.10.0" +aws-sdk-ec2 = "1.241.0" +aws-credential-types = "1.3.0" ## CRON english-to-cron = "0.1.7" chrono-tz = "0.10.4" -chrono = "0.4.44" +chrono = "0.4.45" croner = "3.0.1" # MISC @@ -133,8 +135,8 @@ comfy-table = "7.2.2" typeshare = "1.0.5" wildcard = "0.3.0" colored = "3.1.1" -bytes = "1.11.1" -regex = "1.12.3" +bytes = "1.12.1" +regex = "1.13.1" [profile.release] strip = "debuginfo" \ No newline at end of file diff --git a/action/deploy-fe.ts b/action/deploy-ui.ts similarity index 51% rename from action/deploy-fe.ts rename to action/deploy-ui.ts index 8732a40bd0..1f3311e875 100644 --- a/action/deploy-fe.ts +++ b/action/deploy-ui.ts @@ -1,4 +1,4 @@ -const cmd = "km run -y action deploy-komodo-fe-change"; +const cmd = "km run -y action deploy-komodo-ui-change"; new Deno.Command("bash", { args: ["-c", cmd], }).spawn(); \ No newline at end of file diff --git a/bin/binaries.Dockerfile b/bin/binaries.Dockerfile index 18b3cef3e9..4188bb0f8e 100644 --- a/bin/binaries.Dockerfile +++ b/bin/binaries.Dockerfile @@ -1,7 +1,7 @@ ## Builds the Komodo Core, Periphery, and Util binaries ## for a specific architecture. Requires OpenSSL 3 or later. -FROM rust:1.95.0-bookworm AS builder +FROM rust:1.97.1-bookworm AS builder RUN cargo install cargo-strip WORKDIR /builder diff --git a/bin/chef.binaries.Dockerfile b/bin/chef.binaries.Dockerfile index 9933981b12..89acc13a7e 100644 --- a/bin/chef.binaries.Dockerfile +++ b/bin/chef.binaries.Dockerfile @@ -3,7 +3,7 @@ ## Uses chef for dependency caching to help speed up back-to-back builds. -FROM lukemathwalker/cargo-chef:latest-rust-1.95.0-bookworm AS chef +FROM lukemathwalker/cargo-chef:latest-rust-1.97.1-bookworm AS chef WORKDIR /builder # Plan just the RECIPE to see if things have changed diff --git a/bin/cli/Cargo.toml b/bin/cli/Cargo.toml index 8e13a4476b..04a3926276 100644 --- a/bin/cli/Cargo.toml +++ b/bin/cli/Cargo.toml @@ -3,7 +3,6 @@ name = "komodo_cli" description = "Command line tool for Komodo" version.workspace = true edition.workspace = true -authors.workspace = true license.workspace = true repository.workspace = true homepage.workspace = true diff --git a/bin/cli/aio.Dockerfile b/bin/cli/aio.Dockerfile index df0aee90d0..d1564d5446 100644 --- a/bin/cli/aio.Dockerfile +++ b/bin/cli/aio.Dockerfile @@ -1,4 +1,4 @@ -FROM rust:1.95.0-trixie AS builder +FROM rust:1.97.1-trixie AS builder RUN cargo install cargo-strip WORKDIR /builder diff --git a/bin/cli/src/command/container.rs b/bin/cli/src/command/container.rs index df571d7b06..5f037b1b82 100644 --- a/bin/cli/src/command/container.rs +++ b/bin/cli/src/command/container.rs @@ -7,16 +7,16 @@ use futures_util::{ FutureExt, TryStreamExt, stream::FuturesUnordered, }; use komodo_client::{ - api::read::{ - InspectDockerContainer, ListAllDockerContainers, ListServers, - }, + api::read::{self, ListAllContainers, ListServers}, entities::{ config::cli::args::container::{ Container, ContainerCommand, InspectContainer, }, docker::{ self, - container::{ContainerListItem, ContainerStateStatusEnum}, + container::{ + ContainerListItem, ContainerSortBy, ContainerStateStatusEnum, + }, }, }, }; @@ -44,11 +44,12 @@ async fn list_containers( down, links, reverse, - containers: names, + containers: terms, images, networks, servers, format, + page, command: _, }: &Container, ) -> anyhow::Result<()> { @@ -60,9 +61,16 @@ async fn list_containers( .into_iter() .map(|s| (s.id.clone(), s)) .collect::>())), - client.read(ListAllDockerContainers { + client.read(ListAllContainers { servers: Default::default(), - containers: Default::default(), + tags: Default::default(), + terms: terms.clone(), + state: Default::default(), + limit: Some(100), + // Page is more naturally given starting as 1, 2, 3. + page: if *page == 0 { 0 } else { *page - 1 }, + sort_by: ContainerSortBy::Server, + sort_desc: false, }), )?; @@ -78,7 +86,6 @@ async fn list_containers( (Some(server.name.as_str()), c) }); - let names = parse_wildcards(names); let servers = parse_wildcards(servers); let images = parse_wildcards(images); let networks = parse_wildcards(networks); @@ -105,7 +112,6 @@ async fn list_containers( ); state_check && network_check - && matches_wildcards(&names, &[c.name.as_str()]) && matches_wildcards( &servers, &server_name @@ -145,9 +151,15 @@ pub async fn inspect_container( .into_iter() .map(|s| (s.id.clone(), s)) .collect::>())), - client.read(ListAllDockerContainers { - servers: Default::default(), - containers: Default::default() + client.read(ListAllContainers { + servers: inspect.servers.clone(), + tags: Default::default(), + terms: vec![inspect.container.clone()], + state: Default::default(), + limit: Some(0), + page: 0, + sort_by: ContainerSortBy::Server, + sort_desc: false, }), )?; @@ -162,25 +174,11 @@ pub async fn inspect_container( c.server_id = Some(server.name.clone()); }); - let names = [inspect.container.to_string()]; - let names = parse_wildcards(&names); - let servers = parse_wildcards(&inspect.servers); - let mut containers = containers .into_iter() - .filter(|c| { - matches_wildcards(&names, &[c.name.as_str()]) - && matches_wildcards( - &servers, - &c.server_id - .as_deref() - .map(|i| vec![i]) - .unwrap_or_default(), - ) - }) .map(|c| async move { client - .read(InspectDockerContainer { + .read(read::InspectContainer { container: c.name, server: c.server_id.context("No server...")?, }) diff --git a/bin/cli/src/command/execute.rs b/bin/cli/src/command/execute.rs index 87abeb9552..ad49395487 100644 --- a/bin/cli/src/command/execute.rs +++ b/bin/cli/src/command/execute.rs @@ -81,7 +81,9 @@ pub async fn handle( let res = handle_execution!( execute: [ RunAction, + CancelAction, RunProcedure, + CancelProcedure, RunBuild, CancelBuild, Deploy, diff --git a/bin/cli/src/command/list.rs b/bin/cli/src/command/list.rs index b761fdff1f..ddb2fb4a0c 100644 --- a/bin/cli/src/command/list.rs +++ b/bin/cli/src/command/list.rs @@ -7,7 +7,8 @@ use komodo_client::{ api::read::{ ListActions, ListAlerters, ListBuilders, ListBuilds, ListDeployments, ListProcedures, ListRepos, ListResourceSyncs, - ListSchedules, ListServers, ListStacks, ListTags, ListTerminals, + ListSchedules, ListServers, ListStacks, ListSwarms, ListTags, + ListTerminals, }, entities::{ ResourceTargetVariant, @@ -31,6 +32,7 @@ use komodo_client::{ schedule::Schedule, server::{ServerListItem, ServerListItemInfo, ServerState}, stack::{StackListItem, StackListItemInfo, StackState}, + swarm::{SwarmListItem, SwarmListItemInfo, SwarmState}, sync::{ ResourceSyncListItem, ResourceSyncListItemInfo, ResourceSyncState, @@ -52,28 +54,46 @@ pub async fn handle(list: &args::list::List) -> anyhow::Result<()> { match &list.command { None => list_all(list).await, Some(ListCommand::Servers(filters)) => { - list_resources::(filters, false).await + list_resources::(filters, list.page, false) + .await + } + Some(ListCommand::Swarms(filters)) => { + list_resources::(filters, list.page, false).await } Some(ListCommand::Stacks(filters)) => { - list_resources::(filters, false).await + list_resources::(filters, list.page, false).await } Some(ListCommand::Deployments(filters)) => { - list_resources::(filters, false).await + list_resources::(filters, list.page, false) + .await } Some(ListCommand::Builds(filters)) => { - list_resources::(filters, false).await + list_resources::(filters, list.page, false).await } Some(ListCommand::Repos(filters)) => { - list_resources::(filters, false).await + list_resources::(filters, list.page, false).await } Some(ListCommand::Procedures(filters)) => { - list_resources::(filters, false).await + list_resources::(filters, list.page, false) + .await } Some(ListCommand::Actions(filters)) => { - list_resources::(filters, false).await + list_resources::(filters, list.page, false) + .await } Some(ListCommand::Syncs(filters)) => { - list_resources::(filters, false).await + list_resources::( + filters, list.page, false, + ) + .await + } + Some(ListCommand::Builders(filters)) => { + list_resources::(filters, list.page, false) + .await + } + Some(ListCommand::Alerters(filters)) => { + list_resources::(filters, list.page, false) + .await } Some(ListCommand::Terminals(filters)) => { list_terminals(filters).await @@ -81,12 +101,6 @@ pub async fn handle(list: &args::list::List) -> anyhow::Result<()> { Some(ListCommand::Schedules(filters)) => { list_schedules(filters).await } - Some(ListCommand::Builders(filters)) => { - list_resources::(filters, false).await - } - Some(ListCommand::Alerters(filters)) => { - list_resources::(filters, false).await - } } } @@ -97,6 +111,7 @@ async fn list_all(list: &args::list::List) -> anyhow::Result<()> { let ( tags, mut servers, + mut swarms, mut stacks, mut deployments, mut builds, @@ -109,14 +124,15 @@ async fn list_all(list: &args::list::List) -> anyhow::Result<()> { .into_iter() .map(|t| (t.id, t.name)) .collect::>())), - ServerListItem::list(client, &filters, true), - StackListItem::list(client, &filters, true), - DeploymentListItem::list(client, &filters, true), - BuildListItem::list(client, &filters, true), - RepoListItem::list(client, &filters, true), - ProcedureListItem::list(client, &filters, true), - ActionListItem::list(client, &filters, true), - ResourceSyncListItem::list(client, &filters, true), + ServerListItem::list(client, &filters, list.page, true), + SwarmListItem::list(client, &filters, list.page, true), + StackListItem::list(client, &filters, list.page, true), + DeploymentListItem::list(client, &filters, list.page, true), + BuildListItem::list(client, &filters, list.page, true), + RepoListItem::list(client, &filters, list.page, true), + ProcedureListItem::list(client, &filters, list.page, true), + ActionListItem::list(client, &filters, list.page, true), + ResourceSyncListItem::list(client, &filters, list.page, true), )?; if !servers.is_empty() { @@ -125,6 +141,12 @@ async fn list_all(list: &args::list::List) -> anyhow::Result<()> { println!(); } + if !swarms.is_empty() { + fix_tags(&mut swarms, &tags); + print_items(swarms, filters.format, list.links)?; + println!(); + } + if !stacks.is_empty() { fix_tags(&mut stacks, &tags); print_items(stacks, filters.format, list.links)?; @@ -172,6 +194,7 @@ async fn list_all(list: &args::list::List) -> anyhow::Result<()> { async fn list_resources( filters: &ResourceFilters, + page: u64, minimal: bool, ) -> anyhow::Result<()> where @@ -180,7 +203,7 @@ where { let client = crate::command::komodo_client().await?; let (mut resources, tags) = tokio::try_join!( - T::list(client, filters, minimal), + T::list(client, filters, page, minimal), client.read(ListTags::default()).map(|res| res.map(|res| res .into_iter() .map(|t| (t.id, t.name)) @@ -205,6 +228,8 @@ async fn list_terminals( .read(ListTerminals { target: None, use_names: true, + limit: Some(0), + ..Default::default() }) .await?; if !terminals.is_empty() { @@ -222,6 +247,8 @@ async fn list_schedules( .read(ListSchedules { tags: filters.tags.clone(), tag_behavior: Default::default(), + limit: Some(0), + ..Default::default() }) .map(|res| res.map(|res| res .into_iter() @@ -279,6 +306,7 @@ where async fn list( client: &KomodoClient, filters: &ResourceFilters, + page: u64, // For use with root `km ls` minimal: bool, ) -> anyhow::Result>>; @@ -291,6 +319,7 @@ impl ListResources for ServerListItem { async fn list( client: &KomodoClient, filters: &ResourceFilters, + page: u64, _minimal: bool, ) -> anyhow::Result> { let servers = client @@ -300,6 +329,10 @@ impl ListResources for ServerListItem { // .tag_behavior(TagQueryBehavior::Any) .templates(filters.templates) .build(), + limit: None, + page: page.saturating_sub(1), + sort_by: Default::default(), + sort_desc: false, }) .await?; let names = parse_wildcards(&filters.names); @@ -329,17 +362,68 @@ impl ListResources for ServerListItem { } } +impl ListResources for SwarmListItem { + type Info = SwarmListItemInfo; + async fn list( + client: &KomodoClient, + filters: &ResourceFilters, + page: u64, + _minimal: bool, + ) -> anyhow::Result> { + let servers = client + .read(ListSwarms { + query: ResourceQuery::builder() + .tags(filters.tags.clone()) + // .tag_behavior(TagQueryBehavior::Any) + .templates(filters.templates) + .build(), + limit: None, + page: page.saturating_sub(1), + sort_by: Default::default(), + sort_desc: false, + }) + .await?; + let names = parse_wildcards(&filters.names); + let server_wildcards = parse_wildcards(&filters.servers); + let mut servers = servers + .into_iter() + .filter(|server| { + let state_check = if filters.all { + true + } else if filters.down { + !matches!(server.info.state, SwarmState::Healthy) + } else if filters.in_progress { + false + } else { + matches!(server.info.state, SwarmState::Healthy) + }; + let name_items = &[server.name.as_str()]; + state_check + && matches_wildcards(&names, name_items) + && matches_wildcards(&server_wildcards, name_items) + }) + .collect::>(); + servers.sort_by(|a, b| { + a.info.state.cmp(&b.info.state).then(a.name.cmp(&b.name)) + }); + Ok(servers) + } +} + impl ListResources for StackListItem { type Info = StackListItemInfo; async fn list( client: &KomodoClient, filters: &ResourceFilters, + page: u64, _minimal: bool, ) -> anyhow::Result> { let (servers, mut stacks) = tokio::try_join!( client .read(ListServers { query: ResourceQuery::builder().build(), + limit: Some(0), + ..Default::default() }) .map(|res| res.map(|res| res .into_iter() @@ -351,6 +435,10 @@ impl ListResources for StackListItem { // .tag_behavior(TagQueryBehavior::Any) .templates(filters.templates) .build(), + limit: None, + page: page.saturating_sub(1), + sort_by: Default::default(), + sort_desc: false }) )?; stacks.iter_mut().for_each(|stack| { @@ -406,12 +494,15 @@ impl ListResources for DeploymentListItem { async fn list( client: &KomodoClient, filters: &ResourceFilters, + page: u64, _minimal: bool, ) -> anyhow::Result> { let (servers, mut deployments) = tokio::try_join!( client .read(ListServers { query: ResourceQuery::builder().build(), + limit: Some(0), + ..Default::default() }) .map(|res| res.map(|res| res .into_iter() @@ -423,6 +514,10 @@ impl ListResources for DeploymentListItem { // .tag_behavior(TagQueryBehavior::Any) .templates(filters.templates) .build(), + limit: None, + page: page.saturating_sub(1), + sort_by: Default::default(), + sort_desc: false }) )?; deployments.iter_mut().for_each(|deployment| { @@ -479,12 +574,15 @@ impl ListResources for BuildListItem { async fn list( client: &KomodoClient, filters: &ResourceFilters, + page: u64, minimal: bool, ) -> anyhow::Result> { let (builders, mut builds) = tokio::try_join!( client .read(ListBuilders { query: ResourceQuery::builder().build(), + limit: Some(0), + ..Default::default() }) .map(|res| res.map(|res| res .into_iter() @@ -496,6 +594,10 @@ impl ListResources for BuildListItem { // .tag_behavior(TagQueryBehavior::Any) .templates(filters.templates) .build(), + limit: None, + page: page.saturating_sub(1), + sort_by: Default::default(), + sort_desc: false }) )?; builds.iter_mut().for_each(|build| { @@ -533,10 +635,7 @@ impl ListResources for BuildListItem { }) .collect::>(); builds.sort_by(|a, b| { - a.name - .cmp(&b.name) - .then(a.info.builder_id.cmp(&b.info.builder_id)) - .then(a.info.state.cmp(&b.info.state)) + a.info.state.cmp(&b.info.state).then(a.name.cmp(&b.name)) }); Ok(builds) } @@ -547,6 +646,7 @@ impl ListResources for RepoListItem { async fn list( client: &KomodoClient, filters: &ResourceFilters, + page: u64, minimal: bool, ) -> anyhow::Result> { let names = parse_wildcards(&filters.names); @@ -557,6 +657,10 @@ impl ListResources for RepoListItem { // .tag_behavior(TagQueryBehavior::Any) .templates(filters.templates) .build(), + limit: None, + page: page.saturating_sub(1), + sort_by: Default::default(), + sort_desc: false, }) .await? .into_iter() @@ -595,6 +699,7 @@ impl ListResources for ProcedureListItem { async fn list( client: &KomodoClient, filters: &ResourceFilters, + page: u64, minimal: bool, ) -> anyhow::Result> { let names = parse_wildcards(&filters.names); @@ -605,6 +710,10 @@ impl ListResources for ProcedureListItem { // .tag_behavior(TagQueryBehavior::Any) .templates(filters.templates) .build(), + limit: None, + page: page.saturating_sub(1), + sort_by: Default::default(), + sort_desc: false, }) .await? .into_iter() @@ -632,7 +741,7 @@ impl ListResources for ProcedureListItem { (Some(a), Some(b)) => return a.cmp(&b), (None, None) => {} } - a.name.cmp(&b.name).then(a.info.state.cmp(&b.info.state)) + a.info.state.cmp(&b.info.state).then(a.name.cmp(&b.name)) }); Ok(procedures) } @@ -643,6 +752,7 @@ impl ListResources for ActionListItem { async fn list( client: &KomodoClient, filters: &ResourceFilters, + page: u64, minimal: bool, ) -> anyhow::Result> { let names = parse_wildcards(&filters.names); @@ -653,6 +763,10 @@ impl ListResources for ActionListItem { // .tag_behavior(TagQueryBehavior::Any) .templates(filters.templates) .build(), + limit: None, + page: page.saturating_sub(1), + sort_by: Default::default(), + sort_desc: false, }) .await? .into_iter() @@ -680,7 +794,7 @@ impl ListResources for ActionListItem { (Some(a), Some(b)) => return a.cmp(&b), (None, None) => {} } - a.name.cmp(&b.name).then(a.info.state.cmp(&b.info.state)) + a.info.state.cmp(&b.info.state).then(a.name.cmp(&b.name)) }); Ok(actions) } @@ -691,6 +805,7 @@ impl ListResources for ResourceSyncListItem { async fn list( client: &KomodoClient, filters: &ResourceFilters, + page: u64, minimal: bool, ) -> anyhow::Result> { let names = parse_wildcards(&filters.names); @@ -701,6 +816,10 @@ impl ListResources for ResourceSyncListItem { // .tag_behavior(TagQueryBehavior::Any) .templates(filters.templates) .build(), + limit: None, + page: page.saturating_sub(1), + sort_by: Default::default(), + sort_desc: false, }) .await? .into_iter() @@ -725,7 +844,7 @@ impl ListResources for ResourceSyncListItem { }) .collect::>(); syncs.sort_by(|a, b| { - a.name.cmp(&b.name).then(a.info.state.cmp(&b.info.state)) + a.info.state.cmp(&b.info.state).then(a.name.cmp(&b.name)) }); Ok(syncs) } @@ -736,6 +855,7 @@ impl ListResources for BuilderListItem { async fn list( client: &KomodoClient, filters: &ResourceFilters, + page: u64, minimal: bool, ) -> anyhow::Result> { let names = parse_wildcards(&filters.names); @@ -746,6 +866,10 @@ impl ListResources for BuilderListItem { // .tag_behavior(TagQueryBehavior::Any) .templates(filters.templates) .build(), + limit: None, + page: page.saturating_sub(1), + sort_by: Default::default(), + sort_desc: false, }) .await? .into_iter() @@ -768,6 +892,7 @@ impl ListResources for AlerterListItem { async fn list( client: &KomodoClient, filters: &ResourceFilters, + page: u64, minimal: bool, ) -> anyhow::Result> { let names = parse_wildcards(&filters.names); @@ -778,6 +903,10 @@ impl ListResources for AlerterListItem { // .tag_behavior(TagQueryBehavior::Any) .templates(filters.templates) .build(), + limit: None, + page: page.saturating_sub(1), + sort_by: Default::default(), + sort_desc: false, }) .await? .into_iter() @@ -832,6 +961,39 @@ impl PrintTable for ResourceListItem { } } +impl PrintTable for ResourceListItem { + fn header(links: bool) -> &'static [&'static str] { + if links { + &["Swarm", "State", "Tags", "Link"] + } else { + &["Swarm", "State", "Tags"] + } + } + fn row(self, links: bool) -> Vec { + let color = match self.info.state { + SwarmState::Healthy => Color::Green, + SwarmState::Unhealthy => Color::Red, + SwarmState::Down => Color::Blue, + SwarmState::Unknown => Color::Magenta, + }; + let mut res = vec![ + Cell::new(self.name).add_attribute(Attribute::Bold), + Cell::new(self.info.state.to_string()) + .fg(color) + .add_attribute(Attribute::Bold), + Cell::new(self.tags.join(", ")), + ]; + if links { + res.push(Cell::new(resource_link( + &cli_config().host, + ResourceTargetVariant::Server, + &self.id, + ))) + } + res + } +} + impl PrintTable for ResourceListItem { fn header(links: bool) -> &'static [&'static str] { if links { diff --git a/bin/cli/src/command/terminal.rs b/bin/cli/src/command/terminal.rs index f286e4ca39..b7c6c4e59d 100644 --- a/bin/cli/src/command/terminal.rs +++ b/bin/cli/src/command/terminal.rs @@ -2,7 +2,7 @@ use anyhow::{Context, anyhow}; use colored::Colorize; use komodo_client::{ api::{ - read::{GetServer, ListAllDockerContainers, ListServers}, + read::{GetServer, ListAllContainers, ListServers}, terminal::InitTerminal, }, entities::{ @@ -118,6 +118,8 @@ async fn get_server( server: Option, container: &str, ) -> anyhow::Result { + // Don't need to find server for terminal, + // user provided it, early return. if let Some(server) = server { return Ok(server); } @@ -125,9 +127,15 @@ async fn get_server( let client = super::komodo_client().await?; let mut containers = client - .read(ListAllDockerContainers { + .read(ListAllContainers { servers: Default::default(), - containers: vec![container.to_string()], + tags: Default::default(), + terms: vec![container.to_string()], + state: Default::default(), + limit: Some(0), + page: 0, + sort_by: Default::default(), + sort_desc: false, }) .await?; @@ -156,6 +164,8 @@ async fn get_server( let servers = client .read(ListServers { query: ServerQuery::builder().names(servers).build(), + limit: Some(0), + ..Default::default() }) .await? .into_iter() diff --git a/bin/core/Cargo.toml b/bin/core/Cargo.toml index 19cd11b64e..a9e1f4847a 100644 --- a/bin/core/Cargo.toml +++ b/bin/core/Cargo.toml @@ -2,7 +2,6 @@ name = "komodo_core" version.workspace = true edition.workspace = true -authors.workspace = true license.workspace = true homepage.workspace = true repository.workspace = true diff --git a/bin/core/aio.Dockerfile b/bin/core/aio.Dockerfile index 0db1611e8c..2cbcc7b5f9 100644 --- a/bin/core/aio.Dockerfile +++ b/bin/core/aio.Dockerfile @@ -1,7 +1,7 @@ ## All in one, multi stage compile + runtime Docker build for your architecture. # Build Core -FROM rust:1.95.0-trixie AS core-builder +FROM rust:1.97.1-trixie AS core-builder RUN cargo install cargo-strip WORKDIR /builder diff --git a/bin/core/src/api/execute/action.rs b/bin/core/src/api/execute/action.rs index 3764efadbe..8d602ef326 100644 --- a/bin/core/src/api/execute/action.rs +++ b/bin/core/src/api/execute/action.rs @@ -4,23 +4,26 @@ use std::{ sync::OnceLock, }; -use anyhow::Context; -use command::run_komodo_standard_command; -use database::mungos::{ - by_id::update_one_by_id, mongodb::bson::to_document, +use anyhow::Context as _; +use command::{CommandOptions, run_komodo_standard_command}; +use database::{ + bson::doc, + mungos::{by_id::update_one_by_id, mongodb::bson::to_document}, }; use interpolate::Interpolator; use komodo_client::{ - api::execute::{BatchExecutionResponse, BatchRunAction, RunAction}, + api::execute::{ + BatchExecutionResponse, BatchRunAction, CancelAction, RunAction, + }, entities::{ - FileFormat, JsonObject, + FileFormat, JsonObject, Operation, action::Action, alert::{Alert, AlertData, SeverityLevel}, config::core::CoreConfig, komodo_timestamp, permission::PermissionLevel, random_string, - update::Update, + update::{Update, UpdateStatus}, user::action_user, }, parsers::parse_key_value_list, @@ -34,6 +37,7 @@ use mogh_auth_server::api::manage::api_key::{ use mogh_config::merge_objects; use mogh_resolver::Resolve; use tokio::fs; +use tokio_util::sync::CancellationToken; use crate::{ alert::send_alerts, @@ -46,7 +50,7 @@ use crate::{ }, permission::get_check_permissions, resource::refresh_action_state_cache, - state::{action_states, db_client}, + state::{action_cancel_cache, action_states, db_client}, }; use super::ExecuteArgs; @@ -69,6 +73,7 @@ impl Resolve for BatchRunAction { task_id = task_id.to_string(), operator = user.id, pattern = self.pattern, + tags = self.tags.join(","), ) )] async fn resolve( @@ -76,8 +81,12 @@ impl Resolve for BatchRunAction { ExecuteArgs { user, task_id, .. }: &ExecuteArgs, ) -> mogh_error::Result { Ok( - super::batch_execute::(&self.pattern, user) - .await?, + super::batch_execute::( + &self.pattern, + self.tags, + user, + ) + .await?, ) } } @@ -116,7 +125,7 @@ impl Resolve for RunAction { // This will set action state back to default when dropped. // Will also check to ensure action not already busy before updating. - let _action_guard = action_state.update_custom( + let action_guard = action_state.update_custom( |state| state.running += 1, |state| state.running -= 1, false, @@ -197,14 +206,20 @@ impl Resolve for RunAction { "" }; + let cancel = CancellationToken::new(); + + action_cancel_cache() + .insert(update.id.clone(), cancel.clone()) + .await; + let mut res = run_komodo_standard_command( // Keep this stage name as is, the UI will find the latest update log by matching the stage name "Execute Action", - None, format!( "deno run --allow-all{https_cert_flag}{reload} {}", path.display() ), + CommandOptions::default().cancel(cancel), ) .await; @@ -218,7 +233,7 @@ impl Resolve for RunAction { update.logs.push(res); update.finalize(); - mogh_error::Ok(update) + mogh_error::Ok(()) } .await; @@ -231,7 +246,13 @@ impl Resolve for RunAction { ); }; - let update = res?; + action_cancel_cache().remove(&update.id).await; + + // Drop action guard before updating + // clients to requery action state + drop(action_guard); + + res?; // Need to manually update the update before cache refresh, // and before broadcast with update_update. @@ -459,3 +480,65 @@ fn parse_action_arguments( .context("Failed to parse Json to action args"), } } + +impl Resolve for CancelAction { + #[instrument( + "CancelAction", + skip_all, + fields( + task_id = task_id.to_string(), + operator = user.id, + update_id = update.id, + action = self.action, + ) + )] + async fn resolve( + self, + ExecuteArgs { + user, + update, + task_id, + }: &ExecuteArgs, + ) -> Result { + let action = get_check_permissions::( + &self.action, + user, + PermissionLevel::Execute.into(), + ) + .await?; + + let update_id = if let Some(update_id) = self.update_id + && !update_id.is_empty() + { + update_id + } else { + db_client() + .updates + .find_one(doc! { + "target.type": "Action", + "target.id": &action.id, + "operation": Operation::RunAction.as_ref(), + "status": UpdateStatus::InProgress.as_ref(), + }) + .await? + .context("No active run to cancel found")? + .id + }; + + action_cancel_cache() + .get(&update_id) + .await + .context("Action run cancel token not found")? + .cancel(); + + let mut update = update.clone(); + update.push_simple_log( + "Cancel Triggered", + "The action cancel has been triggered.", + ); + update.finalize(); + update_update(update.clone()).await?; + + Ok(update) + } +} diff --git a/bin/core/src/api/execute/alerter.rs b/bin/core/src/api/execute/alerter.rs index 8e70da7018..791e1f2a3a 100644 --- a/bin/core/src/api/execute/alerter.rs +++ b/bin/core/src/api/execute/alerter.rs @@ -114,6 +114,8 @@ impl Resolve for SendAlert { ) -> Result { let alerters = list_full_for_user::( Default::default(), + None, + None, user, PermissionLevel::Read.into(), &[], diff --git a/bin/core/src/api/execute/build.rs b/bin/core/src/api/execute/build.rs index 4d511b82f3..9b5efc059e 100644 --- a/bin/core/src/api/execute/build.rs +++ b/bin/core/src/api/execute/build.rs @@ -28,7 +28,7 @@ use komodo_client::{ alert::{Alert, AlertData, SeverityLevel}, all_logs_success, build::{Build, BuildConfig}, - builder::{Builder, BuilderConfig}, + builder::Builder, deployment::DeploymentState, komodo_timestamp, optional_string, permission::PermissionLevel, @@ -78,6 +78,7 @@ impl Resolve for BatchRunBuild { task_id = task_id.to_string(), operator = user.id, pattern = self.pattern, + tags = self.tags.join(","), ) )] async fn resolve( @@ -85,8 +86,12 @@ impl Resolve for BatchRunBuild { ExecuteArgs { user, task_id, .. }: &ExecuteArgs, ) -> mogh_error::Result { Ok( - super::batch_execute::(&self.pattern, user) - .await?, + super::batch_execute::( + &self.pattern, + self.tags, + user, + ) + .await?, ) } } @@ -148,7 +153,7 @@ impl Resolve for RunBuild { // This will set action state back to default when dropped. // Will also check to ensure build not already busy before updating. - let _action_guard = + let action_guard = action_state.update(|state| state.building = true)?; if build.config.auto_increment_version { @@ -175,9 +180,6 @@ impl Resolve for RunBuild { let builder = resource::get::(&build.config.builder_id).await?; - let is_server_builder = - matches!(&builder.config, BuilderConfig::Server(_)); - tokio::spawn(async move { let poll = async { loop { @@ -186,19 +188,13 @@ impl Resolve for RunBuild { id = cancel_recv.recv() => id? }; if incoming_build_id == build_id { - if is_server_builder { - update.push_error_log("Cancel acknowledged", "Build cancellation is not possible on server builders at this time. Use an AWS builder to enable this feature."); - } else { - update.push_simple_log("Cancel acknowledged", "The build cancellation has been queued, it may still take some time."); - } + update.push_simple_log("Cancel acknowledged", "The build cancellation has been queued, it may still take some time."); update.finalize(); let id = update.id.clone(); if let Err(e) = update_update(update).await { warn!("Failed to modify Update {id} on db | {e:#}"); } - if !is_server_builder { - cancel_clone.cancel(); - } + cancel_clone.cancel(); return Ok(()); } } @@ -216,7 +212,7 @@ impl Resolve for RunBuild { build.name.clone(), Some(build.config.version), builder, - &mut update, + Some(&mut update), ) .await { @@ -273,11 +269,11 @@ impl Resolve for RunBuild { replacers: Default::default(), }) => res, _ = cancel.cancelled() => { - debug!("Build cancelled during clone, cleaning up builder"); - update.push_error_log("Build cancelled", String::from("user cancelled build during repo clone")); + debug!("Build cancelled during repo clone, cleaning up builder"); + update.push_error_log("Build cancelled", String::from("Build cancelled during repo clone")); cleanup_builder_instance(periphery, cleanup_data, &mut update) .await; - info!("Builder cleaned up"); + debug!("Builder cleaned up"); return handle_early_return(update, build.id, build.name, true).await }, }; @@ -323,7 +319,14 @@ impl Resolve for RunBuild { }) => res.context("Failed at call to Periphery to build"), _ = cancel.cancelled() => { info!("Build cancelled during build, cleaning up builder"); - update.push_error_log("Build cancelled", String::from("User cancelled build during docker build")); + if let Err(e) = periphery.request(api::build::CancelBuild { + id: build.id.clone() + }) + .await + .context("Failed to cancel build execution on Server") { + update.push_error_log("Cancel Build", format_serror(&e.into())); + } + update.push_error_log("Build Cancelled", String::from("User cancelled build during image build step")); cleanup_builder_instance(periphery, cleanup_data, &mut update) .await; return handle_early_return(update, build.id, build.name, true).await @@ -373,6 +376,10 @@ impl Resolve for RunBuild { cleanup_builder_instance(periphery, cleanup_data, &mut update) .await; + // Drop action guard before updating + // clients to requery action state + drop(action_guard); + // Need to manually update the update before cache refresh, // and before broadcast with add_update. // The Err case of to_document should be unreachable, @@ -561,8 +568,8 @@ impl Resolve for CancelBuild { let mut update = update.clone(); update.push_simple_log( - "cancel triggered", - "the build cancel has been triggered", + "Cancel Triggered", + "The build cancel has been triggered", ); update_update(update.clone()).await?; diff --git a/bin/core/src/api/execute/deployment.rs b/bin/core/src/api/execute/deployment.rs index e21f0694ce..95e72f6b3b 100644 --- a/bin/core/src/api/execute/deployment.rs +++ b/bin/core/src/api/execute/deployment.rs @@ -58,6 +58,7 @@ impl Resolve for BatchDeploy { task_id = task_id.to_string(), operator = user.id, pattern = self.pattern, + tags = self.tags.join(","), ) )] async fn resolve( @@ -65,8 +66,12 @@ impl Resolve for BatchDeploy { ExecuteArgs { user, task_id, .. }: &ExecuteArgs, ) -> mogh_error::Result { Ok( - super::batch_execute::(&self.pattern, user) - .await?, + super::batch_execute::( + &self.pattern, + self.tags, + user, + ) + .await?, ) } } @@ -110,7 +115,7 @@ impl Resolve for Deploy { // Will check to ensure deployment not already busy before updating, and return Err if so. // The returned guard will set the action state back to default when dropped. - let _action_guard = + let action_guard = action_state.update(|state| state.deploying = true)?; let mut update = update.clone(); @@ -122,26 +127,16 @@ impl Resolve for Deploy { let (version, registry_token) = match &deployment.config.image { DeploymentImage::Build { build_id, version } => { let build = resource::get::(build_id).await?; - let image_names = build.get_image_names(); - let image_name = image_names - .first() - .context("No image name could be created") - .context("Failed to create image name")?; let version = if version.is_none() { build.config.version } else { *version }; - let version_str = version.to_string(); - // Potentially add the build image_tag postfix - let version_str = if build.config.image_tag.is_empty() { - version_str - } else { - format!("{version_str}-{}", build.config.image_tag) - }; + let image_name = build.get_deployment_image_name(); + let image_tag = build.get_deployment_image_tag(version); // replace image with corresponding build image. deployment.config.image = DeploymentImage::Image { - image: format!("{image_name}:{version_str}"), + image: format!("{image_name}:{image_tag}"), }; let first_registry = build .config @@ -211,6 +206,12 @@ impl Resolve for Deploy { update_update(update.clone()).await?; let deployment_id = deployment.id.clone(); + // Track the name the container / service is deployed under, + // so the Deployment stays matched to it even if the + // name configuration changes before the next deploy. + let fresh_name = deployment.custom_name().to_string(); + let prev_deployed_name = deployment.info.deployed_name.clone(); + let mut deployed = false; match swarm_or_server { SwarmOrServer::None => unreachable!(), @@ -227,6 +228,7 @@ impl Resolve for Deploy { { Ok(logs) => { refresh_swarm_cache(&swarm, true).await; + deployed = logs.iter().all(|log| log.success); update.logs.extend(logs) } Err(e) => { @@ -251,6 +253,7 @@ impl Resolve for Deploy { { Ok(log) => { refresh_server_cache(&server, true).await; + deployed = log.success; update.logs.push(log) } Err(e) => { @@ -267,6 +270,11 @@ impl Resolve for Deploy { &deployment_id, &DeploymentInfo { latest_image_digest: Default::default(), + deployed_name: if deployed { + fresh_name + } else { + prev_deployed_name + }, }, ) .await @@ -278,6 +286,11 @@ impl Resolve for Deploy { } update.finalize(); + + // Drop action guard before updating + // clients to requery action state + drop(action_guard); + update_update(update.clone()).await?; Ok(update) @@ -458,7 +471,7 @@ impl Resolve for PullDeployment { // Will check to ensure deployment not already busy before updating, and return Err if so. // The returned guard will set the action state back to default when dropped. - let _action_guard = + let action_guard = action_state.update(|state| state.pulling = true)?; let mut update = update.clone(); @@ -469,6 +482,10 @@ impl Resolve for PullDeployment { update.logs.push(log); update.finalize(); + + // Drop action guard before updating + // clients to requery action state + drop(action_guard); update_update(update.clone()).await?; Ok(update) @@ -516,7 +533,7 @@ impl Resolve for StartDeployment { // Will check to ensure deployment not already busy before updating, and return Err if so. // The returned guard will set the action state back to default when dropped. - let _action_guard = + let action_guard = action_state.update(|state| state.starting = true)?; let mut update = update.clone(); @@ -527,7 +544,7 @@ impl Resolve for StartDeployment { let log = match periphery_client(&server) .await? .request(api::container::StartContainer { - name: deployment.name, + name: deployment.deployed_name().to_string(), }) .await { @@ -541,6 +558,10 @@ impl Resolve for StartDeployment { update.logs.push(log); refresh_server_cache(&server, true).await; update.finalize(); + + // Drop action guard before updating + // clients to requery action state + drop(action_guard); update_update(update.clone()).await?; Ok(update) @@ -588,7 +609,7 @@ impl Resolve for RestartDeployment { // Will check to ensure deployment not already busy before updating, and return Err if so. // The returned guard will set the action state back to default when dropped. - let _action_guard = + let action_guard = action_state.update(|state| state.restarting = true)?; let mut update = update.clone(); @@ -599,7 +620,7 @@ impl Resolve for RestartDeployment { let log = match periphery_client(&server) .await? .request(api::container::RestartContainer { - name: deployment.name, + name: deployment.deployed_name().to_string(), }) .await { @@ -615,6 +636,10 @@ impl Resolve for RestartDeployment { update.logs.push(log); refresh_server_cache(&server, true).await; update.finalize(); + + // Drop action guard before updating + // clients to requery action state + drop(action_guard); update_update(update.clone()).await?; Ok(update) @@ -662,7 +687,7 @@ impl Resolve for PauseDeployment { // Will check to ensure deployment not already busy before updating, and return Err if so. // The returned guard will set the action state back to default when dropped. - let _action_guard = + let action_guard = action_state.update(|state| state.pausing = true)?; let mut update = update.clone(); @@ -673,7 +698,7 @@ impl Resolve for PauseDeployment { let log = match periphery_client(&server) .await? .request(api::container::PauseContainer { - name: deployment.name, + name: deployment.deployed_name().to_string(), }) .await { @@ -687,6 +712,10 @@ impl Resolve for PauseDeployment { update.logs.push(log); refresh_server_cache(&server, true).await; update.finalize(); + + // Drop action guard before updating + // clients to requery action state + drop(action_guard); update_update(update.clone()).await?; Ok(update) @@ -734,7 +763,7 @@ impl Resolve for UnpauseDeployment { // Will check to ensure deployment not already busy before updating, and return Err if so. // The returned guard will set the action state back to default when dropped. - let _action_guard = + let action_guard = action_state.update(|state| state.unpausing = true)?; let mut update = update.clone(); @@ -745,7 +774,7 @@ impl Resolve for UnpauseDeployment { let log = match periphery_client(&server) .await? .request(api::container::UnpauseContainer { - name: deployment.name, + name: deployment.deployed_name().to_string(), }) .await { @@ -761,6 +790,10 @@ impl Resolve for UnpauseDeployment { update.logs.push(log); refresh_server_cache(&server, true).await; update.finalize(); + + // Drop action guard before updating + // clients to requery action state + drop(action_guard); update_update(update.clone()).await?; Ok(update) @@ -810,7 +843,7 @@ impl Resolve for StopDeployment { // Will check to ensure deployment not already busy before updating, and return Err if so. // The returned guard will set the action state back to default when dropped. - let _action_guard = + let action_guard = action_state.update(|state| state.stopping = true)?; let mut update = update.clone(); @@ -821,7 +854,7 @@ impl Resolve for StopDeployment { let log = match periphery_client(&server) .await? .request(api::container::StopContainer { - name: deployment.name, + name: deployment.deployed_name().to_string(), signal: self .signal .unwrap_or(deployment.config.termination_signal) @@ -843,6 +876,10 @@ impl Resolve for StopDeployment { update.logs.push(log); refresh_server_cache(&server, true).await; update.finalize(); + + // Drop action guard before updating + // clients to requery action state + drop(action_guard); update_update(update.clone()).await?; Ok(update) @@ -868,6 +905,7 @@ impl Resolve for BatchDestroyDeployment { task_id = task_id.to_string(), operator = user.id, pattern = self.pattern, + tags = self.tags.join(","), ) )] async fn resolve( @@ -877,6 +915,7 @@ impl Resolve for BatchDestroyDeployment { Ok( super::batch_execute::( &self.pattern, + self.tags, user, ) .await?, @@ -922,7 +961,7 @@ impl Resolve for DestroyDeployment { // Will check to ensure deployment not already busy before updating, and return Err if so. // The returned guard will set the action state back to default when dropped. - let _action_guard = + let action_guard = action_state.update(|state| state.destroying = true)?; let mut update = update.clone(); @@ -936,7 +975,7 @@ impl Resolve for DestroyDeployment { match swarm_request( &swarm.config.server_ids, api::swarm::RemoveSwarmServices { - services: vec![deployment.name], + services: vec![deployment.deployed_name().to_string()], }, ) .await @@ -957,7 +996,7 @@ impl Resolve for DestroyDeployment { match periphery_client(&server) .await? .request(api::container::RemoveContainer { - name: deployment.name, + name: deployment.deployed_name().to_string(), signal: self .signal .unwrap_or(deployment.config.termination_signal) @@ -983,9 +1022,33 @@ impl Resolve for DestroyDeployment { } }; + // Clear the tracked deployed name once the container / service + // is confirmed removed, so the next deploy uses the fresh name. + if log.success + && let Err(e) = resource::update_info::( + &deployment.id, + &DeploymentInfo { + latest_image_digest: deployment + .info + .latest_image_digest + .clone(), + deployed_name: Default::default(), + }, + ) + .await + { + warn!( + "Failed to clear deployment {} ({}) deployed name after destroy | {e:#}", + deployment.name, deployment.id, + ); + } + update.logs.push(log); update.finalize(); + // Drop action guard before updating + // clients to requery action state + drop(action_guard); update_update(update.clone()).await?; Ok(update) diff --git a/bin/core/src/api/execute/maintenance.rs b/bin/core/src/api/execute/maintenance.rs index 8db8ea95db..551219ba1f 100644 --- a/bin/core/src/api/execute/maintenance.rs +++ b/bin/core/src/api/execute/maintenance.rs @@ -1,7 +1,7 @@ use std::{fmt::Write as _, sync::OnceLock}; use anyhow::{Context, anyhow}; -use command::run_komodo_standard_command; +use command::{CommandOptions, run_komodo_standard_command}; use database::{ bson::{Document, doc}, mungos::find::find_collect, @@ -167,8 +167,8 @@ impl Resolve for BackupCoreDatabase { let res = run_komodo_standard_command( "Backup Core Database", - None, "km database backup --yes", + CommandOptions::default(), ) .await; diff --git a/bin/core/src/api/execute/mod.rs b/bin/core/src/api/execute/mod.rs index 502120b092..cf4aa6d991 100644 --- a/bin/core/src/api/execute/mod.rs +++ b/bin/core/src/api/execute/mod.rs @@ -13,6 +13,7 @@ use komodo_client::{ entities::{ Operation, permission::PermissionLevel, + resource::ResourceQuery, update::{Log, Update}, user::User, }, @@ -29,7 +30,10 @@ use uuid::Uuid; use crate::{ auth::KomodoAuthImpl, - helpers::update::{init_execution_update, update_update}, + helpers::{ + query::get_all_tags, + update::{init_execution_update, update_update}, + }, resource::{KomodoResource, list_full_for_user_using_pattern}, state::db_client, }; @@ -111,10 +115,12 @@ pub enum ExecuteRequest { // ==== PROCEDURE ==== RunProcedure(RunProcedure), BatchRunProcedure(BatchRunProcedure), + CancelProcedure(CancelProcedure), // ==== ACTION ==== RunAction(RunAction), BatchRunAction(BatchRunAction), + CancelAction(CancelAction), // ==== SYNC ==== RunSync(RunSync), @@ -356,14 +362,26 @@ trait BatchExecute { #[instrument("BatchExecute", skip(user))] async fn batch_execute( pattern: &str, + tags: Vec, user: &User, ) -> anyhow::Result { + let all_tags = if tags.is_empty() { + vec![] + } else { + get_all_tags(None).await? + }; + let resources = list_full_for_user_using_pattern::( pattern, - Default::default(), + ResourceQuery { + tags, + ..Default::default() + }, + None, + None, user, PermissionLevel::Execute.into(), - &[], + &all_tags, ) .await?; diff --git a/bin/core/src/api/execute/procedure.rs b/bin/core/src/api/execute/procedure.rs index c659eba8ba..b0f22cc86d 100644 --- a/bin/core/src/api/execute/procedure.rs +++ b/bin/core/src/api/execute/procedure.rs @@ -1,12 +1,14 @@ use std::pin::Pin; +use anyhow::Context as _; use database::mungos::{ by_id::update_one_by_id, mongodb::bson::to_document, }; use formatting::{Color, bold, colored, format_serror, muted}; use komodo_client::{ api::execute::{ - BatchExecutionResponse, BatchRunProcedure, RunProcedure, + BatchExecutionResponse, BatchRunProcedure, CancelProcedure, + RunProcedure, }, entities::{ alert::{Alert, AlertData, SeverityLevel}, @@ -19,13 +21,14 @@ use komodo_client::{ }; use mogh_resolver::Resolve; use tokio::sync::Mutex; +use tokio_util::sync::CancellationToken; use crate::{ alert::send_alerts, helpers::{procedure::execute_procedure, update::update_update}, permission::get_check_permissions, resource::refresh_procedure_state_cache, - state::{action_states, db_client}, + state::{action_states, db_client, procedure_cancel_cache}, }; use super::{ExecuteArgs, ExecuteRequest}; @@ -48,8 +51,12 @@ impl Resolve for BatchRunProcedure { ExecuteArgs { user, .. }: &ExecuteArgs, ) -> mogh_error::Result { Ok( - super::batch_execute::(&self.pattern, user) - .await?, + super::batch_execute::( + &self.pattern, + self.tags, + user, + ) + .await?, ) } } @@ -117,14 +124,22 @@ fn resolve_inner( // This will set action state back to default when dropped. // Will also check to ensure procedure not already busy before updating. - let _action_guard = + let action_guard = action_state.update(|state| state.running = true)?; update_update(update.clone()).await?; + let cancel = CancellationToken::new(); + + procedure_cancel_cache() + .insert(procedure.id.clone(), cancel.clone()) + .await; + let update = Mutex::new(update); - let res = execute_procedure(&procedure, &update).await; + let res = execute_procedure(&procedure, &update, cancel).await; + + procedure_cancel_cache().remove(&procedure.id).await; let mut update = update.into_inner(); @@ -140,11 +155,15 @@ fn resolve_inner( ); } Err(e) => update - .push_error_log("execution error", format_serror(&e.into())), + .push_error_log("Execution error", format_serror(&e.into())), } update.finalize(); + // Drop action guard before updating + // clients to requery action state + drop(action_guard); + // Need to manually update the update before cache refresh, // and before broadcast with add_update. // The Err case of to_document should be unreachable, @@ -184,3 +203,47 @@ fn resolve_inner( Ok(update) }) } + +impl Resolve for CancelProcedure { + #[instrument( + "CancelProcedure", + skip_all, + fields( + task_id = task_id.to_string(), + operator = user.id, + update_id = update.id, + procedure = self.procedure, + ) + )] + async fn resolve( + self, + ExecuteArgs { + user, + update, + task_id, + }: &ExecuteArgs, + ) -> Result { + let procedure = get_check_permissions::( + &self.procedure, + user, + PermissionLevel::Execute.into(), + ) + .await?; + + procedure_cancel_cache() + .get(&procedure.id) + .await + .context("Procedure run cancel token not found")? + .cancel(); + + let mut update = update.clone(); + update.push_simple_log( + "Cancel Triggered", + "Procedure cancel has been triggered. The procedure will exit after the currently running stage is complete.", + ); + update.finalize(); + update_update(update.clone()).await?; + + Ok(update) + } +} diff --git a/bin/core/src/api/execute/repo.rs b/bin/core/src/api/execute/repo.rs index 0fcc1badd6..3eeb0be30f 100644 --- a/bin/core/src/api/execute/repo.rs +++ b/bin/core/src/api/execute/repo.rs @@ -58,6 +58,7 @@ impl Resolve for BatchCloneRepo { task_id = task_id.to_string(), operator = user.id, pattern = self.pattern, + tags = self.tags.join(","), ) )] async fn resolve( @@ -65,8 +66,12 @@ impl Resolve for BatchCloneRepo { ExecuteArgs { user, task_id, .. }: &ExecuteArgs, ) -> mogh_error::Result { Ok( - super::batch_execute::(&self.pattern, user) - .await?, + super::batch_execute::( + &self.pattern, + self.tags, + user, + ) + .await?, ) } } @@ -103,7 +108,7 @@ impl Resolve for CloneRepo { // This will set action state back to default when dropped. // Will also check to ensure repo not already busy before updating. - let _action_guard = + let action_guard = action_state.update(|state| state.cloning = true)?; let mut update = update.clone(); @@ -174,6 +179,10 @@ impl Resolve for CloneRepo { ); }; + // Drop action guard before updating + // clients to requery action state + drop(action_guard); + handle_repo_update_return(update).await } } @@ -192,7 +201,8 @@ impl Resolve for BatchPullRepo { fields( task_id = task_id.to_string(), operator = user.id, - pattern = self.pattern + pattern = self.pattern, + tags = self.tags.join(","), ) )] async fn resolve( @@ -200,8 +210,12 @@ impl Resolve for BatchPullRepo { ExecuteArgs { user, task_id, .. }: &ExecuteArgs, ) -> mogh_error::Result { Ok( - super::batch_execute::(&self.pattern, user) - .await?, + super::batch_execute::( + &self.pattern, + self.tags, + user, + ) + .await?, ) } } @@ -238,7 +252,7 @@ impl Resolve for PullRepo { // This will set action state back to default when dropped. // Will also check to ensure repo not already busy before updating. - let _action_guard = + let action_guard = action_state.update(|state| state.pulling = true)?; let mut update = update.clone(); @@ -313,6 +327,10 @@ impl Resolve for PullRepo { ); }; + // Drop action guard before updating + // clients to requery action state + drop(action_guard); + handle_repo_update_return(update).await } } @@ -374,6 +392,7 @@ impl Resolve for BatchBuildRepo { task_id = task_id.to_string(), operator = user.id, pattern = self.pattern, + tags = self.tags.join(","), ) )] async fn resolve( @@ -381,8 +400,12 @@ impl Resolve for BatchBuildRepo { ExecuteArgs { user, task_id, .. }: &ExecuteArgs, ) -> mogh_error::Result { Ok( - super::batch_execute::(&self.pattern, user) - .await?, + super::batch_execute::( + &self.pattern, + self.tags, + user, + ) + .await?, ) } } @@ -423,7 +446,7 @@ impl Resolve for BuildRepo { // This will set action state back to default when dropped. // Will also check to ensure repo not already busy before updating. - let _action_guard = + let action_guard = action_state.update(|state| state.building = true)?; let mut update = update.clone(); @@ -490,7 +513,7 @@ impl Resolve for BuildRepo { repo.name.clone(), None, builder, - &mut update, + Some(&mut update), ) .await { @@ -501,6 +524,9 @@ impl Resolve for BuildRepo { "get builder", format_serror(&e.context("failed to get builder").into()), )); + // Drop action guard before updating + // clients to requery action state + drop(action_guard); return handle_builder_early_return( update, repo.id, repo.name, false, ) @@ -533,6 +559,9 @@ impl Resolve for BuildRepo { cleanup_builder_instance(periphery, cleanup_data, &mut update) .await; info!("builder cleaned up"); + // Drop action guard before updating + // clients to requery action state + drop(action_guard); return handle_builder_early_return(update, repo.id, repo.name, true).await }, }; @@ -580,6 +609,10 @@ impl Resolve for BuildRepo { cleanup_builder_instance(periphery, cleanup_data, &mut update) .await; + // Drop action guard before updating + // clients to requery action state + drop(action_guard); + // Need to manually update the update before cache refresh, // and before broadcast with add_update. // The Err case of to_document should be unreachable, diff --git a/bin/core/src/api/execute/server.rs b/bin/core/src/api/execute/server.rs index 3efdb30561..8557de3a4a 100644 --- a/bin/core/src/api/execute/server.rs +++ b/bin/core/src/api/execute/server.rs @@ -56,8 +56,11 @@ impl Resolve for StartContainer { // Will check to ensure deployment not already busy before updating, and return Err if so. // The returned guard will set the action state back to default when dropped. - let _action_guard = action_state - .update(|state| state.starting_containers = true)?; + let action_guard = action_state.update_custom( + |state| state.starting_containers += 1, + |state| state.starting_containers -= 1, + false, + )?; let mut update = update.clone(); @@ -83,6 +86,10 @@ impl Resolve for StartContainer { refresh_server_cache(&server, true).await; update.finalize(); + + // Drop action guard before updating + // clients to requery action state + drop(action_guard); update_update(update.clone()).await?; Ok(update) @@ -124,8 +131,11 @@ impl Resolve for RestartContainer { // Will check to ensure server not already busy before updating, and return Err if so. // The returned guard will set the action state back to default when dropped. - let _action_guard = action_state - .update(|state| state.restarting_containers = true)?; + let action_guard = action_state.update_custom( + |state| state.restarting_containers += 1, + |state| state.restarting_containers -= 1, + false, + )?; let mut update = update.clone(); @@ -153,6 +163,10 @@ impl Resolve for RestartContainer { refresh_server_cache(&server, true).await; update.finalize(); + + // Drop action guard before updating + // clients to requery action state + drop(action_guard); update_update(update.clone()).await?; Ok(update) @@ -194,8 +208,11 @@ impl Resolve for PauseContainer { // Will check to ensure server not already busy before updating, and return Err if so. // The returned guard will set the action state back to default when dropped. - let _action_guard = - action_state.update(|state| state.pausing_containers = true)?; + let action_guard = action_state.update_custom( + |state| state.pausing_containers += 1, + |state| state.pausing_containers -= 1, + false, + )?; let mut update = update.clone(); @@ -221,6 +238,10 @@ impl Resolve for PauseContainer { refresh_server_cache(&server, true).await; update.finalize(); + + // Drop action guard before updating + // clients to requery action state + drop(action_guard); update_update(update.clone()).await?; Ok(update) @@ -262,8 +283,11 @@ impl Resolve for UnpauseContainer { // Will check to ensure server not already busy before updating, and return Err if so. // The returned guard will set the action state back to default when dropped. - let _action_guard = action_state - .update(|state| state.unpausing_containers = true)?; + let action_guard = action_state.update_custom( + |state| state.unpausing_containers += 1, + |state| state.unpausing_containers -= 1, + false, + )?; let mut update = update.clone(); @@ -291,6 +315,10 @@ impl Resolve for UnpauseContainer { refresh_server_cache(&server, true).await; update.finalize(); + + // Drop action guard before updating + // clients to requery action state + drop(action_guard); update_update(update.clone()).await?; Ok(update) @@ -334,8 +362,11 @@ impl Resolve for StopContainer { // Will check to ensure server not already busy before updating, and return Err if so. // The returned guard will set the action state back to default when dropped. - let _action_guard = action_state - .update(|state| state.stopping_containers = true)?; + let action_guard = action_state.update_custom( + |state| state.stopping_containers += 1, + |state| state.stopping_containers -= 1, + false, + )?; let mut update = update.clone(); @@ -363,6 +394,10 @@ impl Resolve for StopContainer { refresh_server_cache(&server, true).await; update.finalize(); + + // Drop action guard before updating + // clients to requery action state + drop(action_guard); update_update(update.clone()).await?; Ok(update) @@ -412,8 +447,11 @@ impl Resolve for DestroyContainer { // Will check to ensure server not already busy before updating, and return Err if so. // The returned guard will set the action state back to default when dropped. - let _action_guard = - action_state.update(|state| state.pruning_containers = true)?; + let action_guard = action_state.update_custom( + |state| state.destroying_containers += 1, + |state| state.destroying_containers -= 1, + false, + )?; let mut update = update.clone(); @@ -443,6 +481,10 @@ impl Resolve for DestroyContainer { refresh_server_cache(&server, true).await; update.finalize(); + + // Drop action guard before updating + // clients to requery action state + drop(action_guard); update_update(update.clone()).await?; Ok(update) @@ -483,8 +525,11 @@ impl Resolve for StartAllContainers { // Will check to ensure server not already busy before updating, and return Err if so. // The returned guard will set the action state back to default when dropped. - let _action_guard = action_state - .update(|state| state.starting_containers = true)?; + let action_guard = action_state.update_custom( + |state| state.starting_containers += 1, + |state| state.starting_containers -= 1, + false, + )?; let mut update = update.clone(); @@ -507,6 +552,10 @@ impl Resolve for StartAllContainers { refresh_server_cache(&server, true).await; update.finalize(); + + // Drop action guard before updating + // clients to requery action state + drop(action_guard); update_update(update.clone()).await?; Ok(update) @@ -547,8 +596,11 @@ impl Resolve for RestartAllContainers { // Will check to ensure server not already busy before updating, and return Err if so. // The returned guard will set the action state back to default when dropped. - let _action_guard = action_state - .update(|state| state.restarting_containers = true)?; + let action_guard = action_state.update_custom( + |state| state.restarting_containers += 1, + |state| state.restarting_containers -= 1, + false, + )?; let mut update = update.clone(); @@ -573,6 +625,10 @@ impl Resolve for RestartAllContainers { refresh_server_cache(&server, true).await; update.finalize(); + + // Drop action guard before updating + // clients to requery action state + drop(action_guard); update_update(update.clone()).await?; Ok(update) @@ -613,8 +669,11 @@ impl Resolve for PauseAllContainers { // Will check to ensure server not already busy before updating, and return Err if so. // The returned guard will set the action state back to default when dropped. - let _action_guard = - action_state.update(|state| state.pausing_containers = true)?; + let action_guard = action_state.update_custom( + |state| state.pausing_containers += 1, + |state| state.pausing_containers -= 1, + false, + )?; let mut update = update.clone(); @@ -637,6 +696,10 @@ impl Resolve for PauseAllContainers { refresh_server_cache(&server, true).await; update.finalize(); + + // Drop action guard before updating + // clients to requery action state + drop(action_guard); update_update(update.clone()).await?; Ok(update) @@ -677,8 +740,11 @@ impl Resolve for UnpauseAllContainers { // Will check to ensure server not already busy before updating, and return Err if so. // The returned guard will set the action state back to default when dropped. - let _action_guard = action_state - .update(|state| state.unpausing_containers = true)?; + let action_guard = action_state.update_custom( + |state| state.unpausing_containers += 1, + |state| state.unpausing_containers -= 1, + false, + )?; let mut update = update.clone(); @@ -703,6 +769,10 @@ impl Resolve for UnpauseAllContainers { refresh_server_cache(&server, true).await; update.finalize(); + + // Drop action guard before updating + // clients to requery action state + drop(action_guard); update_update(update.clone()).await?; Ok(update) @@ -743,8 +813,11 @@ impl Resolve for StopAllContainers { // Will check to ensure server not already busy before updating, and return Err if so. // The returned guard will set the action state back to default when dropped. - let _action_guard = action_state - .update(|state| state.stopping_containers = true)?; + let action_guard = action_state.update_custom( + |state| state.stopping_containers += 1, + |state| state.stopping_containers -= 1, + false, + )?; let mut update = update.clone(); @@ -767,6 +840,10 @@ impl Resolve for StopAllContainers { refresh_server_cache(&server, true).await; update.finalize(); + + // Drop action guard before updating + // clients to requery action state + drop(action_guard); update_update(update.clone()).await?; Ok(update) @@ -807,7 +884,7 @@ impl Resolve for PruneContainers { // Will check to ensure server not already busy before updating, and return Err if so. // The returned guard will set the action state back to default when dropped. - let _action_guard = + let action_guard = action_state.update(|state| state.pruning_containers = true)?; let mut update = update.clone(); @@ -836,6 +913,10 @@ impl Resolve for PruneContainers { refresh_server_cache(&server, true).await; update.finalize(); + + // Drop action guard before updating + // clients to requery action state + drop(action_guard); update_update(update.clone()).await?; Ok(update) @@ -941,7 +1022,7 @@ impl Resolve for PruneNetworks { // Will check to ensure server not already busy before updating, and return Err if so. // The returned guard will set the action state back to default when dropped. - let _action_guard = + let action_guard = action_state.update(|state| state.pruning_networks = true)?; let mut update = update.clone(); @@ -968,6 +1049,10 @@ impl Resolve for PruneNetworks { refresh_server_cache(&server, true).await; update.finalize(); + + // Drop action guard before updating + // clients to requery action state + drop(action_guard); update_update(update.clone()).await?; Ok(update) @@ -1070,7 +1155,7 @@ impl Resolve for PruneImages { // Will check to ensure server not already busy before updating, and return Err if so. // The returned guard will set the action state back to default when dropped. - let _action_guard = + let action_guard = action_state.update(|state| state.pruning_images = true)?; let mut update = update.clone(); @@ -1095,6 +1180,10 @@ impl Resolve for PruneImages { refresh_server_cache(&server, true).await; update.finalize(); + + // Drop action guard before updating + // clients to requery action state + drop(action_guard); update_update(update.clone()).await?; Ok(update) @@ -1200,7 +1289,7 @@ impl Resolve for PruneVolumes { // Will check to ensure server not already busy before updating, and return Err if so. // The returned guard will set the action state back to default when dropped. - let _action_guard = + let action_guard = action_state.update(|state| state.pruning_volumes = true)?; let mut update = update.clone(); @@ -1225,6 +1314,10 @@ impl Resolve for PruneVolumes { refresh_server_cache(&server, true).await; update.finalize(); + + // Drop action guard before updating + // clients to requery action state + drop(action_guard); update_update(update.clone()).await?; Ok(update) @@ -1265,7 +1358,7 @@ impl Resolve for PruneDockerBuilders { // Will check to ensure server not already busy before updating, and return Err if so. // The returned guard will set the action state back to default when dropped. - let _action_guard = + let action_guard = action_state.update(|state| state.pruning_builders = true)?; let mut update = update.clone(); @@ -1290,6 +1383,10 @@ impl Resolve for PruneDockerBuilders { refresh_server_cache(&server, true).await; update.finalize(); + + // Drop action guard before updating + // clients to requery action state + drop(action_guard); update_update(update.clone()).await?; Ok(update) @@ -1330,7 +1427,7 @@ impl Resolve for PruneBuildx { // Will check to ensure server not already busy before updating, and return Err if so. // The returned guard will set the action state back to default when dropped. - let _action_guard = + let action_guard = action_state.update(|state| state.pruning_buildx = true)?; let mut update = update.clone(); @@ -1355,6 +1452,10 @@ impl Resolve for PruneBuildx { refresh_server_cache(&server, true).await; update.finalize(); + + // Drop action guard before updating + // clients to requery action state + drop(action_guard); update_update(update.clone()).await?; Ok(update) @@ -1395,7 +1496,7 @@ impl Resolve for PruneSystem { // Will check to ensure server not already busy before updating, and return Err if so. // The returned guard will set the action state back to default when dropped. - let _action_guard = + let action_guard = action_state.update(|state| state.pruning_system = true)?; let mut update = update.clone(); @@ -1419,6 +1520,10 @@ impl Resolve for PruneSystem { refresh_server_cache(&server, true).await; update.finalize(); + + // Drop action guard before updating + // clients to requery action state + drop(action_guard); update_update(update.clone()).await?; Ok(update) diff --git a/bin/core/src/api/execute/stack.rs b/bin/core/src/api/execute/stack.rs index 50f20b1d7f..ec710f6a0d 100644 --- a/bin/core/src/api/execute/stack.rs +++ b/bin/core/src/api/execute/stack.rs @@ -72,6 +72,7 @@ impl Resolve for BatchDeployStack { task_id = task_id.to_string(), operator = user.id, pattern = self.pattern, + tags = self.tags.join(","), ) )] async fn resolve( @@ -79,8 +80,12 @@ impl Resolve for BatchDeployStack { ExecuteArgs { user, task_id, .. }: &ExecuteArgs, ) -> mogh_error::Result { Ok( - super::batch_execute::(&self.pattern, user) - .await?, + super::batch_execute::( + &self.pattern, + self.tags, + user, + ) + .await?, ) } } @@ -131,7 +136,7 @@ impl Resolve for DeployStack { // Will check to ensure stack not already busy before updating, and return Err if so. // The returned guard will set the action state back to default when dropped. - let _action_guard = + let action_guard = action_state.update(|state| state.deploying = true)?; let mut update = update.clone(); @@ -325,6 +330,10 @@ impl Resolve for DeployStack { } update.finalize(); + + // Drop action guard before updating + // clients to requery action state + drop(action_guard); update_update(update.clone()).await?; Ok(update) @@ -349,6 +358,7 @@ impl Resolve for BatchDeployStackIfChanged { task_id = task_id.to_string(), operator = user.id, pattern = self.pattern, + tags = self.tags.join(","), ) )] async fn resolve( @@ -358,6 +368,7 @@ impl Resolve for BatchDeployStackIfChanged { Ok( super::batch_execute::( &self.pattern, + self.tags, user, ) .await?, @@ -777,6 +788,7 @@ impl Resolve for BatchPullStack { task_id = task_id.to_string(), operator = user.id, pattern = self.pattern, + tags = self.tags.join(","), ) )] async fn resolve( @@ -784,8 +796,12 @@ impl Resolve for BatchPullStack { ExecuteArgs { user, task_id, .. }: &ExecuteArgs, ) -> mogh_error::Result { Ok( - super::batch_execute::(&self.pattern, user) - .await?, + super::batch_execute::( + &self.pattern, + self.tags, + user, + ) + .await?, ) } } @@ -943,7 +959,7 @@ impl Resolve for PullStack { // Will check to ensure stack not already busy before updating, and return Err if so. // The returned guard will set the action state back to default when dropped. - let _action_guard = + let action_guard = action_state.update(|state| state.pulling = true)?; let mut update = update.clone(); @@ -960,6 +976,10 @@ impl Resolve for PullStack { update.logs.extend(res.logs); update.finalize(); + + // Drop action guard before updating + // clients to requery action state + drop(action_guard); update_update(update.clone()).await?; Ok(update) @@ -1153,15 +1173,20 @@ impl Resolve for BatchDestroyStack { task_id = task_id.to_string(), operator = user.id, pattern = self.pattern, + tags = self.tags.join(","), ) )] async fn resolve( self, ExecuteArgs { user, task_id, .. }: &ExecuteArgs, ) -> mogh_error::Result { - super::batch_execute::(&self.pattern, user) - .await - .map_err(Into::into) + super::batch_execute::( + &self.pattern, + self.tags, + user, + ) + .await + .map_err(Into::into) } } @@ -1214,7 +1239,7 @@ impl Resolve for DestroyStack { // Will check to ensure stack not already busy before updating, and return Err if so. // The returned guard will set the action state back to default when dropped. - let _action_guard = + let action_guard = action_state.update(|state| state.destroying = true)?; let mut update = update.clone(); @@ -1244,6 +1269,10 @@ impl Resolve for DestroyStack { refresh_swarm_cache(&swarm, true).await; update.finalize(); + + // Drop action guard before updating + // clients to requery action state + drop(action_guard); update_update(update.clone()).await?; Ok(update) @@ -1314,7 +1343,7 @@ impl Resolve for RunStackService { let action_state = action_states().stack.get_or_insert_default(&stack.id).await; - let _action_guard = + let action_guard = action_state.update(|state| state.deploying = true)?; let mut update = update.clone(); @@ -1374,6 +1403,10 @@ impl Resolve for RunStackService { update.logs.push(log); update.finalize(); + + // Drop action guard before updating + // clients to requery action state + drop(action_guard); update_update(update.clone()).await?; Ok(update) diff --git a/bin/core/src/api/execute/sync.rs b/bin/core/src/api/execute/sync.rs index 7273b1aeae..5e0c709938 100644 --- a/bin/core/src/api/execute/sync.rs +++ b/bin/core/src/api/execute/sync.rs @@ -98,7 +98,7 @@ impl Resolve for RunSync { // This will set action state back to default when dropped. // Will also check to ensure sync not already busy before updating. - let _action_guard = + let action_guard = action_state.update(|state| state.syncing = true)?; let mut update = update.clone(); @@ -311,6 +311,10 @@ impl Resolve for RunSync { ), ); update.finalize(); + + // Drop action guard before updating + // clients to requery action state + drop(action_guard); update_update(update.clone()).await?; return Ok(update); } @@ -442,6 +446,10 @@ impl Resolve for RunSync { } update.finalize(); + + // Drop action guard before updating + // clients to requery action state + drop(action_guard); update_update(update.clone()).await?; Ok(update) diff --git a/bin/core/src/api/listener/mod.rs b/bin/core/src/api/listener/mod.rs index e90aeea9f8..d964c15ffa 100644 --- a/bin/core/src/api/listener/mod.rs +++ b/bin/core/src/api/listener/mod.rs @@ -1,6 +1,5 @@ use std::sync::Arc; -use anyhow::anyhow; use axum::{Router, http::HeaderMap}; use komodo_client::entities::resource::Resource; use mogh_cache::CloneCache; @@ -41,12 +40,21 @@ trait VerifySecret { /// Implemented on the integration struct, eg [integrations::github::Github] trait ExtractBranch { fn extract_branch(body: &str) -> anyhow::Result; - fn verify_branch(body: &str, expected: &str) -> anyhow::Result<()> { + /// Whether the webhook body's branch matches `expected`. + /// A mismatch is routine and only logged at debug; errors + /// only when the branch cannot be extracted from the body. + fn branch_matches( + body: &str, + expected: &str, + ) -> anyhow::Result { let branch = Self::extract_branch(body)?; if branch == expected { - Ok(()) + Ok(true) } else { - Err(anyhow!("request branch does not match expected")) + debug!( + "Ignoring webhook | push to branch '{branch}' does not match expected branch '{expected}'" + ); + Ok(false) } } } diff --git a/bin/core/src/api/listener/resources.rs b/bin/core/src/api/listener/resources.rs index 2f1be49641..02fab8abd0 100644 --- a/bin/core/src/api/listener/resources.rs +++ b/bin/core/src/api/listener/resources.rs @@ -1,4 +1,4 @@ -use std::{str::FromStr, sync::OnceLock}; +use std::{str::FromStr, sync::OnceLock, time::Duration}; use anyhow::{Context, anyhow}; use komodo_client::{ @@ -23,6 +23,7 @@ use crate::{ }, helpers::update::init_execution_update, resource, + state::action_states, }; use super::{ANY_BRANCH, ListenerLockCache}; @@ -37,11 +38,6 @@ impl super::CustomSecret for Build { } } -fn build_locks() -> &'static ListenerLockCache { - static BUILD_LOCKS: OnceLock = OnceLock::new(); - BUILD_LOCKS.get_or_init(Default::default) -} - pub async fn handle_build_webhook( build: Build, body: String, @@ -50,12 +46,6 @@ pub async fn handle_build_webhook( return Ok(()); } - // Acquire and hold lock to make a task queue for - // subsequent listener calls on same resource. - // It would fail if we let it go through from action state busy. - let lock = build_locks().get_or_insert_default(&build.id).await; - let _lock = lock.lock().await; - // Use the correct target branch when using linked repo. let branch = if build.config.linked_repo.is_empty() { build.config.branch @@ -67,15 +57,49 @@ pub async fn handle_build_webhook( .branch }; - B::verify_branch(&body, &branch)?; + if !B::branch_matches(&body, &branch)? { + return Ok(()); + } + + // Cancel if currently building + if action_states() + .build + .get(&build.id) + .await + .and_then(|states| { + states.get().ok().map(|states| states.building) + }) + .unwrap_or_default() + { + let user = git_webhook_user().to_owned(); + let cancel = ExecuteRequest::CancelBuild(CancelBuild { + build: build.id.clone(), + }); + let update = init_execution_update(&cancel, &user).await?; + let ExecuteRequest::CancelBuild(cancel) = cancel else { + unreachable!() + }; + + cancel + .resolve(&ExecuteArgs { + user, + update, + task_id: Uuid::new_v4(), + }) + .await + .ok(); + + poll_build_until_cancelled(&build.id).await?; + } let user = git_webhook_user().to_owned(); - let req = ExecuteRequest::RunBuild(RunBuild { build: build.id }); - let update = init_execution_update(&req, &user).await?; - let ExecuteRequest::RunBuild(req) = req else { + let run = ExecuteRequest::RunBuild(RunBuild { build: build.id }); + let update = init_execution_update(&run, &user).await?; + let ExecuteRequest::RunBuild(run) = run else { unreachable!() }; - req + + run .resolve(&ExecuteArgs { user, update, @@ -83,9 +107,32 @@ pub async fn handle_build_webhook( }) .await .map_err(|e| e.error)?; + Ok(()) } +async fn poll_build_until_cancelled( + build_id: &String, +) -> anyhow::Result<()> { + let action_states = action_states(); + // Poll to ensure cancelled + for _ in 0..10 { + if !action_states + .build + .get(build_id) + .await + .and_then(|states| { + states.get().ok().map(|states| states.building) + }) + .unwrap_or_default() + { + return Ok(()); + } + tokio::time::sleep(Duration::from_secs(1)).await; + } + Err(anyhow!("Build still running after cancel")) +} + // ====== // REPO // ====== @@ -220,7 +267,9 @@ async fn handle_repo_webhook_inner< let lock = repo_locks().get_or_insert_default(&repo.id).await; let _lock = lock.lock().await; - B::verify_branch(&body, &repo.config.branch)?; + if !B::branch_matches(&body, &repo.config.branch)? { + return Ok(()); + } E::resolve(repo).await } @@ -351,7 +400,9 @@ pub async fn handle_stack_webhook_inner< .branch }; - B::verify_branch(&body, &branch)?; + if !B::branch_matches(&body, &branch)? { + return Ok(()); + } E::resolve(stack).await.map_err(|e| e.error) } @@ -464,7 +515,9 @@ async fn handle_sync_webhook_inner< .branch }; - B::verify_branch(&body, &branch)?; + if !B::branch_matches(&body, &branch)? { + return Ok(()); + } E::resolve(sync).await } @@ -501,8 +554,10 @@ pub async fn handle_procedure_webhook( procedure_locks().get_or_insert_default(&procedure.id).await; let _lock = lock.lock().await; - if target_branch != ANY_BRANCH { - B::verify_branch(&body, target_branch)?; + if target_branch != ANY_BRANCH + && !B::branch_matches(&body, target_branch)? + { + return Ok(()); } let user = git_webhook_user().to_owned(); @@ -557,7 +612,10 @@ pub async fn handle_action_webhook( let branch = B::extract_branch(&body)?; if target_branch != ANY_BRANCH && branch != target_branch { - return Err(anyhow!("request branch does not match expected")); + debug!( + "Ignoring webhook | push to branch '{branch}' does not match expected branch '{target_branch}'" + ); + return Ok(()); } let user = git_webhook_user().to_owned(); diff --git a/bin/core/src/api/read/action.rs b/bin/core/src/api/read/action.rs index d815d9d43b..7206f3d882 100644 --- a/bin/core/src/api/read/action.rs +++ b/bin/core/src/api/read/action.rs @@ -3,7 +3,8 @@ use komodo_client::{ api::read::*, entities::{ action::{ - Action, ActionActionState, ActionListItem, ActionState, + Action, ActionActionState, ActionListItem, ActionSortBy, + ActionState, }, permission::PermissionLevel, }, @@ -11,13 +12,13 @@ use komodo_client::{ use mogh_resolver::Resolve; use crate::{ - helpers::query::get_all_tags, + helpers::query::{get_action_state, get_all_tags}, permission::get_check_permissions, resource, state::{action_state_cache, action_states}, }; -use super::ReadArgs; +use super::{ReadArgs, list_limit}; impl Resolve for GetAction { async fn resolve( @@ -45,15 +46,45 @@ impl Resolve for ListActions { } else { get_all_tags(None).await? }; - Ok( - resource::list_for_user::( - self.query, - user, - PermissionLevel::Read.into(), - &all_tags, - ) - .await?, + let states = self.query.specific.states.clone(); + let limit = list_limit(self.limit); + let sort_by: resource::ListItemSort = + match self.sort_by { + ActionSortBy::Name => resource::ListItemSort::Name, + ActionSortBy::State => { + resource::ListItemSort::InMemory(Box::new(|a, b| { + a.info + .state + .cmp(&b.info.state) + .then_with(|| a.name.cmp(&b.name)) + })) + } + ActionSortBy::NextRun => { + resource::ListItemSort::InMemory(Box::new(|a, b| { + a.info + .next_scheduled_run + .cmp(&b.info.next_scheduled_run) + .then_with(|| a.name.cmp(&b.name)) + })) + } + }; + let actions = resource::list_items_for_user::( + self.query, + resource::ListItemsQueryOptions { + limit, + page: self.page, + sort_desc: self.sort_desc, + sort_by, + }, + user, + PermissionLevel::Read.into(), + &all_tags, + |action| { + states.is_empty() || states.contains(&action.info.state) + }, ) + .await?; + Ok(actions) } } @@ -67,12 +98,28 @@ impl Resolve for ListFullActions { } else { get_all_tags(None).await? }; + let states = self.query.specific.states.clone(); + let limit = list_limit(self.limit); Ok( - resource::list_full_for_user::( + resource::list_full_for_user_filtered::( self.query, + limit, + self.page, user, PermissionLevel::Read.into(), &all_tags, + |action| { + let states = states.clone(); + async move { + if states.is_empty() + || states.contains(&get_action_state(&action.id).await) + { + Some(action) + } else { + None + } + } + }, ) .await?, ) @@ -107,6 +154,8 @@ impl Resolve for GetActionsSummary { ) -> mogh_error::Result { let actions = resource::list_full_for_user::( Default::default(), + None, + None, user, PermissionLevel::Read.into(), &[], diff --git a/bin/core/src/api/read/alert.rs b/bin/core/src/api/read/alert.rs index a543fadf8f..8f04e78854 100644 --- a/bin/core/src/api/read/alert.rs +++ b/bin/core/src/api/read/alert.rs @@ -40,7 +40,7 @@ impl Resolve for ListAlerts { FindOptions::builder() .sort(doc! { "ts": -1 }) .limit(NUM_ALERTS_PER_PAGE as i64) - .skip(self.page * NUM_ALERTS_PER_PAGE) + .skip(self.page.saturating_mul(NUM_ALERTS_PER_PAGE)) .build(), ) .await diff --git a/bin/core/src/api/read/alerter.rs b/bin/core/src/api/read/alerter.rs index a6c44a2c19..97c08d5190 100644 --- a/bin/core/src/api/read/alerter.rs +++ b/bin/core/src/api/read/alerter.rs @@ -4,7 +4,7 @@ use database::mungos::mongodb::bson::doc; use komodo_client::{ api::read::*, entities::{ - alerter::{Alerter, AlerterListItem}, + alerter::{Alerter, AlerterListItem, AlerterSortBy}, permission::PermissionLevel, }, }; @@ -17,7 +17,7 @@ use crate::{ state::db_client, }; -use super::ReadArgs; +use super::{ReadArgs, list_limit}; impl Resolve for GetAlerter { async fn resolve( @@ -45,12 +45,30 @@ impl Resolve for ListAlerters { } else { get_all_tags(None).await? }; + let limit = list_limit(self.limit); + let sort_by: resource::ListItemSort = + match self.sort_by { + AlerterSortBy::Name => resource::ListItemSort::Name, + AlerterSortBy::Type => { + resource::ListItemSort::DbField("config.endpoint.type") + } + AlerterSortBy::Enabled => { + resource::ListItemSort::DbField("config.enabled") + } + }; Ok( - resource::list_for_user::( + resource::list_items_for_user::( self.query, + resource::ListItemsQueryOptions { + limit, + page: self.page, + sort_desc: self.sort_desc, + sort_by, + }, user, PermissionLevel::Read.into(), &all_tags, + |_| true, ) .await?, ) @@ -67,9 +85,12 @@ impl Resolve for ListFullAlerters { } else { get_all_tags(None).await? }; + let limit = list_limit(self.limit); Ok( resource::list_full_for_user::( self.query, + limit as i64, + self.page.saturating_mul(limit), user, PermissionLevel::Read.into(), &all_tags, @@ -85,6 +106,8 @@ impl Resolve for GetAlertersSummary { ReadArgs { user }: &ReadArgs, ) -> mogh_error::Result { let query = match list_resource_ids_for_user::( + Default::default(), + None, None, user, PermissionLevel::Read.into(), diff --git a/bin/core/src/api/read/build.rs b/bin/core/src/api/read/build.rs index 011b04d34a..dac73142b3 100644 --- a/bin/core/src/api/read/build.rs +++ b/bin/core/src/api/read/build.rs @@ -11,7 +11,9 @@ use komodo_client::{ api::read::*, entities::{ Operation, - build::{Build, BuildActionState, BuildListItem, BuildState}, + build::{ + Build, BuildActionState, BuildListItem, BuildSortBy, BuildState, + }, permission::PermissionLevel, update::UpdateStatus, }, @@ -25,7 +27,7 @@ use crate::{ state::{action_states, build_state_cache, db_client}, }; -use super::ReadArgs; +use super::{ReadArgs, list_limit}; impl Resolve for GetBuild { async fn resolve( @@ -53,15 +55,47 @@ impl Resolve for ListBuilds { } else { get_all_tags(None).await? }; - Ok( - resource::list_for_user::( - self.query, - user, - PermissionLevel::Read.into(), - &all_tags, - ) - .await?, + let states = self.query.specific.states.clone(); + let limit = list_limit(self.limit); + let sort_by: resource::ListItemSort = + match self.sort_by { + BuildSortBy::Name => resource::ListItemSort::Name, + BuildSortBy::Source => { + resource::ListItemSort::InMemory(Box::new(|a, b| { + a.info + .files_on_host + .cmp(&b.info.files_on_host) + .then_with(|| { + a.info.linked_repo_name.cmp(&b.info.linked_repo_name) + }) + .then_with(|| a.info.repo.cmp(&b.info.repo)) + .then_with(|| a.name.cmp(&b.name)) + })) + } + BuildSortBy::State => { + resource::ListItemSort::InMemory(Box::new(|a, b| { + a.info + .state + .cmp(&b.info.state) + .then_with(|| a.name.cmp(&b.name)) + })) + } + }; + let builds = resource::list_items_for_user::( + self.query, + resource::ListItemsQueryOptions { + limit, + page: self.page, + sort_desc: self.sort_desc, + sort_by, + }, + user, + PermissionLevel::Read.into(), + &all_tags, + |build| states.is_empty() || states.contains(&build.info.state), ) + .await?; + Ok(builds) } } @@ -75,12 +109,29 @@ impl Resolve for ListFullBuilds { } else { get_all_tags(None).await? }; + let states = self.query.specific.states.clone(); + let limit = list_limit(self.limit); Ok( - resource::list_full_for_user::( + resource::list_full_for_user_filtered::( self.query, + limit, + self.page, user, PermissionLevel::Read.into(), &all_tags, + |build| { + let states = states.clone(); + async move { + if states.is_empty() + || states + .contains(&resource::get_build_state(&build.id).await) + { + Some(build) + } else { + None + } + } + }, ) .await?, ) @@ -115,6 +166,8 @@ impl Resolve for GetBuildsSummary { ) -> mogh_error::Result { let builds = resource::list_full_for_user::( Default::default(), + None, + None, user, PermissionLevel::Read.into(), &[], @@ -164,7 +217,8 @@ impl Resolve for GetBuildMonthlyStats { let curr_ts = unix_timestamp_ms() as i64; let next_day = curr_ts - curr_ts % ONE_DAY_MS + ONE_DAY_MS; - let close_ts = next_day - self.page as i64 * 30 * ONE_DAY_MS; + let close_ts = + next_day - (self.page as i64).saturating_mul(30 * ONE_DAY_MS); let open_ts = close_ts - 30 * ONE_DAY_MS; let mut build_updates = db_client() @@ -281,6 +335,8 @@ impl Resolve for ListCommonBuildExtraArgs { }; let builds = resource::list_full_for_user::( self.query, + None, + None, user, PermissionLevel::Read.into(), &all_tags, diff --git a/bin/core/src/api/read/builder.rs b/bin/core/src/api/read/builder.rs index 6062182777..b6826c9471 100644 --- a/bin/core/src/api/read/builder.rs +++ b/bin/core/src/api/read/builder.rs @@ -4,7 +4,7 @@ use database::mungos::mongodb::bson::doc; use komodo_client::{ api::read::*, entities::{ - builder::{Builder, BuilderListItem}, + builder::{Builder, BuilderListItem, BuilderSortBy}, permission::PermissionLevel, }, }; @@ -17,7 +17,7 @@ use crate::{ state::db_client, }; -use super::ReadArgs; +use super::{ReadArgs, list_limit}; impl Resolve for GetBuilder { async fn resolve( @@ -45,12 +45,35 @@ impl Resolve for ListBuilders { } else { get_all_tags(None).await? }; + let limit = list_limit(self.limit); + let sort_by: resource::ListItemSort = + match self.sort_by { + BuilderSortBy::Name => resource::ListItemSort::Name, + BuilderSortBy::Provider => { + resource::ListItemSort::DbField("config.type") + } + BuilderSortBy::InstanceType => { + resource::ListItemSort::InMemory(Box::new(|a, b| { + a.info + .instance_type + .cmp(&b.info.instance_type) + .then_with(|| a.name.cmp(&b.name)) + })) + } + }; Ok( - resource::list_for_user::( + resource::list_items_for_user::( self.query, + resource::ListItemsQueryOptions { + limit, + page: self.page, + sort_desc: self.sort_desc, + sort_by, + }, user, PermissionLevel::Read.into(), &all_tags, + |_| true, ) .await?, ) @@ -67,9 +90,12 @@ impl Resolve for ListFullBuilders { } else { get_all_tags(None).await? }; + let limit = list_limit(self.limit); Ok( resource::list_full_for_user::( self.query, + limit as i64, + self.page.saturating_mul(limit), user, PermissionLevel::Read.into(), &all_tags, @@ -85,6 +111,8 @@ impl Resolve for GetBuildersSummary { ReadArgs { user }: &ReadArgs, ) -> mogh_error::Result { let query = match list_resource_ids_for_user::( + None, + None, None, user, PermissionLevel::Read.into(), diff --git a/bin/core/src/api/read/deployment.rs b/bin/core/src/api/read/deployment.rs index adc0614181..208e918cfb 100644 --- a/bin/core/src/api/read/deployment.rs +++ b/bin/core/src/api/read/deployment.rs @@ -6,8 +6,8 @@ use komodo_client::{ entities::{ SwarmOrServer, deployment::{ - Deployment, DeploymentActionState, DeploymentConfig, - DeploymentListItem, DeploymentState, + Deployment, DeploymentActionState, DeploymentListItem, + DeploymentSortBy, DeploymentState, }, docker::{ container::{Container, ContainerStats}, @@ -25,7 +25,9 @@ use reqwest::StatusCode; use crate::{ helpers::{ - periphery_client, query::get_all_tags, swarm::swarm_request, + periphery_client, + query::{get_all_tags, get_deployment_state}, + swarm::swarm_request, }, permission::get_check_permissions, resource::{self, setup_deployment_execution}, @@ -34,7 +36,7 @@ use crate::{ }, }; -use super::ReadArgs; +use super::{ReadArgs, list_limit}; impl Resolve for GetDeployment { async fn resolve( @@ -63,21 +65,66 @@ impl Resolve for ListDeployments { get_all_tags(None).await? }; let only_update_available = self.query.specific.update_available; - let deployments = resource::list_for_user::( + let states = self.query.specific.states.clone(); + let limit = list_limit(self.limit); + let sort_by: resource::ListItemSort = + match self.sort_by { + DeploymentSortBy::Name => resource::ListItemSort::Name, + DeploymentSortBy::Image => { + resource::ListItemSort::InMemory(Box::new(|a, b| { + a.info + .image + .cmp(&b.info.image) + .then_with(|| a.name.cmp(&b.name)) + })) + } + DeploymentSortBy::Host => { + resource::ListItemSort::InMemory(Box::new(|a, b| { + let host_a = if a.info.swarm_id.is_empty() { + &a.info.server_name + } else { + &a.info.swarm_name + }; + let host_b = if b.info.swarm_id.is_empty() { + &b.info.server_name + } else { + &b.info.swarm_name + }; + host_a.cmp(host_b).then_with(|| a.name.cmp(&b.name)) + })) + } + DeploymentSortBy::State => { + resource::ListItemSort::InMemory(Box::new(|a, b| { + a.info + .state + .cmp(&b.info.state) + .then_with(|| { + // Use ! with update available to order 'true' first + (!a.info.update_available) + .cmp(&!b.info.update_available) + }) + .then_with(|| a.name.cmp(&b.name)) + })) + } + }; + let deployments = resource::list_items_for_user::( self.query, + resource::ListItemsQueryOptions { + limit, + page: self.page, + sort_desc: self.sort_desc, + sort_by, + }, user, PermissionLevel::Read.into(), &all_tags, + |deployment| { + (!only_update_available || deployment.info.update_available) + && (states.is_empty() + || states.contains(&deployment.info.state)) + }, ) .await?; - let deployments = if only_update_available { - deployments - .into_iter() - .filter(|deployment| deployment.info.update_available) - .collect() - } else { - deployments - }; Ok(deployments) } } @@ -92,12 +139,32 @@ impl Resolve for ListFullDeployments { } else { get_all_tags(None).await? }; + let states = self.query.specific.states.clone(); + let limit = list_limit(self.limit); Ok( - resource::list_full_for_user::( + resource::list_full_for_user_filtered::( self.query, + limit, + self.page, user, PermissionLevel::Read.into(), &all_tags, + |deployment| { + let states = states.clone(); + async move { + if states.is_empty() + || states.contains( + &get_deployment_state(&deployment.id) + .await + .unwrap_or_default(), + ) + { + Some(deployment) + } else { + None + } + } + }, ) .await?, ) @@ -154,7 +221,7 @@ impl Resolve for GetDeploymentLog { SwarmOrServer::Swarm(swarm) => swarm_request( &swarm.config.server_ids, periphery_client::api::swarm::GetSwarmServiceLog { - service: deployment.name, + service: deployment.deployed_name().to_string(), tail, timestamps, no_task_ids: false, @@ -167,7 +234,7 @@ impl Resolve for GetDeploymentLog { SwarmOrServer::Server(server) => periphery_client(&server) .await? .request(api::container::GetContainerLog { - name: deployment.name, + name: deployment.deployed_name().to_string(), tail: cmp::min(tail, MAX_LOG_LENGTH), timestamps, }) @@ -206,7 +273,7 @@ impl Resolve for SearchDeploymentLog { SwarmOrServer::Swarm(swarm) => swarm_request( &swarm.config.server_ids, periphery_client::api::swarm::GetSwarmServiceLogSearch { - service: deployment.name, + service: deployment.deployed_name().to_string(), terms, combinator, invert, @@ -221,7 +288,7 @@ impl Resolve for SearchDeploymentLog { SwarmOrServer::Server(server) => periphery_client(&server) .await? .request(api::container::GetContainerLogSearch { - name: deployment.name, + name: deployment.deployed_name().to_string(), terms, combinator, invert, @@ -274,7 +341,7 @@ impl Resolve for InspectDeploymentContainer { periphery_client(&server) .await? .request(InspectContainer { - name: deployment.name, + name: deployment.deployed_name().to_string(), }) .await .context("Failed to inspect container on server") @@ -307,7 +374,7 @@ impl Resolve for InspectDeploymentSwarmService { swarm_request( &swarm.config.server_ids, periphery_client::api::swarm::InspectSwarmService { - service: deployment.name, + service: deployment.deployed_name().to_string(), }, ) .await @@ -321,25 +388,24 @@ impl Resolve for GetDeploymentStats { self, ReadArgs { user }: &ReadArgs, ) -> mogh_error::Result { - let Deployment { - name, - config: DeploymentConfig { server_id, .. }, - .. - } = get_check_permissions::( + let deployment = get_check_permissions::( &self.deployment, user, PermissionLevel::Read.into(), ) .await?; - if server_id.is_empty() { + if deployment.config.server_id.is_empty() { return Err( - anyhow!("deployment has no server attached").into(), + anyhow!("Deployment has no Server attached").into(), ); } - let server = resource::get::(&server_id).await?; + let server = + resource::get::(&deployment.config.server_id).await?; let res = periphery_client(&server) .await? - .request(api::container::GetContainerStats { name }) + .request(api::container::GetContainerStats { + name: deployment.deployed_name().to_string(), + }) .await .context("failed to get stats from periphery")?; Ok(res) @@ -374,6 +440,8 @@ impl Resolve for GetDeploymentsSummary { ) -> mogh_error::Result { let deployments = resource::list_full_for_user::( Default::default(), + None, + None, user, PermissionLevel::Read.into(), &[], @@ -422,6 +490,8 @@ impl Resolve for ListCommonDeploymentExtraArgs { }; let deployments = resource::list_full_for_user::( self.query, + None, + None, user, PermissionLevel::Read.into(), &all_tags, diff --git a/bin/core/src/api/read/docker.rs b/bin/core/src/api/read/docker.rs new file mode 100644 index 0000000000..9a50cd5d6e --- /dev/null +++ b/bin/core/src/api/read/docker.rs @@ -0,0 +1,590 @@ +use std::cmp; + +use anyhow::{Context as _, anyhow}; +use database::bson::doc; +use komodo_client::{ + api::read::*, + entities::{ + ResourceTarget, + deployment::Deployment, + docker::{ + container::{ + Container, ContainerListItem, ContainerSortBy, + ContainerStateStatusEnum, + }, + image::{Image, ImageHistoryResponseItem}, + network::Network, + volume::Volume, + }, + permission::PermissionLevel, + server::{Server, ServerQuery, ServerState}, + stack::{Stack, StackServiceNames}, + update::Log, + }, +}; +use mogh_resolver::Resolve; +use periphery_client::api as periphery; + +use crate::{ + api::read::{ReadArgs, list_limit}, + helpers::{periphery_client, query::get_all_tags}, + permission::{get_check_permissions, list_resources_for_user}, + resource, + stack::compose_container_match_regex, + state::{db_client, server_status_cache}, +}; + +impl Resolve for GetContainersSummary { + async fn resolve( + self, + ReadArgs { user }: &ReadArgs, + ) -> mogh_error::Result { + let servers = resource::list_full_for_user::( + Default::default(), + None, + None, + user, + PermissionLevel::Read.into(), + &[], + ) + .await + .context("failed to get servers from db")?; + + let mut res = GetContainersSummaryResponse::default(); + + for server in servers { + let cache = server_status_cache() + .get_or_insert_default(&server.id) + .await; + + if let Some(docker) = &cache.docker { + for container in &docker.containers { + res.total += 1; + match container.state { + ContainerStateStatusEnum::Created + | ContainerStateStatusEnum::Paused + | ContainerStateStatusEnum::Exited => res.stopped += 1, + ContainerStateStatusEnum::Running => res.running += 1, + ContainerStateStatusEnum::Empty => res.unknown += 1, + _ => res.unhealthy += 1, + } + } + } + } + + Ok(res) + } +} + +impl Resolve for ListAllContainers { + async fn resolve( + self, + ReadArgs { user }: &ReadArgs, + ) -> mogh_error::Result { + let all_tags = if self.tags.is_empty() { + vec![] + } else { + get_all_tags(None).await? + }; + let servers = resource::list_for_user::( + ServerQuery::builder() + .names(self.servers.clone()) + .tags(self.tags) + .build(), + None, + None, + user, + PermissionLevel::Read.into(), + &all_tags, + ) + .await?; + + let mut containers = Vec::::new(); + let limit = list_limit(self.limit); + // Match terms case insensitively. + let terms = self + .terms + .iter() + .map(|term| term.to_lowercase()) + .collect::>(); + + for server in servers { + let cache = server_status_cache() + .get_or_insert_default(&server.id) + .await; + let Some(docker) = &cache.docker else { + continue; + }; + containers.extend( + docker + .containers + .iter() + .filter(|container| { + // Apply state filter if defined. + (self.state.is_empty() || self.state.contains(&container.state)) && + // Apply terms filter if defined + (terms.is_empty() + // Match when all terms contained within a name. + || { + let name = container.name.to_lowercase(); + terms.iter().all(|term| name.contains(term)) + }) + }) + .cloned(), + ); + } + + // The containers all come from the in memory status cache, + // so all matching containers are collected and sorted + // before applying pagination. + let compare = |a: &ContainerListItem, b: &ContainerListItem| { + match self.sort_by { + ContainerSortBy::Name => a.name.cmp(&b.name), + ContainerSortBy::Server => a.server_name.cmp(&b.server_name), + ContainerSortBy::State => a.state.cmp(&b.state), + ContainerSortBy::Image => a.image.cmp(&b.image), + ContainerSortBy::Networks => { + a.networks.first().cmp(&b.networks.first()) + } + ContainerSortBy::Ports => a + .ports + .first() + .map(|port| port.private_port) + .cmp(&b.ports.first().map(|port| port.private_port)), + ContainerSortBy::Volumes => { + a.volumes.first().cmp(&b.volumes.first()) + } + } + // Fall back to name based sorting for equal sort keys. + // Inside `compare`, so descending sorts are fully descending, + // matching the List apis. + .then_with(|| a.name.cmp(&b.name)) + }; + if self.sort_desc { + containers.sort_by(|a, b| compare(b, a)); + } else { + containers.sort_by(|a, b| compare(a, b)); + } + + let skip = limit.saturating_mul(self.page) as usize; + let take = if limit == 0 { + usize::MAX + } else { + limit as usize + }; + Ok(containers.into_iter().skip(skip).take(take).collect()) + } +} + +impl Resolve for ListContainers { + async fn resolve( + self, + ReadArgs { user }: &ReadArgs, + ) -> mogh_error::Result { + let server = get_check_permissions::( + &self.server, + user, + PermissionLevel::Read.into(), + ) + .await?; + let cache = server_status_cache() + .get_or_insert_default(&server.id) + .await; + if let Some(docker) = &cache.docker { + Ok(docker.containers.clone()) + } else { + Ok(Vec::new()) + } + } +} + +impl Resolve for InspectContainer { + async fn resolve( + self, + ReadArgs { user }: &ReadArgs, + ) -> mogh_error::Result { + let server = get_check_permissions::( + &self.server, + user, + PermissionLevel::Read.inspect(), + ) + .await?; + let cache = server_status_cache() + .get_or_insert_default(&server.id) + .await; + if cache.state != ServerState::Ok { + return Err( + anyhow!( + "Cannot inspect container: server is {:?}", + cache.state + ) + .into(), + ); + } + let res = periphery_client(&server) + .await? + .request(periphery::container::InspectContainer { + name: self.container, + }) + .await?; + Ok(res) + } +} + +impl Resolve for GetResourceMatchingContainer { + async fn resolve( + self, + ReadArgs { user }: &ReadArgs, + ) -> mogh_error::Result { + let server = get_check_permissions::( + &self.server, + user, + PermissionLevel::Read.into(), + ) + .await?; + + // First check deployments with a matching deployed / custom container name. + // The empty check is required to avoid matching + // deployments with no custom name configured. + if !self.container.is_empty() + && let Ok(Some(deployment)) = db_client() + .deployments + .find_one(doc! { + "$or": [ + { "info.deployed_name": &self.container }, + { "config.custom_name": &self.container }, + ] + }) + .await + { + return Ok(GetResourceMatchingContainerResponse { + resource: ResourceTarget::Deployment(deployment.id).into(), + }); + } + + // Then check deployments matching by name + if let Ok(deployment) = + resource::get::(&self.container).await + && deployment.custom_name() == self.container + { + return Ok(GetResourceMatchingContainerResponse { + resource: ResourceTarget::Deployment(deployment.id).into(), + }); + } + + // then check stacks + let stacks = list_resources_for_user::( + doc! { "config.server_id": &server.id }, + None, + None, + user, + PermissionLevel::Read.into(), + ) + .await?; + + // check matching stack + for stack in stacks { + for StackServiceNames { + service_name, + container_name, + .. + } in stack + .info + .deployed_services + .unwrap_or(stack.info.latest_services) + { + let is_match = match compose_container_match_regex(&container_name) + .with_context(|| format!("failed to construct container name matching regex for service {service_name}")) + { + Ok(regex) => regex, + Err(e) => { + warn!("{e:#}"); + continue; + } + }.is_match(&self.container); + + if is_match { + return Ok(GetResourceMatchingContainerResponse { + resource: ResourceTarget::Stack(stack.id).into(), + }); + } + } + } + + Ok(GetResourceMatchingContainerResponse { resource: None }) + } +} + +const MAX_LOG_LENGTH: u64 = 5000; + +impl Resolve for GetContainerLog { + async fn resolve( + self, + ReadArgs { user }: &ReadArgs, + ) -> mogh_error::Result { + let GetContainerLog { + server, + container, + tail, + timestamps, + } = self; + let server = get_check_permissions::( + &server, + user, + PermissionLevel::Read.logs(), + ) + .await?; + let res = periphery_client(&server) + .await? + .request(periphery::container::GetContainerLog { + name: container, + tail: cmp::min(tail, MAX_LOG_LENGTH), + timestamps, + }) + .await + .context("failed at call to periphery")?; + Ok(res) + } +} + +impl Resolve for SearchContainerLog { + async fn resolve( + self, + ReadArgs { user }: &ReadArgs, + ) -> mogh_error::Result { + let SearchContainerLog { + server, + container, + terms, + combinator, + invert, + timestamps, + } = self; + let server = get_check_permissions::( + &server, + user, + PermissionLevel::Read.logs(), + ) + .await?; + let res = periphery_client(&server) + .await? + .request(periphery::container::GetContainerLogSearch { + name: container, + terms, + combinator, + invert, + timestamps, + }) + .await + .context("failed at call to periphery")?; + Ok(res) + } +} + +impl Resolve for ListComposeProjects { + async fn resolve( + self, + ReadArgs { user }: &ReadArgs, + ) -> mogh_error::Result { + let server = get_check_permissions::( + &self.server, + user, + PermissionLevel::Read.into(), + ) + .await?; + let cache = server_status_cache() + .get_or_insert_default(&server.id) + .await; + if let Some(docker) = &cache.docker { + Ok(docker.projects.clone()) + } else { + Ok(Vec::new()) + } + } +} + +impl Resolve for ListNetworks { + async fn resolve( + self, + ReadArgs { user }: &ReadArgs, + ) -> mogh_error::Result { + let server = get_check_permissions::( + &self.server, + user, + PermissionLevel::Read.into(), + ) + .await?; + let cache = server_status_cache() + .get_or_insert_default(&server.id) + .await; + if let Some(docker) = &cache.docker { + Ok(docker.networks.clone()) + } else { + Ok(Vec::new()) + } + } +} + +impl Resolve for InspectNetwork { + async fn resolve( + self, + ReadArgs { user }: &ReadArgs, + ) -> mogh_error::Result { + let server = get_check_permissions::( + &self.server, + user, + PermissionLevel::Read.into(), + ) + .await?; + let cache = server_status_cache() + .get_or_insert_default(&server.id) + .await; + if cache.state != ServerState::Ok { + return Err( + anyhow!( + "Cannot inspect network: server is {:?}", + cache.state + ) + .into(), + ); + } + let res = periphery_client(&server) + .await? + .request(periphery::docker::InspectNetwork { + name: self.network, + }) + .await?; + Ok(res) + } +} + +impl Resolve for ListImages { + async fn resolve( + self, + ReadArgs { user }: &ReadArgs, + ) -> mogh_error::Result { + let server = get_check_permissions::( + &self.server, + user, + PermissionLevel::Read.into(), + ) + .await?; + let cache = server_status_cache() + .get_or_insert_default(&server.id) + .await; + if let Some(docker) = &cache.docker { + Ok(docker.images.clone()) + } else { + Ok(Vec::new()) + } + } +} + +impl Resolve for InspectImage { + async fn resolve( + self, + ReadArgs { user }: &ReadArgs, + ) -> mogh_error::Result { + let server = get_check_permissions::( + &self.server, + user, + PermissionLevel::Read.into(), + ) + .await?; + let cache = server_status_cache() + .get_or_insert_default(&server.id) + .await; + if cache.state != ServerState::Ok { + return Err( + anyhow!("Cannot inspect image: server is {:?}", cache.state) + .into(), + ); + } + let res = periphery_client(&server) + .await? + .request(periphery::docker::InspectImage { name: self.image }) + .await?; + Ok(res) + } +} + +impl Resolve for ListImageHistory { + async fn resolve( + self, + ReadArgs { user }: &ReadArgs, + ) -> mogh_error::Result> { + let server = get_check_permissions::( + &self.server, + user, + PermissionLevel::Read.into(), + ) + .await?; + let cache = server_status_cache() + .get_or_insert_default(&server.id) + .await; + if cache.state != ServerState::Ok { + return Err( + anyhow!( + "Cannot get image history: server is {:?}", + cache.state + ) + .into(), + ); + } + let res = periphery_client(&server) + .await? + .request(periphery::docker::ImageHistory { name: self.image }) + .await?; + Ok(res) + } +} + +impl Resolve for ListVolumes { + async fn resolve( + self, + ReadArgs { user }: &ReadArgs, + ) -> mogh_error::Result { + let server = get_check_permissions::( + &self.server, + user, + PermissionLevel::Read.into(), + ) + .await?; + let cache = server_status_cache() + .get_or_insert_default(&server.id) + .await; + if let Some(docker) = &cache.docker { + Ok(docker.volumes.clone()) + } else { + Ok(Vec::new()) + } + } +} + +impl Resolve for InspectVolume { + async fn resolve( + self, + ReadArgs { user }: &ReadArgs, + ) -> mogh_error::Result { + let server = get_check_permissions::( + &self.server, + user, + PermissionLevel::Read.into(), + ) + .await?; + let cache = server_status_cache() + .get_or_insert_default(&server.id) + .await; + if cache.state != ServerState::Ok { + return Err( + anyhow!("Cannot inspect volume: server is {:?}", cache.state) + .into(), + ); + } + let res = periphery_client(&server) + .await? + .request(periphery::docker::InspectVolume { name: self.volume }) + .await?; + Ok(res) + } +} diff --git a/bin/core/src/api/read/mod.rs b/bin/core/src/api/read/mod.rs index 9ac98a06ab..1f0e1745ef 100644 --- a/bin/core/src/api/read/mod.rs +++ b/bin/core/src/api/read/mod.rs @@ -10,7 +10,7 @@ use komodo_client::{ ResourceTarget, build::Build, builder::{Builder, BuilderConfig}, - config::{DockerRegistry, GitProvider}, + config::{GitProvider, ImageRegistry}, permission::PermissionLevel, repo::Repo, server::Server, @@ -44,6 +44,7 @@ mod alerter; mod build; mod builder; mod deployment; +mod docker; mod onboarding_key; mod permission; mod procedure; @@ -66,6 +67,12 @@ pub struct ReadArgs { pub user: User, } +/// Resolve the page limit for List apis, falling back to the +/// configured `default_pagination_limit` when not provided. +fn list_limit(limit: Option) -> u64 { + limit.unwrap_or(core_config().default_pagination_limit) +} + #[typeshare] #[derive( Serialize, Deserialize, Debug, Clone, Resolve, EnumDiscriminants, @@ -80,7 +87,8 @@ enum ReadRequest { GetCoreInfo(GetCoreInfo), ListSecrets(ListSecrets), ListGitProvidersFromConfig(ListGitProvidersFromConfig), - ListDockerRegistriesFromConfig(ListDockerRegistriesFromConfig), + #[serde(alias = "ListDockerRegistriesFromConfig")] + ListImageRegistriesFromConfig(ListImageRegistriesFromConfig), // ==== SWARM ==== GetSwarmsSummary(GetSwarmsSummary), @@ -117,22 +125,33 @@ enum ReadRequest { // ==== TERMINAL ==== ListTerminals(ListTerminals), - // ==== DOCKER ==== - GetDockerContainersSummary(GetDockerContainersSummary), - ListAllDockerContainers(ListAllDockerContainers), - ListDockerContainers(ListDockerContainers), - InspectDockerContainer(InspectDockerContainer), + // ==== CONTAINER ==== + #[serde(alias = "GetDockerContainersSummary")] + GetContainersSummary(GetContainersSummary), + #[serde(alias = "ListAllDockerContainers")] + ListAllContainers(ListAllContainers), + #[serde(alias = "ListDockerContainers")] + ListContainers(ListContainers), + #[serde(alias = "InspectDockerContainer")] + InspectContainer(InspectContainer), GetResourceMatchingContainer(GetResourceMatchingContainer), GetContainerLog(GetContainerLog), SearchContainerLog(SearchContainerLog), ListComposeProjects(ListComposeProjects), - ListDockerNetworks(ListDockerNetworks), - InspectDockerNetwork(InspectDockerNetwork), - ListDockerImages(ListDockerImages), - InspectDockerImage(InspectDockerImage), - ListDockerImageHistory(ListDockerImageHistory), - ListDockerVolumes(ListDockerVolumes), - InspectDockerVolume(InspectDockerVolume), + #[serde(alias = "ListDockerNetworks")] + ListNetworks(ListNetworks), + #[serde(alias = "InspectDockerNetwork")] + InspectNetwork(InspectNetwork), + #[serde(alias = "ListDockerImages")] + ListImages(ListImages), + #[serde(alias = "InspectDockerImage")] + InspectImage(InspectImage), + #[serde(alias = "ListDockerImageHistory")] + ListImageHistory(ListImageHistory), + #[serde(alias = "ListDockerVolumes")] + ListVolumes(ListVolumes), + #[serde(alias = "InspectDockerVolume")] + InspectVolume(InspectVolume), // ==== SERVER STATS ==== GetSystemInformation(GetSystemInformation), @@ -151,6 +170,7 @@ enum ReadRequest { ListStacks(ListStacks), ListFullStacks(ListFullStacks), ListStackServices(ListStackServices), + ListAllStackServices(ListAllStackServices), ListCommonStackExtraArgs(ListCommonStackExtraArgs), ListCommonStackBuildExtraArgs(ListCommonStackBuildExtraArgs), @@ -258,8 +278,10 @@ enum ReadRequest { // ==== PROVIDER ==== GetGitProviderAccount(GetGitProviderAccount), ListGitProviderAccounts(ListGitProviderAccounts), - GetDockerRegistryAccount(GetDockerRegistryAccount), - ListDockerRegistryAccounts(ListDockerRegistryAccounts), + #[serde(alias = "GetDockerRegistryAccount")] + GetImageRegistryAccount(GetImageRegistryAccount), + #[serde(alias = "ListDockerRegistryAccounts")] + ListImageRegistryAccounts(ListImageRegistryAccounts), // ==== ONBOARDING KEY ==== ListOnboardingKeys(ListOnboardingKeys), @@ -354,6 +376,7 @@ impl Resolve for GetCoreInfo { disable_websocket_reconnect: config.disable_websocket_reconnect, enable_fancy_toml: config.enable_fancy_toml, timezone: config.timezone.clone(), + default_pagination_limit: config.default_pagination_limit, public_key: core_keys().load().public.to_string(), }; Ok(info) @@ -379,7 +402,9 @@ impl Resolve for ListSecrets { ResourceTarget::Builder(id) => { match resource::get::(&id).await?.config { BuilderConfig::Url(_) => None, - BuilderConfig::Server(config) => Some(config.server_id), + BuilderConfig::Server(config) => { + config.server_ids.first().cloned() + } BuilderConfig::Aws(config) => { secrets.extend(config.secrets); None @@ -434,11 +459,13 @@ impl Resolve for ListGitProvidersFromConfig { match resource::get::(&id).await?.config { BuilderConfig::Url(_) => {} BuilderConfig::Server(config) => { - merge_git_providers_for_server( - &mut providers, - &config.server_id, - ) - .await?; + if let Some(server_id) = config.server_ids.first() { + merge_git_providers_for_server( + &mut providers, + server_id, + ) + .await?; + } } BuilderConfig::Aws(config) => { merge_git_providers( @@ -460,18 +487,24 @@ impl Resolve for ListGitProvidersFromConfig { let (builds, repos, syncs) = tokio::try_join!( resource::list_full_for_user::( Default::default(), + None, + None, user, PermissionLevel::Read.into(), &[] ), resource::list_full_for_user::( Default::default(), + None, + None, user, PermissionLevel::Read.into(), &[] ), resource::list_full_for_user::( Default::default(), + None, + None, user, PermissionLevel::Read.into(), &[] @@ -523,33 +556,35 @@ impl Resolve for ListGitProvidersFromConfig { // -impl Resolve for ListDockerRegistriesFromConfig { +impl Resolve for ListImageRegistriesFromConfig { async fn resolve( self, _: &ReadArgs, - ) -> mogh_error::Result { - let mut registries = core_config().docker_registries.clone(); + ) -> mogh_error::Result { + let mut registries = core_config().image_registries.clone(); if let Some(target) = self.target { match target { ResourceTarget::Server(id) => { - merge_docker_registries_for_server(&mut registries, &id) + merge_image_registries_for_server(&mut registries, &id) .await?; } ResourceTarget::Builder(id) => { match resource::get::(&id).await?.config { BuilderConfig::Url(_) => {} BuilderConfig::Server(config) => { - merge_docker_registries_for_server( - &mut registries, - &config.server_id, - ) - .await?; + if let Some(server_id) = config.server_ids.first() { + merge_image_registries_for_server( + &mut registries, + server_id, + ) + .await?; + } } BuilderConfig::Aws(config) => { - merge_docker_registries( + merge_image_registries( &mut registries, - config.docker_registries, + config.image_registries, ); } } @@ -607,14 +642,14 @@ fn merge_git_providers( } } -async fn merge_docker_registries_for_server( - registries: &mut Vec, +async fn merge_image_registries_for_server( + registries: &mut Vec, server_id: &str, ) -> mogh_error::Result<()> { let server = resource::get::(server_id).await?; let more = periphery_client(&server) .await? - .request(periphery_client::api::ListDockerRegistries {}) + .request(periphery_client::api::ListImageRegistries {}) .await .with_context(|| { format!( @@ -622,13 +657,13 @@ async fn merge_docker_registries_for_server( server.name ) })?; - merge_docker_registries(registries, more); + merge_image_registries(registries, more); Ok(()) } -fn merge_docker_registries( - registries: &mut Vec, - more: Vec, +fn merge_image_registries( + registries: &mut Vec, + more: Vec, ) { for incoming_registry in more { if let Some(registry) = registries diff --git a/bin/core/src/api/read/procedure.rs b/bin/core/src/api/read/procedure.rs index b8eac7eb99..3c72404a79 100644 --- a/bin/core/src/api/read/procedure.rs +++ b/bin/core/src/api/read/procedure.rs @@ -3,19 +3,21 @@ use komodo_client::{ api::read::*, entities::{ permission::PermissionLevel, - procedure::{Procedure, ProcedureState}, + procedure::{ + Procedure, ProcedureListItem, ProcedureSortBy, ProcedureState, + }, }, }; use mogh_resolver::Resolve; use crate::{ - helpers::query::get_all_tags, + helpers::query::{get_all_tags, get_procedure_state}, permission::get_check_permissions, resource, state::{action_states, procedure_state_cache}, }; -use super::ReadArgs; +use super::{ReadArgs, list_limit}; impl Resolve for GetProcedure { async fn resolve( @@ -43,15 +45,45 @@ impl Resolve for ListProcedures { } else { get_all_tags(None).await? }; - Ok( - resource::list_for_user::( - self.query, - user, - PermissionLevel::Read.into(), - &all_tags, - ) - .await?, + let states = self.query.specific.states.clone(); + let limit = list_limit(self.limit); + let sort_by: resource::ListItemSort = + match self.sort_by { + ProcedureSortBy::Name => resource::ListItemSort::Name, + ProcedureSortBy::State => { + resource::ListItemSort::InMemory(Box::new(|a, b| { + a.info + .state + .cmp(&b.info.state) + .then_with(|| a.name.cmp(&b.name)) + })) + } + ProcedureSortBy::NextRun => { + resource::ListItemSort::InMemory(Box::new(|a, b| { + a.info + .next_scheduled_run + .cmp(&b.info.next_scheduled_run) + .then_with(|| a.name.cmp(&b.name)) + })) + } + }; + let procedures = resource::list_items_for_user::( + self.query, + resource::ListItemsQueryOptions { + limit, + page: self.page, + sort_desc: self.sort_desc, + sort_by, + }, + user, + PermissionLevel::Read.into(), + &all_tags, + |procedure| { + states.is_empty() || states.contains(&procedure.info.state) + }, ) + .await?; + Ok(procedures) } } @@ -65,12 +97,29 @@ impl Resolve for ListFullProcedures { } else { get_all_tags(None).await? }; + let states = self.query.specific.states.clone(); + let limit = list_limit(self.limit); Ok( - resource::list_full_for_user::( + resource::list_full_for_user_filtered::( self.query, + limit, + self.page, user, PermissionLevel::Read.into(), &all_tags, + |procedure| { + let states = states.clone(); + async move { + if states.is_empty() + || states + .contains(&get_procedure_state(&procedure.id).await) + { + Some(procedure) + } else { + None + } + } + }, ) .await?, ) @@ -84,6 +133,8 @@ impl Resolve for GetProceduresSummary { ) -> mogh_error::Result { let procedures = resource::list_full_for_user::( Default::default(), + None, + None, user, PermissionLevel::Read.into(), &[], diff --git a/bin/core/src/api/read/provider.rs b/bin/core/src/api/read/provider.rs index b915ab6246..94c44b4466 100644 --- a/bin/core/src/api/read/provider.rs +++ b/bin/core/src/api/read/provider.rs @@ -61,11 +61,11 @@ impl Resolve for ListGitProviderAccounts { } } -impl Resolve for GetDockerRegistryAccount { +impl Resolve for GetImageRegistryAccount { async fn resolve( self, ReadArgs { user }: &ReadArgs, - ) -> mogh_error::Result { + ) -> mogh_error::Result { if !user.admin { return Err( anyhow!("Only admins can read docker registry accounts") @@ -83,11 +83,11 @@ impl Resolve for GetDockerRegistryAccount { } } -impl Resolve for ListDockerRegistryAccounts { +impl Resolve for ListImageRegistryAccounts { async fn resolve( self, ReadArgs { user }: &ReadArgs, - ) -> mogh_error::Result { + ) -> mogh_error::Result { if !user.admin { return Err( anyhow!("Only admins can read docker registry accounts") diff --git a/bin/core/src/api/read/repo.rs b/bin/core/src/api/read/repo.rs index 15b8e093be..a99a47f672 100644 --- a/bin/core/src/api/read/repo.rs +++ b/bin/core/src/api/read/repo.rs @@ -3,7 +3,9 @@ use komodo_client::{ api::read::*, entities::{ permission::PermissionLevel, - repo::{Repo, RepoActionState, RepoListItem, RepoState}, + repo::{ + Repo, RepoActionState, RepoListItem, RepoSortBy, RepoState, + }, }, }; use mogh_resolver::Resolve; @@ -15,7 +17,7 @@ use crate::{ state::{action_states, repo_state_cache}, }; -use super::ReadArgs; +use super::{ReadArgs, list_limit}; impl Resolve for GetRepo { async fn resolve( @@ -43,15 +45,41 @@ impl Resolve for ListRepos { } else { get_all_tags(None).await? }; - Ok( - resource::list_for_user::( - self.query, - user, - PermissionLevel::Read.into(), - &all_tags, - ) - .await?, + let states = self.query.specific.states.clone(); + let limit = list_limit(self.limit); + let sort_by: resource::ListItemSort = + match self.sort_by { + RepoSortBy::Name => resource::ListItemSort::Name, + RepoSortBy::Repo => { + resource::ListItemSort::DbField("config.repo") + } + RepoSortBy::Branch => { + resource::ListItemSort::DbField("config.branch") + } + RepoSortBy::State => { + resource::ListItemSort::InMemory(Box::new(|a, b| { + a.info + .state + .cmp(&b.info.state) + .then_with(|| a.name.cmp(&b.name)) + })) + } + }; + let repos = resource::list_items_for_user::( + self.query, + resource::ListItemsQueryOptions { + limit, + page: self.page, + sort_desc: self.sort_desc, + sort_by, + }, + user, + PermissionLevel::Read.into(), + &all_tags, + |repo| states.is_empty() || states.contains(&repo.info.state), ) + .await?; + Ok(repos) } } @@ -65,12 +93,29 @@ impl Resolve for ListFullRepos { } else { get_all_tags(None).await? }; + let states = self.query.specific.states.clone(); + let limit = list_limit(self.limit); Ok( - resource::list_full_for_user::( + resource::list_full_for_user_filtered::( self.query, + limit, + self.page, user, PermissionLevel::Read.into(), &all_tags, + |repo| { + let states = states.clone(); + async move { + if states.is_empty() + || states + .contains(&resource::get_repo_state(&repo.id).await) + { + Some(repo) + } else { + None + } + } + }, ) .await?, ) @@ -105,6 +150,8 @@ impl Resolve for GetReposSummary { ) -> mogh_error::Result { let repos = resource::list_full_for_user::( Default::default(), + None, + None, user, PermissionLevel::Read.into(), &[], diff --git a/bin/core/src/api/read/schedule.rs b/bin/core/src/api/read/schedule.rs index fe4843a0cc..d4dde51045 100644 --- a/bin/core/src/api/read/schedule.rs +++ b/bin/core/src/api/read/schedule.rs @@ -1,13 +1,15 @@ +use std::cmp::Ordering; + use futures_util::future::join_all; use komodo_client::{ api::read::*, entities::{ ResourceTarget, - action::Action, + action::{Action, ActionQuerySpecifics}, permission::PermissionLevel, - procedure::Procedure, + procedure::{Procedure, ProcedureQuerySpecifics}, resource::{ResourceQuery, TemplatesQueryBehavior}, - schedule::Schedule, + schedule::{Schedule, ScheduleSortBy}, }, }; use mogh_resolver::Resolve; @@ -18,7 +20,7 @@ use crate::{ schedule::get_schedule_item_info, }; -use super::ReadArgs; +use super::{ReadArgs, list_limit}; impl Resolve for ListSchedules { async fn resolve( @@ -29,24 +31,36 @@ impl Resolve for ListSchedules { let (actions, procedures) = tokio::try_join!( list_full_for_user::( ResourceQuery { - names: Default::default(), templates: TemplatesQueryBehavior::Include, tag_behavior: self.tag_behavior, tags: self.tags.clone(), - specific: Default::default(), + terms: self.terms.clone(), + specific: ActionQuerySpecifics { + scheduled: Some(true), + ..Default::default() + }, + ..Default::default() }, + None, + None, &args.user, PermissionLevel::Read.into(), &all_tags, ), list_full_for_user::( ResourceQuery { - names: Default::default(), templates: TemplatesQueryBehavior::Include, tag_behavior: self.tag_behavior, tags: self.tags.clone(), - specific: Default::default(), + terms: self.terms, + specific: ProcedureQuerySpecifics { + scheduled: Some(true), + ..Default::default() + }, + ..Default::default() }, + None, + None, &args.user, PermissionLevel::Read.into(), &all_tags, @@ -96,12 +110,51 @@ impl Resolve for ListSchedules { let (actions, procedures) = tokio::join!(join_all(actions), join_all(procedures)); - Ok( - actions - .into_iter() - .chain(procedures) - .filter(|s| !s.schedule.is_empty()) - .collect(), - ) + // The terms / scheduled filters are already applied + // at the db level by the queries above. + let mut schedules = + actions.into_iter().chain(procedures).collect::>(); + + // The schedules are composed in memory across resource types, + // so all matching schedules are collected and sorted + // before applying pagination. + // All comparators fall back to name based sorting for equal + // sort keys, inside `compare`, so descending sorts are fully + // descending, matching the List apis. + let compare: fn(&Schedule, &Schedule) -> Ordering = + match self.sort_by { + ScheduleSortBy::Name => |a, b| a.name.cmp(&b.name), + ScheduleSortBy::Schedule => |a, b| { + a.schedule + .cmp(&b.schedule) + .then_with(|| a.name.cmp(&b.name)) + }, + // Order unscheduled (None) last, matching the UI. + ScheduleSortBy::NextRun => |a, b| { + (a.next_scheduled_run.is_none(), a.next_scheduled_run) + .cmp(&( + b.next_scheduled_run.is_none(), + b.next_scheduled_run, + )) + .then_with(|| a.name.cmp(&b.name)) + }, + ScheduleSortBy::Enabled => |a, b| { + a.enabled.cmp(&b.enabled).then_with(|| a.name.cmp(&b.name)) + }, + }; + if self.sort_desc { + schedules.sort_by(|a, b| compare(b, a)); + } else { + schedules.sort_by(compare); + } + + let limit = list_limit(self.limit); + let skip = limit.saturating_mul(self.page) as usize; + let take = if limit == 0 { + usize::MAX + } else { + limit as usize + }; + Ok(schedules.into_iter().skip(skip).take(take).collect()) } } diff --git a/bin/core/src/api/read/server.rs b/bin/core/src/api/read/server.rs index 24090d2d9b..3e72a1e31f 100644 --- a/bin/core/src/api/read/server.rs +++ b/bin/core/src/api/read/server.rs @@ -1,5 +1,5 @@ use std::{ - cmp, + cmp::Ordering, collections::HashMap, sync::{Arc, OnceLock}, }; @@ -15,47 +15,31 @@ use database::mungos::{ use komodo_client::{ api::read::*, entities::{ - ResourceTarget, - deployment::Deployment, - docker::{ - container::{ - Container, ContainerListItem, ContainerStateStatusEnum, - }, - image::{Image, ImageHistoryResponseItem}, - network::Network, - volume::Volume, - }, permission::PermissionLevel, server::{ - Server, ServerActionState, ServerListItem, ServerQuery, + Server, ServerActionState, ServerListItem, ServerSortBy, ServerState, }, - stack::{Stack, StackServiceNames}, - stats::{SystemInformation, SystemProcess}, - update::Log, + stats::{MinimalSystemStats, SystemInformation, SystemProcess}, }, }; use mogh_error::AddStatusCode; use mogh_resolver::Resolve; -use periphery_client::api::{ - self as periphery, - container::InspectContainer, - docker::{ - ImageHistory, InspectImage, InspectNetwork, InspectVolume, - }, -}; +use periphery_client::api as periphery; use reqwest::StatusCode; use tokio::sync::Mutex; use crate::{ - helpers::{periphery_client, query::get_all_tags}, - permission::{get_check_permissions, list_resources_for_user}, + helpers::{ + periphery_client, + query::{get_all_tags, get_cached_server_state}, + }, + permission::get_check_permissions, resource, - stack::compose_container_match_regex, state::{action_states, db_client, server_status_cache}, }; -use super::ReadArgs; +use super::{ReadArgs, list_limit}; impl Resolve for GetServersSummary { async fn resolve( @@ -64,6 +48,8 @@ impl Resolve for GetServersSummary { ) -> mogh_error::Result { let servers = resource::list_for_user::( Default::default(), + None, + None, user, PermissionLevel::Read.into(), &[], @@ -125,15 +111,89 @@ impl Resolve for ListServers { } else { get_all_tags(None).await? }; - Ok( - resource::list_for_user::( - self.query, - user, - PermissionLevel::Read.into(), - &all_tags, - ) - .await?, + let states = self.query.specific.states.clone(); + let limit = list_limit(self.limit); + let sort_by: resource::ListItemSort = match self + .sort_by + { + ServerSortBy::Name => resource::ListItemSort::Name, + ServerSortBy::Region => { + resource::ListItemSort::DbField("config.region") + } + ServerSortBy::Version => { + resource::ListItemSort::InMemory(Box::new(|a, b| { + a.info + .version + .cmp(&b.info.version) + .then_with(|| a.name.cmp(&b.name)) + })) + } + ServerSortBy::State => { + resource::ListItemSort::InMemory(Box::new(|a, b| { + a.info + .state + .cmp(&b.info.state) + .then_with(|| a.name.cmp(&b.name)) + })) + } + ServerSortBy::Cpu => stats_sort(|stats| stats.cpu_perc as f64), + ServerSortBy::Memory => stats_sort(|stats| { + usage_percent(stats.mem_used_gb, stats.mem_total_gb) + }), + ServerSortBy::Disk => stats_sort(|stats| { + usage_percent(stats.disk_used_gb, stats.disk_total_gb) + }), + ServerSortBy::LoadAverage => { + stats_sort(|stats| stats.load_average.one) + } + ServerSortBy::Network => stats_sort(|stats| { + stats.network_ingress_bytes + stats.network_egress_bytes + }), + }; + let servers = resource::list_items_for_user::( + self.query, + resource::ListItemsQueryOptions { + limit, + page: self.page, + sort_desc: self.sort_desc, + sort_by, + }, + user, + PermissionLevel::Read.into(), + &all_tags, + |server| { + states.is_empty() || states.contains(&server.info.state) + }, ) + .await?; + Ok(servers) + } +} + +/// Build an in memory sort on the list item stats, +/// matching the stats displayed on the server stats table. +/// Servers without stats (unreachable / disabled) order last. +fn stats_sort( + metric: fn(&MinimalSystemStats) -> f64, +) -> resource::ListItemSort { + resource::ListItemSort::InMemory(Box::new(move |a, b| { + match (a.info.stats.as_ref(), b.info.stats.as_ref()) { + (Some(a_stats), Some(b_stats)) => { + metric(a_stats).total_cmp(&metric(b_stats)) + } + (Some(_), None) => Ordering::Greater, + (None, Some(_)) => Ordering::Less, + (None, None) => Ordering::Equal, + } + .then_with(|| a.name.cmp(&b.name)) + })) +} + +fn usage_percent(used: f64, total: f64) -> f64 { + if total > 0.0 { + 100.0 * used / total + } else { + 0.0 } } @@ -147,12 +207,29 @@ impl Resolve for ListFullServers { } else { get_all_tags(None).await? }; + let states = self.query.specific.states.clone(); + let limit = list_limit(self.limit); Ok( - resource::list_full_for_user::( + resource::list_full_for_user_filtered::( self.query, + limit, + self.page, user, PermissionLevel::Read.into(), &all_tags, + |server| { + let states = states.clone(); + async move { + if states.is_empty() + || states + .contains(&get_cached_server_state(&server.id).await) + { + Some(server) + } else { + None + } + } + }, ) .await?, ) @@ -339,7 +416,7 @@ impl Resolve for GetHistoricalServerStats { let curr_ts = unix_timestamp_ms() as i64; let mut curr_ts = curr_ts - curr_ts % granularity - - granularity * STATS_PER_PAGE * page as i64; + - granularity * (page as i64).saturating_mul(STATS_PER_PAGE); for _ in 0..STATS_PER_PAGE { ts_vec.push(curr_ts); curr_ts -= granularity; @@ -353,7 +430,6 @@ impl Resolve for GetHistoricalServerStats { }, FindOptions::builder() .sort(doc! { "ts": -1 }) - .skip(page as u64 * STATS_PER_PAGE as u64) .limit(STATS_PER_PAGE) .build(), ) @@ -368,512 +444,3 @@ impl Resolve for GetHistoricalServerStats { Ok(res) } } - -impl Resolve for ListDockerContainers { - async fn resolve( - self, - ReadArgs { user }: &ReadArgs, - ) -> mogh_error::Result { - let server = get_check_permissions::( - &self.server, - user, - PermissionLevel::Read.into(), - ) - .await?; - let cache = server_status_cache() - .get_or_insert_default(&server.id) - .await; - if let Some(docker) = &cache.docker { - Ok(docker.containers.clone()) - } else { - Ok(Vec::new()) - } - } -} - -impl Resolve for ListAllDockerContainers { - async fn resolve( - self, - ReadArgs { user }: &ReadArgs, - ) -> mogh_error::Result { - let servers = resource::list_for_user::( - ServerQuery::builder().names(self.servers.clone()).build(), - user, - PermissionLevel::Read.into(), - &[], - ) - .await?; - - let mut containers = Vec::::new(); - - for server in servers { - let cache = server_status_cache() - .get_or_insert_default(&server.id) - .await; - let Some(docker) = &cache.docker else { - continue; - }; - let more = docker - .containers - .iter() - .filter(|container| { - self.containers.is_empty() - || self.containers.contains(&container.name) - }) - .cloned(); - containers.extend(more); - } - - Ok(containers) - } -} - -impl Resolve for GetDockerContainersSummary { - async fn resolve( - self, - ReadArgs { user }: &ReadArgs, - ) -> mogh_error::Result { - let servers = resource::list_full_for_user::( - Default::default(), - user, - PermissionLevel::Read.into(), - &[], - ) - .await - .context("failed to get servers from db")?; - - let mut res = GetDockerContainersSummaryResponse::default(); - - for server in servers { - let cache = server_status_cache() - .get_or_insert_default(&server.id) - .await; - - if let Some(docker) = &cache.docker { - for container in &docker.containers { - res.total += 1; - match container.state { - ContainerStateStatusEnum::Created - | ContainerStateStatusEnum::Paused - | ContainerStateStatusEnum::Exited => res.stopped += 1, - ContainerStateStatusEnum::Running => res.running += 1, - ContainerStateStatusEnum::Empty => res.unknown += 1, - _ => res.unhealthy += 1, - } - } - } - } - - Ok(res) - } -} - -impl Resolve for InspectDockerContainer { - async fn resolve( - self, - ReadArgs { user }: &ReadArgs, - ) -> mogh_error::Result { - let server = get_check_permissions::( - &self.server, - user, - PermissionLevel::Read.inspect(), - ) - .await?; - let cache = server_status_cache() - .get_or_insert_default(&server.id) - .await; - if cache.state != ServerState::Ok { - return Err( - anyhow!( - "Cannot inspect container: server is {:?}", - cache.state - ) - .into(), - ); - } - let res = periphery_client(&server) - .await? - .request(InspectContainer { - name: self.container, - }) - .await?; - Ok(res) - } -} - -const MAX_LOG_LENGTH: u64 = 5000; - -impl Resolve for GetContainerLog { - async fn resolve( - self, - ReadArgs { user }: &ReadArgs, - ) -> mogh_error::Result { - let GetContainerLog { - server, - container, - tail, - timestamps, - } = self; - let server = get_check_permissions::( - &server, - user, - PermissionLevel::Read.logs(), - ) - .await?; - let res = periphery_client(&server) - .await? - .request(periphery::container::GetContainerLog { - name: container, - tail: cmp::min(tail, MAX_LOG_LENGTH), - timestamps, - }) - .await - .context("failed at call to periphery")?; - Ok(res) - } -} - -impl Resolve for SearchContainerLog { - async fn resolve( - self, - ReadArgs { user }: &ReadArgs, - ) -> mogh_error::Result { - let SearchContainerLog { - server, - container, - terms, - combinator, - invert, - timestamps, - } = self; - let server = get_check_permissions::( - &server, - user, - PermissionLevel::Read.logs(), - ) - .await?; - let res = periphery_client(&server) - .await? - .request(periphery::container::GetContainerLogSearch { - name: container, - terms, - combinator, - invert, - timestamps, - }) - .await - .context("failed at call to periphery")?; - Ok(res) - } -} - -impl Resolve for GetResourceMatchingContainer { - async fn resolve( - self, - ReadArgs { user }: &ReadArgs, - ) -> mogh_error::Result { - let server = get_check_permissions::( - &self.server, - user, - PermissionLevel::Read.into(), - ) - .await?; - // first check deployments - if let Ok(deployment) = - resource::get::(&self.container).await - { - return Ok(GetResourceMatchingContainerResponse { - resource: ResourceTarget::Deployment(deployment.id).into(), - }); - } - - // then check stacks - let stacks = list_resources_for_user::( - doc! { "config.server_id": &server.id }, - user, - PermissionLevel::Read.into(), - ) - .await?; - - // check matching stack - for stack in stacks { - for StackServiceNames { - service_name, - container_name, - .. - } in stack - .info - .deployed_services - .unwrap_or(stack.info.latest_services) - { - let is_match = match compose_container_match_regex(&container_name) - .with_context(|| format!("failed to construct container name matching regex for service {service_name}")) - { - Ok(regex) => regex, - Err(e) => { - warn!("{e:#}"); - continue; - } - }.is_match(&self.container); - - if is_match { - return Ok(GetResourceMatchingContainerResponse { - resource: ResourceTarget::Stack(stack.id).into(), - }); - } - } - } - - Ok(GetResourceMatchingContainerResponse { resource: None }) - } -} - -impl Resolve for ListDockerNetworks { - async fn resolve( - self, - ReadArgs { user }: &ReadArgs, - ) -> mogh_error::Result { - let server = get_check_permissions::( - &self.server, - user, - PermissionLevel::Read.into(), - ) - .await?; - let cache = server_status_cache() - .get_or_insert_default(&server.id) - .await; - if let Some(docker) = &cache.docker { - Ok(docker.networks.clone()) - } else { - Ok(Vec::new()) - } - } -} - -impl Resolve for InspectDockerNetwork { - async fn resolve( - self, - ReadArgs { user }: &ReadArgs, - ) -> mogh_error::Result { - let server = get_check_permissions::( - &self.server, - user, - PermissionLevel::Read.into(), - ) - .await?; - let cache = server_status_cache() - .get_or_insert_default(&server.id) - .await; - if cache.state != ServerState::Ok { - return Err( - anyhow!( - "Cannot inspect network: server is {:?}", - cache.state - ) - .into(), - ); - } - let res = periphery_client(&server) - .await? - .request(InspectNetwork { name: self.network }) - .await?; - Ok(res) - } -} - -impl Resolve for ListDockerImages { - async fn resolve( - self, - ReadArgs { user }: &ReadArgs, - ) -> mogh_error::Result { - let server = get_check_permissions::( - &self.server, - user, - PermissionLevel::Read.into(), - ) - .await?; - let cache = server_status_cache() - .get_or_insert_default(&server.id) - .await; - if let Some(docker) = &cache.docker { - Ok(docker.images.clone()) - } else { - Ok(Vec::new()) - } - } -} - -impl Resolve for InspectDockerImage { - async fn resolve( - self, - ReadArgs { user }: &ReadArgs, - ) -> mogh_error::Result { - let server = get_check_permissions::( - &self.server, - user, - PermissionLevel::Read.into(), - ) - .await?; - let cache = server_status_cache() - .get_or_insert_default(&server.id) - .await; - if cache.state != ServerState::Ok { - return Err( - anyhow!("Cannot inspect image: server is {:?}", cache.state) - .into(), - ); - } - let res = periphery_client(&server) - .await? - .request(InspectImage { name: self.image }) - .await?; - Ok(res) - } -} - -impl Resolve for ListDockerImageHistory { - async fn resolve( - self, - ReadArgs { user }: &ReadArgs, - ) -> mogh_error::Result> { - let server = get_check_permissions::( - &self.server, - user, - PermissionLevel::Read.into(), - ) - .await?; - let cache = server_status_cache() - .get_or_insert_default(&server.id) - .await; - if cache.state != ServerState::Ok { - return Err( - anyhow!( - "Cannot get image history: server is {:?}", - cache.state - ) - .into(), - ); - } - let res = periphery_client(&server) - .await? - .request(ImageHistory { name: self.image }) - .await?; - Ok(res) - } -} - -impl Resolve for ListDockerVolumes { - async fn resolve( - self, - ReadArgs { user }: &ReadArgs, - ) -> mogh_error::Result { - let server = get_check_permissions::( - &self.server, - user, - PermissionLevel::Read.into(), - ) - .await?; - let cache = server_status_cache() - .get_or_insert_default(&server.id) - .await; - if let Some(docker) = &cache.docker { - Ok(docker.volumes.clone()) - } else { - Ok(Vec::new()) - } - } -} - -impl Resolve for InspectDockerVolume { - async fn resolve( - self, - ReadArgs { user }: &ReadArgs, - ) -> mogh_error::Result { - let server = get_check_permissions::( - &self.server, - user, - PermissionLevel::Read.into(), - ) - .await?; - let cache = server_status_cache() - .get_or_insert_default(&server.id) - .await; - if cache.state != ServerState::Ok { - return Err( - anyhow!("Cannot inspect volume: server is {:?}", cache.state) - .into(), - ); - } - let res = periphery_client(&server) - .await? - .request(InspectVolume { name: self.volume }) - .await?; - Ok(res) - } -} - -impl Resolve for ListComposeProjects { - async fn resolve( - self, - ReadArgs { user }: &ReadArgs, - ) -> mogh_error::Result { - let server = get_check_permissions::( - &self.server, - user, - PermissionLevel::Read.into(), - ) - .await?; - let cache = server_status_cache() - .get_or_insert_default(&server.id) - .await; - if let Some(docker) = &cache.docker { - Ok(docker.projects.clone()) - } else { - Ok(Vec::new()) - } - } -} - -// impl Resolve for ListAllTerminals { -// async fn resolve( -// self, -// args: &ReadArgs, -// ) -> Result { -// // match self.tar -// let mut terminals = resource::list_full_for_user::( -// self.query, &args.user, &all_tags, -// ) -// .await? -// .into_iter() -// .map(|server| async move { -// ( -// list_terminals_inner(&server, self.fresh).await, -// (server.id, server.name), -// ) -// }) -// .collect::>() -// .collect::>() -// .await -// .into_iter() -// .flat_map(|(terminals, server)| { -// let terminals = terminals.ok()?; -// Some((terminals, server)) -// }) -// .flat_map(|(terminals, (server_id, server_name))| { -// terminals.into_iter().map(move |info| { -// TerminalInfoWithServer::from_terminal_info( -// &server_id, -// &server_name, -// info, -// ) -// }) -// }) -// .collect::>(); - -// terminals.sort_by(|a, b| { -// a.server_name.cmp(&b.server_name).then(a.name.cmp(&b.name)) -// }); - -// Ok(terminals) -// } -// } diff --git a/bin/core/src/api/read/stack.rs b/bin/core/src/api/read/stack.rs index 984cc04678..4d66d04d75 100644 --- a/bin/core/src/api/read/stack.rs +++ b/bin/core/src/api/read/stack.rs @@ -9,7 +9,10 @@ use komodo_client::{ container::Container, service::SwarmService, stack::SwarmStack, }, permission::PermissionLevel, - stack::{Stack, StackActionState, StackListItem, StackState}, + stack::{ + Stack, StackActionState, StackListItem, StackQuery, + StackService, StackSortBy, StackState, + }, }, }; use mogh_error::AddStatusCodeError as _; @@ -22,7 +25,9 @@ use reqwest::StatusCode; use crate::{ helpers::{ - periphery_client, query::get_all_tags, swarm::swarm_request, + periphery_client, + query::{get_all_tags, get_cached_stack_state}, + swarm::swarm_request, }, permission::get_check_permissions, resource, @@ -30,7 +35,7 @@ use crate::{ state::{action_states, stack_status_cache}, }; -use super::ReadArgs; +use super::{ReadArgs, list_limit}; impl Resolve for GetStack { async fn resolve( @@ -48,6 +53,128 @@ impl Resolve for GetStack { } } +impl Resolve for ListStacks { + async fn resolve( + self, + ReadArgs { user }: &ReadArgs, + ) -> mogh_error::Result> { + let all_tags = if self.query.tags.is_empty() { + vec![] + } else { + get_all_tags(None).await? + }; + let only_update_available = self.query.specific.update_available; + let states = self.query.specific.states.clone(); + let limit = list_limit(self.limit); + let sort_by: resource::ListItemSort = + match self.sort_by { + StackSortBy::Name => resource::ListItemSort::Name, + StackSortBy::Source => { + resource::ListItemSort::InMemory(Box::new(|a, b| { + a.info + .files_on_host + .cmp(&b.info.files_on_host) + .then_with(|| { + a.info.linked_repo_name.cmp(&b.info.linked_repo_name) + }) + .then_with(|| a.info.repo.cmp(&b.info.repo)) + .then_with(|| a.name.cmp(&b.name)) + })) + } + StackSortBy::Host => { + resource::ListItemSort::InMemory(Box::new(|a, b| { + let host_a = if a.info.swarm_id.is_empty() { + &a.info.server_name + } else { + &a.info.swarm_name + }; + let host_b = if b.info.swarm_id.is_empty() { + &b.info.server_name + } else { + &b.info.swarm_name + }; + host_a.cmp(host_b).then_with(|| a.name.cmp(&b.name)) + })) + } + StackSortBy::State => { + resource::ListItemSort::InMemory(Box::new(|a, b| { + a.info + .state + .cmp(&b.info.state) + .then_with(|| { + // Use ! with update available to order 'true' first + (!a.info.update_available()) + .cmp(&!b.info.update_available()) + }) + .then_with(|| a.name.cmp(&b.name)) + })) + } + }; + let stacks = resource::list_items_for_user::( + self.query, + resource::ListItemsQueryOptions { + limit, + page: self.page, + sort_desc: self.sort_desc, + sort_by, + }, + user, + PermissionLevel::Read.into(), + &all_tags, + |stack| { + (!only_update_available + || stack + .info + .services + .iter() + .any(|service| service.update_available)) + && (states.is_empty() || states.contains(&stack.info.state)) + }, + ) + .await?; + Ok(stacks) + } +} + +impl Resolve for ListFullStacks { + async fn resolve( + self, + ReadArgs { user }: &ReadArgs, + ) -> mogh_error::Result { + let all_tags = if self.query.tags.is_empty() { + vec![] + } else { + get_all_tags(None).await? + }; + let states = self.query.specific.states.clone(); + let limit = list_limit(self.limit); + Ok( + resource::list_full_for_user_filtered::( + self.query, + limit, + self.page, + user, + PermissionLevel::Read.into(), + &all_tags, + |stack| { + let states = states.clone(); + async move { + if states.is_empty() + || states + .contains(&get_cached_stack_state(&stack.id).await) + { + Some(stack) + } else { + None + } + } + }, + ) + .await?, + ) + } +} + impl Resolve for ListStackServices { async fn resolve( self, @@ -72,6 +199,75 @@ impl Resolve for ListStackServices { } } +impl Resolve for ListAllStackServices { + async fn resolve( + self, + ReadArgs { user }: &ReadArgs, + ) -> mogh_error::Result { + let all_tags = if self.tags.is_empty() { + vec![] + } else { + get_all_tags(None).await? + }; + let stacks = resource::list_for_user::( + StackQuery::builder() + .names(self.stacks.clone()) + .tags(self.tags) + .build(), + None, + None, + user, + PermissionLevel::Read.into(), + &all_tags, + ) + .await?; + + let mut services = Vec::::new(); + let mut skipped = 0; + let limit = list_limit(self.limit); + let limit_usize = limit as usize; + // Eg. page 1 skips until after 100 services, page 2 after 200. + let skip = limit.saturating_mul(self.page); + // Match terms case insensitively. + let terms = self + .terms + .iter() + .map(|term| term.to_lowercase()) + .collect::>(); + + for stack in stacks { + let cache = + stack_status_cache().get_or_insert_default(&stack.id).await; + let more = cache.curr.services + .iter() + .filter(|service| { + // Apply state filter if defined. + (self.state.is_empty() || self.state.contains(&service.state)) && + // Apply terms filter if defined + (terms.is_empty() + // Match when all terms contained within a name. + || { + let name = service.service.to_lowercase(); + terms.iter().all(|term| name.contains(term)) + }) + }); + for service in more { + if skipped < skip { + skipped += 1; + } else { + // push and maybe early return + services.push(service.clone()); + if limit > 0 && services.len() >= limit_usize { + return Ok(services); + } + } + } + } + + Ok(services) + } +} + impl Resolve for GetStackLog { async fn resolve( self, @@ -339,6 +535,8 @@ impl Resolve for ListCommonStackExtraArgs { }; let stacks = resource::list_full_for_user::( self.query, + None, + None, user, PermissionLevel::Read.into(), &all_tags, @@ -373,6 +571,8 @@ impl Resolve for ListCommonStackBuildExtraArgs { }; let stacks = resource::list_full_for_user::( self.query, + None, + None, user, PermissionLevel::Read.into(), &all_tags, @@ -395,64 +595,6 @@ impl Resolve for ListCommonStackBuildExtraArgs { } } -impl Resolve for ListStacks { - async fn resolve( - self, - ReadArgs { user }: &ReadArgs, - ) -> mogh_error::Result> { - let all_tags = if self.query.tags.is_empty() { - vec![] - } else { - get_all_tags(None).await? - }; - let only_update_available = self.query.specific.update_available; - let stacks = resource::list_for_user::( - self.query, - user, - PermissionLevel::Read.into(), - &all_tags, - ) - .await?; - let stacks = if only_update_available { - stacks - .into_iter() - .filter(|stack| { - stack - .info - .services - .iter() - .any(|service| service.update_available) - }) - .collect() - } else { - stacks - }; - Ok(stacks) - } -} - -impl Resolve for ListFullStacks { - async fn resolve( - self, - ReadArgs { user }: &ReadArgs, - ) -> mogh_error::Result { - let all_tags = if self.query.tags.is_empty() { - vec![] - } else { - get_all_tags(None).await? - }; - Ok( - resource::list_full_for_user::( - self.query, - user, - PermissionLevel::Read.into(), - &all_tags, - ) - .await?, - ) - } -} - impl Resolve for GetStackActionState { async fn resolve( self, @@ -481,6 +623,8 @@ impl Resolve for GetStacksSummary { ) -> mogh_error::Result { let stacks = resource::list_full_for_user::( Default::default(), + None, + None, user, PermissionLevel::Read.into(), &[], diff --git a/bin/core/src/api/read/swarm.rs b/bin/core/src/api/read/swarm.rs index a24c0402eb..05a574a5ac 100644 --- a/bin/core/src/api/read/swarm.rs +++ b/bin/core/src/api/read/swarm.rs @@ -3,7 +3,9 @@ use komodo_client::{ api::read::*, entities::{ permission::PermissionLevel, - swarm::{Swarm, SwarmActionState, SwarmListItem, SwarmState}, + swarm::{ + Swarm, SwarmActionState, SwarmListItem, SwarmSortBy, SwarmState, + }, }, }; use mogh_resolver::Resolve; @@ -15,7 +17,7 @@ use crate::{ state::{action_states, server_status_cache, swarm_status_cache}, }; -use super::ReadArgs; +use super::{ReadArgs, list_limit}; impl Resolve for GetSwarm { async fn resolve( @@ -43,12 +45,32 @@ impl Resolve for ListSwarms { } else { get_all_tags(None).await? }; + let limit = list_limit(self.limit); + let sort_by: resource::ListItemSort = + match self.sort_by { + SwarmSortBy::Name => resource::ListItemSort::Name, + SwarmSortBy::State => { + resource::ListItemSort::InMemory(Box::new(|a, b| { + a.info + .state + .cmp(&b.info.state) + .then_with(|| a.name.cmp(&b.name)) + })) + } + }; Ok( - resource::list_for_user::( + resource::list_items_for_user::( self.query, + resource::ListItemsQueryOptions { + limit, + page: self.page, + sort_desc: self.sort_desc, + sort_by, + }, user, PermissionLevel::Read.into(), &all_tags, + |_| true, ) .await?, ) @@ -65,9 +87,12 @@ impl Resolve for ListFullSwarms { } else { get_all_tags(None).await? }; + let limit = list_limit(self.limit); Ok( resource::list_full_for_user::( self.query, + limit as i64, + self.page.saturating_mul(limit), user, PermissionLevel::Read.into(), &all_tags, @@ -105,6 +130,8 @@ impl Resolve for GetSwarmsSummary { ) -> mogh_error::Result { let swarms = resource::list_full_for_user::( Default::default(), + None, + None, user, PermissionLevel::Read.into(), &[], diff --git a/bin/core/src/api/read/sync.rs b/bin/core/src/api/read/sync.rs index da6de125e3..57335d63c0 100644 --- a/bin/core/src/api/read/sync.rs +++ b/bin/core/src/api/read/sync.rs @@ -5,6 +5,7 @@ use komodo_client::{ permission::PermissionLevel, sync::{ ResourceSync, ResourceSyncActionState, ResourceSyncListItem, + ResourceSyncSortBy, }, }, }; @@ -15,7 +16,7 @@ use crate::{ resource, state::action_states, }; -use super::ReadArgs; +use super::{ReadArgs, list_limit}; impl Resolve for GetResourceSync { async fn resolve( @@ -43,12 +44,52 @@ impl Resolve for ListResourceSyncs { } else { get_all_tags(None).await? }; + let limit = list_limit(self.limit); + let sort_by: resource::ListItemSort = + match self.sort_by { + ResourceSyncSortBy::Name => resource::ListItemSort::Name, + ResourceSyncSortBy::Source => { + resource::ListItemSort::InMemory(Box::new(|a, b| { + a.info + .files_on_host + .cmp(&b.info.files_on_host) + .then_with(|| { + a.info.linked_repo_name.cmp(&b.info.linked_repo_name) + }) + .then_with(|| a.info.repo.cmp(&b.info.repo)) + .then_with(|| a.name.cmp(&b.name)) + })) + } + ResourceSyncSortBy::Branch => { + resource::ListItemSort::InMemory(Box::new(|a, b| { + a.info + .branch + .cmp(&b.info.branch) + .then_with(|| a.name.cmp(&b.name)) + })) + } + ResourceSyncSortBy::State => { + resource::ListItemSort::InMemory(Box::new(|a, b| { + a.info + .state + .cmp(&b.info.state) + .then_with(|| a.name.cmp(&b.name)) + })) + } + }; Ok( - resource::list_for_user::( + resource::list_items_for_user::( self.query, + resource::ListItemsQueryOptions { + limit, + page: self.page, + sort_desc: self.sort_desc, + sort_by, + }, user, PermissionLevel::Read.into(), &all_tags, + |_| true, ) .await?, ) @@ -65,9 +106,12 @@ impl Resolve for ListFullResourceSyncs { } else { get_all_tags(None).await? }; + let limit = list_limit(self.limit); Ok( resource::list_full_for_user::( self.query, + limit as i64, + self.page.saturating_mul(limit), user, PermissionLevel::Read.into(), &all_tags, @@ -106,6 +150,8 @@ impl Resolve for GetResourceSyncsSummary { let resource_syncs = resource::list_full_for_user::( Default::default(), + None, + None, user, PermissionLevel::Read.into(), &[], diff --git a/bin/core/src/api/read/terminal.rs b/bin/core/src/api/read/terminal.rs index a7530617d5..f7ebe89983 100644 --- a/bin/core/src/api/read/terminal.rs +++ b/bin/core/src/api/read/terminal.rs @@ -1,3 +1,5 @@ +use std::cmp::Ordering; + use anyhow::Context as _; use futures_util::{ FutureExt, StreamExt as _, stream::FuturesUnordered, @@ -9,7 +11,7 @@ use komodo_client::{ permission::PermissionLevel, server::Server, stack::Stack, - terminal::{Terminal, TerminalTarget}, + terminal::{Terminal, TerminalSortBy, TerminalTarget}, user::User, }, }; @@ -22,7 +24,7 @@ use crate::{ resource, }; -use super::ReadArgs; +use super::{ReadArgs, list_limit}; // @@ -31,57 +33,111 @@ impl Resolve for ListTerminals { self, ReadArgs { user }: &ReadArgs, ) -> mogh_error::Result { - let Some(target) = self.target else { - return list_all_terminals_for_user(user, self.use_names).await; - }; - match &target { - TerminalTarget::Server { server } => { - let server = server - .as_ref() - .context("Must provide 'target.params.server'") - .status_code(StatusCode::BAD_REQUEST)?; - let server = get_check_permissions::( - server, - user, - PermissionLevel::Read.terminal(), - ) - .await?; - list_terminals_on_server(&server, Some(target)).await - } - TerminalTarget::Container { server, .. } => { - let server = get_check_permissions::( - server, - user, - PermissionLevel::Read.terminal(), - ) - .await?; - list_terminals_on_server(&server, Some(target)).await - } - TerminalTarget::Stack { stack, .. } => { - let server = get_check_permissions::( - stack, - user, - PermissionLevel::Read.terminal(), - ) - .await? - .config - .server_id; - let server = resource::get::(&server).await?; - list_terminals_on_server(&server, Some(target)).await - } - TerminalTarget::Deployment { deployment } => { - let server = get_check_permissions::( - deployment, - user, - PermissionLevel::Read.terminal(), - ) - .await? - .config - .server_id; - let server = resource::get::(&server).await?; - list_terminals_on_server(&server, Some(target)).await + let mut terminals = match self.target { + None => { + list_all_terminals_for_user(user, self.use_names).await? } + Some(target) => match &target { + TerminalTarget::Server { server } => { + let server = server + .as_ref() + .context("Must provide 'target.params.server'") + .status_code(StatusCode::BAD_REQUEST)?; + let server = get_check_permissions::( + server, + user, + PermissionLevel::Read.terminal(), + ) + .await?; + list_terminals_on_server(&server, Some(target)).await? + } + TerminalTarget::Container { server, .. } => { + let server = get_check_permissions::( + server, + user, + PermissionLevel::Read.terminal(), + ) + .await?; + list_terminals_on_server(&server, Some(target)).await? + } + TerminalTarget::Stack { stack, .. } => { + let server = get_check_permissions::( + stack, + user, + PermissionLevel::Read.terminal(), + ) + .await? + .config + .server_id; + let server = resource::get::(&server).await?; + list_terminals_on_server(&server, Some(target)).await? + } + TerminalTarget::Deployment { deployment } => { + let server = get_check_permissions::( + deployment, + user, + PermissionLevel::Read.terminal(), + ) + .await? + .config + .server_id; + let server = resource::get::(&server).await?; + list_terminals_on_server(&server, Some(target)).await? + } + }, + }; + + // The terminals come from Periphery agents rather than the db, + // so the terms filter / sort / pagination are applied in memory. + if !self.terms.is_empty() { + let terms = self + .terms + .iter() + .map(|term| term.to_lowercase()) + .collect::>(); + terminals.retain(|terminal| { + let name = terminal.name.to_lowercase(); + terms.iter().all(|term| name.contains(term)) + }); + } + + // All comparators fall back to name based sorting for equal + // sort keys, inside `compare`, so descending sorts are fully + // descending, matching the List apis. + let compare: fn(&Terminal, &Terminal) -> Ordering = + match self.sort_by { + TerminalSortBy::Name => |a, b| a.name.cmp(&b.name), + TerminalSortBy::Target => |a, b| { + a.target.cmp(&b.target).then_with(|| a.name.cmp(&b.name)) + }, + TerminalSortBy::Command => |a, b| { + a.command.cmp(&b.command).then_with(|| a.name.cmp(&b.name)) + }, + TerminalSortBy::Size => |a, b| { + a.stored_size_kb + .total_cmp(&b.stored_size_kb) + .then_with(|| a.name.cmp(&b.name)) + }, + TerminalSortBy::Created => |a, b| { + a.created_at + .cmp(&b.created_at) + .then_with(|| a.name.cmp(&b.name)) + }, + }; + if self.sort_desc { + terminals.sort_by(|a, b| compare(b, a)); + } else { + terminals.sort_by(compare); } + + let limit = list_limit(self.limit); + let skip = limit.saturating_mul(self.page) as usize; + let take = if limit == 0 { + usize::MAX + } else { + limit as usize + }; + Ok(terminals.into_iter().skip(skip).take(take).collect()) } } @@ -92,6 +148,8 @@ async fn list_all_terminals_for_user( let (mut servers, stacks, deployments) = tokio::try_join!( resource::list_full_for_user::( Default::default(), + None, + None, user, PermissionLevel::Read.terminal(), &[] @@ -103,12 +161,16 @@ async fn list_all_terminals_for_user( .collect::>())), resource::list_full_for_user::( Default::default(), + None, + None, user, PermissionLevel::Read.terminal(), &[] ), resource::list_full_for_user::( Default::default(), + None, + None, user, PermissionLevel::Read.terminal(), &[] @@ -139,7 +201,7 @@ async fn list_all_terminals_for_user( } } - let mut terminals = servers + let terminals = servers .into_iter() .map(|(server, server_permission)| async move { ( @@ -168,6 +230,7 @@ async fn list_all_terminals_for_user( server_id.clone() }), }; + terminal.target_name = Some(server_name.clone()); terminal }), TerminalTarget::Container { container, .. } => { @@ -180,6 +243,7 @@ async fn list_all_terminals_for_user( }, container, }; + terminal.target_name = Some(server_name.clone()); terminal }) } @@ -193,6 +257,7 @@ async fn list_all_terminals_for_user( }, service, }; + terminal.target_name = Some(s.name.clone()); terminal }) } @@ -206,6 +271,7 @@ async fn list_all_terminals_for_user( d.id.clone() }, }; + terminal.target_name = Some(d.name.clone()); terminal }, ) @@ -220,10 +286,6 @@ async fn list_all_terminals_for_user( .flatten() .collect::>(); - terminals.sort_by(|a, b| { - a.target.cmp(&b.target).then(a.name.cmp(&b.name)) - }); - Ok(terminals) } diff --git a/bin/core/src/api/read/toml.rs b/bin/core/src/api/read/toml.rs index 66901f21f4..2dc3c27aa3 100644 --- a/bin/core/src/api/read/toml.rs +++ b/bin/core/src/api/read/toml.rs @@ -28,6 +28,7 @@ use crate::{ resource, state::db_client, sync::{ + replace_ids::ReplaceIds, toml::{ToToml, convert_resource}, user_groups::{convert_user_groups, user_group_to_toml}, variables::variable_to_toml, @@ -54,6 +55,8 @@ async fn get_all_targets( targets.extend( resource::list_full_for_user::<$Type>( ResourceQuery::builder().tags(tags).build(), + None, + None, user, PermissionLevel::Read.into(), &all_tags, @@ -142,7 +145,7 @@ impl Resolve for ExportResourcesToToml { PermissionLevel::Read.into(), ) .await?; - $Type::replace_ids(&mut resource); + $Type::replace_ids(&mut resource.config); let (deploy, after) = existing .as_ref() .and_then(|e| { @@ -200,7 +203,7 @@ impl Resolve for ExportResourcesToToml { || !sync.config.repo.is_empty() || !sync.config.linked_repo.is_empty()) { - ResourceSync::replace_ids(&mut sync); + ResourceSync::replace_ids(&mut sync.config); res.resource_syncs.push(convert_resource::( sync, false, diff --git a/bin/core/src/api/read/update.rs b/bin/core/src/api/read/update.rs index a24e642910..52373f677f 100644 --- a/bin/core/src/api/read/update.rs +++ b/bin/core/src/api/read/update.rs @@ -47,7 +47,9 @@ impl Resolve for ListUpdates { query, FindOptions::builder() .sort(doc! { "start_ts": -1 }) - .skip(self.page as u64 * UPDATES_PER_PAGE as u64) + .skip( + (self.page as u64).saturating_mul(UPDATES_PER_PAGE as u64), + ) .limit(UPDATES_PER_PAGE) .build(), ) diff --git a/bin/core/src/api/write/build.rs b/bin/core/src/api/write/build.rs index 87d19fa585..273ce9781a 100644 --- a/bin/core/src/api/write/build.rs +++ b/bin/core/src/api/write/build.rs @@ -1,10 +1,8 @@ -use std::{path::PathBuf, time::Duration}; +use std::path::PathBuf; use anyhow::{Context, anyhow}; +use database::mongo_indexed::doc; use database::mungos::mongodb::bson::to_document; -use database::{ - mongo_indexed::doc, mungos::mongodb::bson::oid::ObjectId, -}; use formatting::format_serror; use komodo_client::{ api::write::*, @@ -15,7 +13,6 @@ use komodo_client::{ builder::{Builder, BuilderConfig}, permission::PermissionLevel, repo::Repo, - server::ServerState, update::Update, }, }; @@ -25,12 +22,11 @@ use periphery_client::api::build::{ }; use tokio::fs; +use crate::helpers::builder::connect_builder_periphery; use crate::{ config::core_config, - connection::PeripheryConnectionArgs, helpers::{ - git_token, periphery_client, - query::get_server_with_state, + git_token, update::{add_update, make_update}, }, periphery::PeripheryClient, @@ -476,48 +472,15 @@ async fn get_on_host_periphery( .await .context("Failed to get builder")?; - match builder.config { - BuilderConfig::Aws(_) => { - Err(anyhow!("Files on host doesn't work with AWS builder")) - } - BuilderConfig::Url(config) => { - // TODO: Ensure connection is actually established. - // Builder id no good because it may be active for multiple connections. - let periphery = PeripheryClient::new( - PeripheryConnectionArgs::from_url_builder( - &ObjectId::new().to_hex(), - &config, - ), - config.insecure_tls, - ) - .await?; - // Poll for connection to be estalished - let mut err = None; - for _ in 0..10 { - tokio::time::sleep(Duration::from_secs(1)).await; - match periphery.health_check().await { - Ok(_) => return Ok(periphery), - Err(e) => err = Some(e), - }; - } - Err(err.context("Missing error")?) - } - BuilderConfig::Server(config) => { - if config.server_id.is_empty() { - return Err(anyhow!( - "Builder is type server, but has no server attached" - )); - } - let (server, state) = - get_server_with_state(&config.server_id).await?; - if state != ServerState::Ok { - return Err(anyhow!( - "Builder server is disabled or not reachable" - )); - }; - periphery_client(&server).await - } + if let BuilderConfig::Aws(_) = builder.config { + return Err(anyhow!( + "Files on host doesn't work with AWS builder" + )); } + + connect_builder_periphery(build.name.clone(), None, builder, None) + .await + .map(|(periphery, _)| periphery) } /// The successful case will be included as Some(remote_contents). diff --git a/bin/core/src/api/write/deployment.rs b/bin/core/src/api/write/deployment.rs index d48bf5357d..438b2f101d 100644 --- a/bin/core/src/api/write/deployment.rs +++ b/bin/core/src/api/write/deployment.rs @@ -15,6 +15,7 @@ use komodo_client::{ docker::container::RestartPolicyNameEnum, komodo_timestamp, optional_string, permission::PermissionLevel, + resource::ResourceQuery, server::{Server, ServerState}, to_container_compatible_name, update::Update, @@ -30,7 +31,9 @@ use crate::{ api::execute::{self, ExecuteRequest, ExecutionResult}, helpers::{ periphery_client, - query::{get_deployment_state, get_swarm_or_server}, + query::{ + get_all_tags, get_deployment_state, get_swarm_or_server, + }, registry_token, update::{add_update, make_update, poll_update_until_complete}, }, @@ -300,10 +303,24 @@ impl Resolve for RenameDeployment { let name = to_container_compatible_name(&self.name); - let container_state = - get_deployment_state(&deployment.id).await?; + let state = get_deployment_state(&deployment.id).await?; + + // When no custom name is configured, the container / service + // follows the Deployment name, and must be kept matched: + // - Server mode: rename the container to the new name. + // - Swarm mode: services cannot be renamed, so the current + // service name is pinned in info.deployed_name, and stays + // in use until the next deploy recreates the service. + // A configured custom name is unaffected by Deployment rename. + let follows_name = + deployment.config.custom_name.trim().is_empty(); + let deployed = state != DeploymentState::NotDeployed; - if container_state == DeploymentState::Unknown { + let rename_container = follows_name + && deployed + && !deployment.config.server_id.is_empty(); + + if rename_container && state == DeploymentState::Unknown { return Err( anyhow!( "Cannot rename Deployment when container status is unknown" @@ -312,27 +329,51 @@ impl Resolve for RenameDeployment { ); } + // Includes the Unknown state, in case a service + // is still running under the current name. + let pin_deployed_name = follows_name + && deployed + && !deployment.config.swarm_id.is_empty() + && deployment.info.deployed_name.is_empty(); + let mut update = make_update(&deployment, Operation::RenameDeployment, user); + let mut set = + doc! { "name": &name, "updated_at": komodo_timestamp() }; + if rename_container { + // Container is renamed to the new name below. + set.insert("info.deployed_name", name.as_str()); + } else if pin_deployed_name { + set.insert("info.deployed_name", deployment.deployed_name()); + } + update_one_by_id( &db_client().deployments, &deployment.id, - database::mungos::update::Update::Set( - doc! { "name": &name, "updated_at": komodo_timestamp() }, - ), + database::mungos::update::Update::Set(set), None, ) .await .context("Failed to update Deployment name on db")?; - if container_state != DeploymentState::NotDeployed { + if pin_deployed_name { + update.push_simple_log( + "Pin Service Name", + format!( + "Swarm services cannot be renamed, so the current service name '{}' stays in use until the next deploy recreates the service under the new name.", + deployment.deployed_name() + ), + ); + } + + if rename_container { let server = resource::get::(&deployment.config.server_id).await?; let log = periphery_client(&server) .await? .request(api::container::RenameContainer { - curr_name: deployment.name.clone(), + curr_name: deployment.deployed_name().to_string(), new_name: name.clone(), }) .await @@ -465,6 +506,7 @@ pub async fn check_deployment_for_update_inner( &deployment.id, &DeploymentInfo { latest_image_digest: latest_digest.clone(), + deployed_name: deployment.info.deployed_name.clone(), }, ) .await?; @@ -621,6 +663,7 @@ impl Resolve for BatchCheckDeploymentForUpdate { fields( operator = user.id, pattern = self.pattern, + tags = self.tags.join(","), skip_auto_update = self.skip_auto_update, wait_for_auto_update = self.wait_for_auto_update, ) @@ -629,12 +672,23 @@ impl Resolve for BatchCheckDeploymentForUpdate { self, WriteArgs { user }: &WriteArgs, ) -> Result { + let all_tags = if self.tags.is_empty() { + vec![] + } else { + get_all_tags(None).await? + }; + let deployments = list_full_for_user_using_pattern::( &self.pattern, - Default::default(), + ResourceQuery { + tags: self.tags, + ..Default::default() + }, + None, + None, user, PermissionLevel::Execute.into(), - &[], + &all_tags, ) .await?; diff --git a/bin/core/src/api/write/mod.rs b/bin/core/src/api/write/mod.rs index 7eb880cdd1..d9533a9089 100644 --- a/bin/core/src/api/write/mod.rs +++ b/bin/core/src/api/write/mod.rs @@ -211,9 +211,12 @@ pub enum WriteRequest { CreateGitProviderAccount(CreateGitProviderAccount), UpdateGitProviderAccount(UpdateGitProviderAccount), DeleteGitProviderAccount(DeleteGitProviderAccount), - CreateDockerRegistryAccount(CreateDockerRegistryAccount), - UpdateDockerRegistryAccount(UpdateDockerRegistryAccount), - DeleteDockerRegistryAccount(DeleteDockerRegistryAccount), + #[serde(alias = "CreateDockerRegistryAccount")] + CreateImageRegistryAccount(CreateImageRegistryAccount), + #[serde(alias = "UpdateDockerRegistryAccount")] + UpdateImageRegistryAccount(UpdateImageRegistryAccount), + #[serde(alias = "DeleteDockerRegistryAccount")] + DeleteImageRegistryAccount(DeleteImageRegistryAccount), // ==== ALERT ==== CloseAlert(CloseAlert), diff --git a/bin/core/src/api/write/permissions.rs b/bin/core/src/api/write/permissions.rs index ad9724f50c..27e4981976 100644 --- a/bin/core/src/api/write/permissions.rs +++ b/bin/core/src/api/write/permissions.rs @@ -347,9 +347,9 @@ async fn extract_resource_target_with_validation( .find_one(filter) .await .context("Failed to query db for swarms")? - .context("No matching server found")? + .context("No matching swarm found")? .id; - Ok((ResourceTargetVariant::Server, id)) + Ok((ResourceTargetVariant::Swarm, id)) } ResourceTarget::Server(ident) => { let filter = match ObjectId::from_str(ident) { diff --git a/bin/core/src/api/write/provider.rs b/bin/core/src/api/write/provider.rs index 0548216b06..398b38c2d4 100644 --- a/bin/core/src/api/write/provider.rs +++ b/bin/core/src/api/write/provider.rs @@ -7,7 +7,7 @@ use komodo_client::{ api::write::*, entities::{ Operation, ResourceTarget, - provider::{DockerRegistryAccount, GitProviderAccount}, + provider::{GitProviderAccount, ImageRegistryAccount}, }, }; use mogh_error::AddStatusCodeError; @@ -248,7 +248,7 @@ impl Resolve for DeleteGitProviderAccount { } } -impl Resolve for CreateDockerRegistryAccount { +impl Resolve for CreateImageRegistryAccount { #[instrument( "CreateDockerRegistryAccount", skip_all, @@ -261,7 +261,7 @@ impl Resolve for CreateDockerRegistryAccount { async fn resolve( self, WriteArgs { user }: &WriteArgs, - ) -> mogh_error::Result { + ) -> mogh_error::Result { if !user.admin { return Err( anyhow!( @@ -271,7 +271,7 @@ impl Resolve for CreateDockerRegistryAccount { ); } - let mut account: DockerRegistryAccount = self.account.into(); + let mut account: ImageRegistryAccount = self.account.into(); if account.domain.is_empty() { return Err( @@ -326,7 +326,7 @@ impl Resolve for CreateDockerRegistryAccount { } } -impl Resolve for UpdateDockerRegistryAccount { +impl Resolve for UpdateImageRegistryAccount { #[instrument( "UpdateDockerRegistryAccount", skip_all, @@ -340,7 +340,7 @@ impl Resolve for UpdateDockerRegistryAccount { async fn resolve( mut self, WriteArgs { user }: &WriteArgs, - ) -> mogh_error::Result { + ) -> mogh_error::Result { if !user.admin { return Err( anyhow!("Only admins can update docker registry accounts") @@ -423,7 +423,7 @@ impl Resolve for UpdateDockerRegistryAccount { } } -impl Resolve for DeleteDockerRegistryAccount { +impl Resolve for DeleteImageRegistryAccount { #[instrument( "DeleteDockerRegistryAccount", skip_all, @@ -435,7 +435,7 @@ impl Resolve for DeleteDockerRegistryAccount { async fn resolve( self, WriteArgs { user }: &WriteArgs, - ) -> mogh_error::Result { + ) -> mogh_error::Result { if !user.admin { return Err( anyhow!("Only admins can delete docker registry accounts") diff --git a/bin/core/src/api/write/stack.rs b/bin/core/src/api/write/stack.rs index ebfa1e6280..78e0b3cfe6 100644 --- a/bin/core/src/api/write/stack.rs +++ b/bin/core/src/api/write/stack.rs @@ -19,6 +19,7 @@ use komodo_client::{ all_logs_success, komodo_timestamp, permission::PermissionLevel, repo::Repo, + resource::ResourceQuery, stack::{Stack, StackInfo, StackServiceWithUpdate, StackState}, update::Update, user::{auto_redeploy_user, stack_user, system_user}, @@ -36,7 +37,7 @@ use crate::{ api::execute::{self, ExecuteRequest, ExecutionResult}, config::core_config, helpers::{ - query::get_swarm_or_server, + query::{get_all_tags, get_swarm_or_server}, stack_git_token, swarm_or_server_request, update::{add_update, make_update, poll_update_until_complete}, }, @@ -1040,6 +1041,7 @@ impl Resolve for BatchCheckStackForUpdate { fields( operator = user.id, pattern = self.pattern, + tags = self.tags.join(","), skip_auto_update = self.skip_auto_update, wait_for_auto_update = self.wait_for_auto_update, ) @@ -1048,12 +1050,23 @@ impl Resolve for BatchCheckStackForUpdate { self, WriteArgs { user }: &WriteArgs, ) -> Result { + let all_tags = if self.tags.is_empty() { + vec![] + } else { + get_all_tags(None).await? + }; + let stacks = list_full_for_user_using_pattern::( &self.pattern, - Default::default(), + ResourceQuery { + tags: self.tags, + ..Default::default() + }, + None, + None, user, PermissionLevel::Execute.into(), - &[], + &all_tags, ) .await?; diff --git a/bin/core/src/api/write/terminal.rs b/bin/core/src/api/write/terminal.rs index 815847072d..dc42b2086e 100644 --- a/bin/core/src/api/write/terminal.rs +++ b/bin/core/src/api/write/terminal.rs @@ -171,10 +171,10 @@ impl Resolve for DeleteTerminal { ) )] async fn resolve( - self, + mut self, WriteArgs { user }: &WriteArgs, ) -> mogh_error::Result { - let server = match &self.target { + let server = match &mut self.target { TerminalTarget::Server { server } => { let server = server .as_ref() @@ -195,27 +195,31 @@ impl Resolve for DeleteTerminal { ) .await? } - TerminalTarget::Stack { stack, .. } => { - let server = get_check_permissions::( - stack, + TerminalTarget::Stack { + stack: stack_id, .. + } => { + let stack = get_check_permissions::( + stack_id, user, PermissionLevel::Read.terminal(), ) - .await? - .config - .server_id; - resource::get::(&server).await? + .await?; + // Must fix any incoming stack name to id + *stack_id = stack.id; + resource::get::(&stack.config.server_id).await? } - TerminalTarget::Deployment { deployment } => { - let server = get_check_permissions::( - deployment, + TerminalTarget::Deployment { + deployment: deployment_id, + } => { + let deployment = get_check_permissions::( + deployment_id, user, PermissionLevel::Read.terminal(), ) - .await? - .config - .server_id; - resource::get::(&server).await? + .await?; + // Must fix any incoming deployment name to id + *deployment_id = deployment.id; + resource::get::(&deployment.config.server_id).await? } }; @@ -289,6 +293,8 @@ impl Resolve for BatchDeleteAllTerminals { resource::list_full_for_user::( self.query, + None, + None, user, PermissionLevel::Read.terminal(), &all_tags, diff --git a/bin/core/src/api/ws/mod.rs b/bin/core/src/api/ws/mod.rs index 7c986a7ed1..9d71c7369c 100644 --- a/bin/core/src/api/ws/mod.rs +++ b/bin/core/src/api/ws/mod.rs @@ -24,6 +24,9 @@ use crate::{ mod terminal; mod update; +pub(super) const WS_KEEP_ALIVE_INTERVAL: std::time::Duration = + std::time::Duration::from_secs(30); + pub fn router() -> Router { Router::new() // Periphery facing diff --git a/bin/core/src/api/ws/terminal.rs b/bin/core/src/api/ws/terminal.rs index a6534d45f6..e7959c9a39 100644 --- a/bin/core/src/api/ws/terminal.rs +++ b/bin/core/src/api/ws/terminal.rs @@ -117,9 +117,25 @@ async fn forward_ws_channel( }; let periphery_to_core = async { + let mut keep_alive = + tokio::time::interval(super::WS_KEEP_ALIVE_INTERVAL); loop { // Already adheres to cancellation token - match periphery_receiver.recv().await { + let recv = tokio::select! { + // Websocket keep-alive ping + _ = keep_alive.tick() => { + if client_send + .send(ws::Message::Ping(Bytes::new())) + .await + .is_err() + { + break; + } + continue; + } + recv = periphery_receiver.recv() => recv, + }; + match recv { Ok(Ok(bytes)) => { if let Err(e) = client_send.send(ws::Message::Binary(bytes.into())).await diff --git a/bin/core/src/api/ws/update.rs b/bin/core/src/api/ws/update.rs index 74d7519176..b5503e9a59 100644 --- a/bin/core/src/api/ws/update.rs +++ b/bin/core/src/api/ws/update.rs @@ -3,6 +3,7 @@ use axum::{ extract::{WebSocketUpgrade, ws::Message}, response::IntoResponse, }; +use bytes::Bytes; use futures_util::{SinkExt, StreamExt}; use komodo_client::entities::{ ResourceTarget, permission::PermissionLevel, user::User, @@ -38,10 +39,19 @@ pub async fn handler( let cancel_clone = cancel.clone(); tokio::spawn(async move { + let mut keep_alive = + tokio::time::interval(super::WS_KEEP_ALIVE_INTERVAL); loop { - // poll for updates off the receiver / await cancel. + // poll for updates off the receiver / await cancel / send keep-alive ping. let update = select! { _ = cancel_clone.cancelled() => break, + _ = keep_alive.tick() => { + if ws_sender.send(Message::Ping(Bytes::new())).await.is_err() { + cancel_clone.cancel(); + break; + } + continue; + } update = receiver.recv() => {update.expect("failed to recv update msg")} }; diff --git a/bin/core/src/cloud/aws/ec2.rs b/bin/core/src/cloud/aws/ec2.rs index 854969e536..563f2e98e1 100644 --- a/bin/core/src/cloud/aws/ec2.rs +++ b/bin/core/src/cloud/aws/ec2.rs @@ -88,7 +88,7 @@ pub async fn launch_ec2_instance( port: _, use_https: _, git_providers: _, - docker_registries: _, + image_registries: _, secrets: _, } = config; let instance_type = handle_unknown_instance_type( diff --git a/bin/core/src/cloud/mod.rs b/bin/core/src/cloud/mod.rs index 60e9aa4ac5..48851fce4b 100644 --- a/bin/core/src/cloud/mod.rs +++ b/bin/core/src/cloud/mod.rs @@ -2,8 +2,9 @@ pub mod aws; #[derive(Debug)] pub enum BuildCleanupData { - /// Nothing to clean up - Server, + /// Store the builder id if it needs + /// to be removed from builder_usage_cache. + Server(Option), /// Cleanup Periphery connection Url, /// Clean up AWS instance diff --git a/bin/core/src/config.rs b/bin/core/src/config.rs index 8459a70189..21e919dd54 100644 --- a/bin/core/src/config.rs +++ b/bin/core/src/config.rs @@ -348,6 +348,9 @@ pub fn core_config() -> &'static CoreConfig { transparent_mode: env .komodo_transparent_mode .unwrap_or(config.transparent_mode), + default_pagination_limit: env + .komodo_default_pagination_limit + .unwrap_or(config.default_pagination_limit), ui_write_disabled: env .komodo_ui_write_disabled .unwrap_or(config.ui_write_disabled), @@ -430,6 +433,12 @@ pub fn core_config() -> &'static CoreConfig { ssl_cert_file: env .komodo_ssl_cert_file .unwrap_or(config.ssl_cert_file), + reporting_enabled: env + .komodo_reporting_enabled + .unwrap_or(config.reporting_enabled), + reporting_private_key: env + .komodo_reporting_private_key + .unwrap_or(config.reporting_private_key), ui_path: env.komodo_ui_path.unwrap_or(config.ui_path), ui_index_force_no_cache: env .komodo_ui_index_force_no_cache @@ -447,7 +456,7 @@ pub fn core_config() -> &'static CoreConfig { // These can't be overridden on env secrets: config.secrets, git_providers: config.git_providers, - docker_registries: config.docker_registries, + image_registries: config.image_registries, } }) } diff --git a/bin/core/src/connection/server.rs b/bin/core/src/connection/server.rs index ee5d6d4ad3..8631ca441e 100644 --- a/bin/core/src/connection/server.rs +++ b/bin/core/src/connection/server.rs @@ -503,7 +503,7 @@ async fn create_server_maybe_builder( name: server_query, config: PartialBuilderConfig::Server( PartialServerBuilderConfig { - server_id: Some(server.id.clone()), + server_ids: Some(vec![server.id.clone()]), }, ), }) diff --git a/bin/core/src/helpers/builder.rs b/bin/core/src/helpers/builder.rs index 19af31bc3a..d37ba1de67 100644 --- a/bin/core/src/helpers/builder.rs +++ b/bin/core/src/helpers/builder.rs @@ -1,16 +1,18 @@ -use std::time::Duration; +use std::{collections::HashMap, time::Duration}; use anyhow::{Context, anyhow}; use database::mungos::mongodb::bson::oid::ObjectId; use formatting::muted; +use futures_util::{StreamExt, stream::FuturesOrdered}; use komodo_client::entities::{ Version, builder::{AwsBuilderConfig, Builder, BuilderConfig}, komodo_timestamp, - server::Server, + server::{Server, ServerState}, update::{Log, Update}, }; use periphery_client::api::{self, GetVersionResponse}; +use tokio::sync::Mutex; use crate::{ cloud::{ @@ -24,6 +26,7 @@ use crate::{ helpers::update::update_update, periphery::PeripheryClient, resource, + state::{builder_usage_cache, server_status_cache}, }; use super::periphery_client; @@ -37,25 +40,26 @@ const BUILDER_POLL_MAX_TRIES: usize = 60; fields( resource_name, builder_id = builder.id, - update_id = update.id + update_id = update.as_ref().map(|u| u.id.as_str()) ) )] pub async fn connect_builder_periphery( - // build: &Build, resource_name: String, version: Option, builder: Builder, - update: &mut Update, + update: Option<&mut Update>, ) -> anyhow::Result<(PeripheryClient, BuildCleanupData)> { match builder.config { + BuilderConfig::Aws(config) => { + get_aws_builder(&resource_name, version, config, update).await + } BuilderConfig::Url(config) => { if config.address.is_empty() { return Err(anyhow!( "Builder has not yet configured an address" )); } - // TODO: Dont use builder id, or will be problems - // with simultaneous spawned builders. + // Builder id no good because it may be active for multiple connections. let periphery = PeripheryClient::new( PeripheryConnectionArgs::from_url_builder( &ObjectId::new().to_hex(), @@ -64,22 +68,66 @@ pub async fn connect_builder_periphery( config.insecure_tls, ) .await?; - periphery - .health_check() - .await - .context("Url Builder failed health check")?; - Ok((periphery, BuildCleanupData::Url)) + // Poll for connection to be estalished + let mut err = None; + for _ in 0..10 { + tokio::time::sleep(Duration::from_secs(1)).await; + match periphery + .health_check() + .await + .context("Url Builder failed health check") + { + Ok(_) => return Ok((periphery, BuildCleanupData::Url)), + Err(e) => err = Some(e), + }; + } + Err(err.context("Missing error")?) } BuilderConfig::Server(config) => { - if config.server_id.is_empty() { - return Err(anyhow!("Builder has not configured a server")); + if config.server_ids.is_empty() { + return Err(anyhow!( + "Server Builder has no configured Servers" + )); } - let server = resource::get::(&config.server_id).await?; + + // Short path for single configured builder + if config.server_ids.len() == 1 { + let server = + resource::get::(&config.server_ids[0]).await?; + let periphery = periphery_client(&server).await?; + return Ok((periphery, BuildCleanupData::Server(None))); + } + + // Get filtered list of available Servers + let server_status_cache = server_status_cache(); + let available_server_ids = config + .server_ids + .iter() + .map(|server_id| async move { + server_status_cache + .get(server_id) + .await + .map(|s| matches!(s.state, ServerState::Ok)) + .unwrap_or_default() + .then_some(server_id) + }) + .collect::>() + .collect::>() + .await + .into_iter() + .flatten() + .collect::>(); + + let selected = builder_usage_cache() + .get_or_insert_default(&builder.id) + .await + .select(&available_server_ids) + .await + .context("Server builder has no available servers")?; + + let server = resource::get::(selected).await?; let periphery = periphery_client(&server).await?; - Ok((periphery, BuildCleanupData::Server)) - } - BuilderConfig::Aws(config) => { - get_aws_builder(&resource_name, version, config, update).await + Ok((periphery, BuildCleanupData::Server(Some(server.id)))) } } } @@ -89,14 +137,14 @@ pub async fn connect_builder_periphery( skip_all, fields( resource_name, - update_id = update.id, + update_id = update.as_ref().map(|u| u.id.as_str()), ) )] async fn get_aws_builder( resource_name: &str, version: Option, config: AwsBuilderConfig, - update: &mut Update, + mut update: Option<&mut Update>, ) -> anyhow::Result<(PeripheryClient, BuildCleanupData)> { let start_create_ts = komodo_timestamp(); @@ -114,9 +162,10 @@ async fn get_aws_builder( ..Default::default() }; - update.logs.push(log); - - update_update(update.clone()).await?; + if let Some(update) = &mut update { + update.logs.push(log); + update_update((*update).clone()).await?; + } let protocol = if config.use_https { "wss" } else { "ws" }; @@ -153,8 +202,10 @@ async fn get_aws_builder( end_ts: komodo_timestamp(), ..Default::default() }; - update.logs.push(connect_log); - update_update(update.clone()).await?; + if let Some(update) = update { + update.logs.push(connect_log); + update_update(update.clone()).await?; + } return Ok(( periphery, BuildCleanupData::Aws { @@ -194,9 +245,17 @@ pub async fn cleanup_builder_instance( update: &mut Update, ) { match cleanup_data { - BuildCleanupData::Server => { + BuildCleanupData::Server(None) => { // Nothing to clean up } + BuildCleanupData::Server(Some(builder_id)) => { + // Release periphery (server) id from builder + builder_usage_cache() + .get_or_insert_default(&builder_id) + .await + .release(&periphery.id) + .await + } BuildCleanupData::Url => { periphery.cleanup().await; } @@ -252,3 +311,29 @@ pub fn start_aws_builder_log( ] .join("\n") } + +#[derive(Default)] +pub struct BuilderUsage(Mutex>); + +impl BuilderUsage { + pub async fn select<'a>( + &self, + available: &'a [&String], + ) -> Option<&'a str> { + let mut lock = self.0.lock().await; + let selected = *available.iter().min_by_key(|key| { + lock.get(key.as_str()).copied().unwrap_or(0) + })?; + *lock.entry(selected.clone()).or_insert(0) += 1; + Some(selected.as_str()) + } + + pub async fn release(&self, key: &str) { + let mut lock = self.0.lock().await; + if let Some(count) = lock.get_mut(key) + && *count > 0 + { + *count -= 1; + } + } +} diff --git a/bin/core/src/helpers/mod.rs b/bin/core/src/helpers/mod.rs index 1fe282fb2a..d327dbac98 100644 --- a/bin/core/src/helpers/mod.rs +++ b/bin/core/src/helpers/mod.rs @@ -169,7 +169,7 @@ pub async fn registry_token( } Ok( core_config() - .docker_registries + .image_registries .iter() .find(|provider| provider.domain == provider_domain) .and_then(|provider| { diff --git a/bin/core/src/helpers/procedure.rs b/bin/core/src/helpers/procedure.rs index b06b3497df..3155911d56 100644 --- a/bin/core/src/helpers/procedure.rs +++ b/bin/core/src/helpers/procedure.rs @@ -14,12 +14,13 @@ use komodo_client::{ procedure::{Procedure, ProcedureStage}, repo::Repo, stack::Stack, - update::{Log, Update}, + update::Update, user::procedure_user, }, }; use mogh_resolver::Resolve; use tokio::sync::Mutex; +use tokio_util::sync::CancellationToken; use uuid::Uuid; use crate::{ @@ -27,6 +28,7 @@ use crate::{ execute::{ExecuteArgs, ExecuteRequest}, write::WriteArgs, }, + config::core_config, resource::{KomodoResource, list_full_for_user_using_pattern}, state::{all_resources_cache, db_client}, }; @@ -36,8 +38,21 @@ use super::update::{init_execution_update, update_update}; pub async fn execute_procedure( procedure: &Procedure, update: &Mutex, + cancel: CancellationToken, ) -> anyhow::Result<()> { for stage in &procedure.config.stages { + if cancel.is_cancelled() { + add_line_to_update( + update, + &format!( + "{}: Cancelled before stage: '{}'", + muted("ERROR"), + bold(&stage.name) + ), + ) + .await; + return Err(anyhow!("Procedure cancelled before completion")); + } if !stage.enabled { continue; } @@ -274,6 +289,9 @@ async fn execute_execution( } resolve_execute!(RunProcedure, req) } + Execution::CancelProcedure(req) => { + resolve_execute!(CancelProcedure, req) + } // Special: write operation Execution::CommitSync(req) => req .resolve(&WriteArgs { user }) @@ -326,6 +344,9 @@ async fn execute_execution( } // Standard executions Execution::RunAction(req) => resolve_execute!(RunAction, req), + Execution::CancelAction(req) => { + resolve_execute!(CancelAction, req) + } Execution::RunBuild(req) => resolve_execute!(RunBuild, req), Execution::CancelBuild(req) => resolve_execute!(CancelBuild, req), Execution::Deploy(req) => resolve_execute!(Deploy, req), @@ -485,9 +506,10 @@ async fn execute_execution( Ok(()) } else { Err(anyhow!( - "{}: execution not successful. see update '{}'", + "{}: Execution not successful, see update: '{}/updates/{}'", colored("ERROR", Color::Red), - bold(&update.id), + core_config().host, + update.id, )) } } @@ -501,14 +523,13 @@ async fn handle_resolve_result( match res { Ok(res) => Ok(res), Err(e) => { - let log = - Log::error("execution error", format_serror(&e.into())); let mut update = find_one_by_id(&db_client().updates, update_id) .await .context("Failed to query to db")? .context("no update exists with given id")?; - update.logs.push(log); + update + .push_error_log("Execution error", format_serror(&e.into())); update.finalize(); update_update(update.clone()).await?; Ok(update) @@ -536,6 +557,8 @@ async fn extend_batch_exection( let more = list_full_for_user_using_pattern::( pattern, Default::default(), + None, + None, procedure_user(), PermissionLevel::Read.into(), &[], @@ -721,7 +744,9 @@ pub fn replace_procedure_stage_ids_with_names( replace_id_with_name!( RunProcedure => procedure, procedures; + CancelProcedure => procedure, procedures; RunAction => action, actions; + CancelAction => action, actions; RunBuild => build, builds; CancelBuild => build, builds; Deploy => deployment, deployments; diff --git a/bin/core/src/helpers/prune.rs b/bin/core/src/helpers/prune.rs index f5d7963058..888762d339 100644 --- a/bin/core/src/helpers/prune.rs +++ b/bin/core/src/helpers/prune.rs @@ -55,7 +55,7 @@ async fn prune_images() -> anyhow::Result<()> { while let Some((res, server)) = futures.next().await { if let Err(e) = res { warn!( - "failed to prune images on Server {} ({}) | {e:#}", + "Failed to prune images on Server {} ({}) | {e:#}", server.name, server.id ) } diff --git a/bin/core/src/helpers/query.rs b/bin/core/src/helpers/query.rs index 98d7ca4780..536aaee320 100644 --- a/bin/core/src/helpers/query.rs +++ b/bin/core/src/helpers/query.rs @@ -45,7 +45,7 @@ use crate::{ state::{ action_state_cache, action_states, db_client, deployment_status_cache, procedure_state_cache, - stack_status_cache, + server_status_cache, stack_status_cache, }, }; @@ -97,6 +97,38 @@ pub async fn get_server_state(server: &Server) -> ServerState { } } +/// The Server state as computed for the server list items, +/// from the in memory status cache. +pub async fn get_cached_server_state(id: &String) -> ServerState { + server_status_cache() + .get(id) + .await + .map(|status| status.state) + .unwrap_or_default() +} + +/// The Stack state as computed for the stack list items, +/// from the in memory action states / status cache. +pub async fn get_cached_stack_state(id: &String) -> StackState { + if action_states() + .stack + .get(id) + .await + .map(|s| s.get().map(|s| s.deploying)) + .transpose() + .ok() + .flatten() + .unwrap_or_default() + { + return StackState::Deploying; + } + stack_status_cache() + .get(id) + .await + .map(|status| status.curr.state) + .unwrap_or_default() +} + pub async fn get_deployment_state( id: &String, ) -> anyhow::Result { diff --git a/bin/core/src/helpers/swarm.rs b/bin/core/src/helpers/swarm.rs index dccc998ce4..d3317ace97 100644 --- a/bin/core/src/helpers/swarm.rs +++ b/bin/core/src/helpers/swarm.rs @@ -1,5 +1,3 @@ -use std::time::Duration; - use anyhow::anyhow; use komodo_client::entities::server::{Server, ServerState}; use mogh_resolver::HasResponse; @@ -13,23 +11,6 @@ pub async fn swarm_request( server_ids: &[String], request: T, ) -> anyhow::Result -where - T: std::fmt::Debug + Clone + Serialize + HasResponse, - T::Response: DeserializeOwned, -{ - swarm_request_custom_timeout( - server_ids, - request, - Duration::from_secs(10), - ) - .await -} - -pub async fn swarm_request_custom_timeout( - server_ids: &[String], - request: T, - timeout: Duration, -) -> anyhow::Result where T: std::fmt::Debug + Clone + Serialize + HasResponse, T::Response: DeserializeOwned, @@ -54,10 +35,7 @@ where else { continue; }; - match periphery - .request_custom_timeout(request.clone(), timeout) - .await - { + match periphery.request(request.clone()).await { Ok(res) => return Ok(res), Err(e) => err = Some(e), } diff --git a/bin/core/src/helpers/terminal.rs b/bin/core/src/helpers/terminal.rs index 7ef0787396..e4755a0d00 100644 --- a/bin/core/src/helpers/terminal.rs +++ b/bin/core/src/helpers/terminal.rs @@ -332,7 +332,7 @@ pub async fn get_deployment_periphery_container( let periphery = periphery_client(&server).await?; - let container = deployment.name.clone(); + let container = deployment.deployed_name().to_string(); Ok(( TerminalTarget::Deployment { diff --git a/bin/core/src/helpers/update.rs b/bin/core/src/helpers/update.rs index 03b727c783..61b43d8a03 100644 --- a/bin/core/src/helpers/update.rs +++ b/bin/core/src/helpers/update.rs @@ -226,8 +226,10 @@ pub async fn init_execution_update( (CancelRepoBuild, Repo, repo), // Procedure (RunProcedure, Procedure, procedure), + (CancelProcedure, Procedure, procedure), // Action (RunAction, Action, action), + (CancelAction, Action, action), // Resource Sync (RunSync, ResourceSync, sync), // Stack (simple) diff --git a/bin/core/src/main.rs b/bin/core/src/main.rs index 3949d75c76..e96e277501 100644 --- a/bin/core/src/main.rs +++ b/bin/core/src/main.rs @@ -19,6 +19,7 @@ mod monitor; mod network; mod periphery; mod permission; +mod reporting; mod resource; mod schedule; mod stack; @@ -71,6 +72,7 @@ async fn app() -> anyhow::Result<()> { resource::spawn_action_state_refresh_loop(); schedule::spawn_schedule_executor(); helpers::prune::spawn_prune_loop(); + reporting::spawn_reporting_loop(); } .instrument(startup_span) .await; diff --git a/bin/core/src/monitor/alert/mod.rs b/bin/core/src/monitor/alert/mod.rs index c213973d84..e00f7065c3 100644 --- a/bin/core/src/monitor/alert/mod.rs +++ b/bin/core/src/monitor/alert/mod.rs @@ -36,6 +36,8 @@ async fn get_all_swarms_map() -> anyhow::Result<(HashMap, HashMap)> { let swarms = resource::list_full_for_user::( ResourceQuery::default(), + None, + None, system_user(), PermissionLevel::Read.into(), &[], @@ -61,6 +63,8 @@ async fn get_all_servers_map() { let servers = resource::list_full_for_user::( ResourceQuery::default(), + None, + None, system_user(), PermissionLevel::Read.into(), &[], diff --git a/bin/core/src/monitor/mod.rs b/bin/core/src/monitor/mod.rs index bc6be6ba3e..eec92b2097 100644 --- a/bin/core/src/monitor/mod.rs +++ b/bin/core/src/monitor/mod.rs @@ -52,17 +52,18 @@ pub fn spawn_monitoring_loops() { fn spawn_server_monitoring_loop() { tokio::spawn(async move { - refresh_all_server_cache(komodo_timestamp()).await; + refresh_all_server_cache().await; let interval = monitoring_interval(); loop { let ts = (wait_until_timelength(interval, ADDITIONAL_MS).await - ADDITIONAL_MS) as i64; - refresh_all_server_cache(ts).await; + refresh_all_server_cache().await; + tokio::join!(check_alerts(ts), record_server_stats(ts)); } }); } -async fn refresh_all_server_cache(ts: i64) { +async fn refresh_all_server_cache() { let servers = match find_collect(&db_client().servers, None, None).await { Ok(servers) => servers, @@ -77,7 +78,6 @@ async fn refresh_all_server_cache(ts: i64) { refresh_server_cache(&server, false).await; }); join_all(futures).await; - tokio::join!(check_alerts(ts), record_server_stats(ts)); } /// Makes sure cache for server doesn't update too frequently / simultaneously. @@ -193,7 +193,8 @@ pub fn refresh_server_cache( if let Some(docker) = &mut docker { docker.containers.iter_mut().for_each(|container| { - container.server_id = Some(server.id.clone()) + container.server_id = Some(server.id.clone()); + container.server_name = Some(server.name.clone()); }); } diff --git a/bin/core/src/monitor/record.rs b/bin/core/src/monitor/record.rs index 3d3eb626c9..559ee734c8 100644 --- a/bin/core/src/monitor/record.rs +++ b/bin/core/src/monitor/record.rs @@ -20,9 +20,13 @@ pub async fn record_server_stats(ts: i64) { ts, sid: status.id.clone(), cpu_perc: stats.cpu_perc, - load_average: stats.load_average.clone(), + load_average: stats.load_average, mem_total_gb: stats.mem_total_gb, mem_used_gb: stats.mem_used_gb, + mem_buff_cache_gb: stats.mem_buff_cache_gb, + mem_zfs_arc_gb: stats.mem_zfs_arc_gb, + swap_total_gb: stats.swap_total_gb, + swap_used_gb: stats.swap_used_gb, disk_total_gb, disk_used_gb, disks: stats.disks.clone(), diff --git a/bin/core/src/monitor/resources.rs b/bin/core/src/monitor/resources.rs index 49fe252090..a812e75627 100644 --- a/bin/core/src/monitor/resources.rs +++ b/bin/core/src/monitor/resources.rs @@ -6,7 +6,10 @@ use komodo_client::entities::{ container::ContainerListItem, image::ImageListItem, service::SwarmServiceListItem, stack::SwarmStackListItem, }, - stack::{Stack, StackService, StackServiceNames, StackState}, + stack::{ + Stack, StackService, StackServiceNames, StackServiceState, + StackState, + }, swarm::SwarmState, }; @@ -95,10 +98,18 @@ pub async fn update_swarm_stack_cache( None, )); + let state = swarm_service + .as_ref() + .map(|c| c.state.into()) + .unwrap_or(StackServiceState::Unknown); + StackService { + stack_id: stack.id.clone(), + stack_name: stack.name.clone(), service: service_name.clone(), container: None, swarm_service, + state, image, image_digests, } @@ -176,11 +187,19 @@ pub async fn update_server_stack_cache( None )); + let state = container + .as_ref() + .map(|c| c.state.into()) + .unwrap_or(StackServiceState::Unknown); + StackService { + stack_id: stack.id.clone(), + stack_name: stack.name.clone(), service: service_name.clone(), image: image.clone(), container, swarm_service: None, + state, image_digests, } }).collect::>(); @@ -228,7 +247,7 @@ pub async fn update_swarm_deployment_cache( service .name .as_ref() - .map(|name| name == &deployment.name) + .map(|name| name == deployment.deployed_name()) .unwrap_or_default() }) .cloned(); @@ -282,7 +301,7 @@ pub async fn update_server_deployment_cache( for deployment in deployments { let container = containers .iter() - .find(|container| container.name == deployment.name) + .find(|container| container.name == deployment.deployed_name()) .cloned(); let image_digests = container diff --git a/bin/core/src/monitor/swarm.rs b/bin/core/src/monitor/swarm.rs index b76e492d02..8c0673ac69 100644 --- a/bin/core/src/monitor/swarm.rs +++ b/bin/core/src/monitor/swarm.rs @@ -1,7 +1,4 @@ -use std::{ - sync::{Arc, OnceLock}, - time::Duration, -}; +use std::sync::{Arc, OnceLock}; use anyhow::anyhow; use async_timing_util::wait_until_timelength; @@ -20,7 +17,7 @@ use tokio::sync::Mutex; use crate::{ config::monitoring_interval, - helpers::swarm::swarm_request_custom_timeout, + helpers::swarm::swarm_request, monitor::{ RefreshCacheResources, resources::{ @@ -118,12 +115,8 @@ pub async fn refresh_swarm_cache(swarm: &Swarm, force: bool) { } let PollSwarmStatusResponse { inspect, lists } = - match swarm_request_custom_timeout( - &swarm.config.server_ids, - PollSwarmStatus {}, - Duration::from_secs(1), - ) - .await + match swarm_request(&swarm.config.server_ids, PollSwarmStatus {}) + .await { Ok(info) => info, Err(e) => { diff --git a/bin/core/src/periphery/mod.rs b/bin/core/src/periphery/mod.rs index 9a05053047..0dcd5a661d 100644 --- a/bin/core/src/periphery/mod.rs +++ b/bin/core/src/periphery/mod.rs @@ -85,20 +85,6 @@ impl PeripheryClient { &self, request: T, ) -> anyhow::Result - where - T: std::fmt::Debug + Serialize + HasResponse, - T::Response: DeserializeOwned, - { - self - .request_custom_timeout(request, Duration::from_secs(10)) - .await - } - - pub async fn request_custom_timeout( - &self, - request: T, - timeout: Duration, - ) -> anyhow::Result where T: std::fmt::Debug + Serialize + HasResponse, T::Response: DeserializeOwned, @@ -134,11 +120,17 @@ impl PeripheryClient { let res = async { // Poll for the associated response loop { - let message = - response_receiever.recv().with_timeout(timeout).await?; + let message = response_receiever + .recv() + // Periphery request handler sends pings every 4s + // *on this channel specifically* so Core knows + // request is being processed. Hardcoded 11s + // allows for missed 5s ping due to network reconnect. + .with_timeout(Duration::from_secs(10)) + .await?; - // Still in progress, sent to avoid timeout. let Some(message) = message.decode()? else { + // Just a ping from periphery request handler continue; }; diff --git a/bin/core/src/permission.rs b/bin/core/src/permission.rs index 0351db447e..c0c7d705f0 100644 --- a/bin/core/src/permission.rs +++ b/bin/core/src/permission.rs @@ -1,10 +1,10 @@ -use std::collections::HashMap; +use std::{collections::HashMap, future::Future}; use anyhow::{Context, anyhow}; use database::{ bson::Document, mongo_indexed::doc, mungos::find::find_collect, }; -use futures_util::{FutureExt, future::BoxFuture}; +use futures_util::{TryStreamExt, future::BoxFuture}; use indexmap::IndexSet; use komodo_client::{ api::read::GetPermission, @@ -16,7 +16,9 @@ use komodo_client::{ builder::Builder, deployment::Deployment, permission::SpecificPermission, - permission::{PermissionLevel, PermissionLevelAndSpecifics}, + permission::{ + Permission, PermissionLevel, PermissionLevelAndSpecifics, + }, procedure::Procedure, repo::Repo, resource::Resource, @@ -176,14 +178,34 @@ pub fn get_user_permission_on_resource<'a, T: KomodoResource>( }) } -pub async fn list_resources_for_user( - filters: impl Into>, +/// Precomputed user permissions for listing resources of a type. +/// Load with [load_list_permits], then check visibility of +/// each resource with [ListPermits::permitted]. +pub enum ListPermits { + /// The user can see all resources of the type, + /// eg. admin, transparent mode, or 'all' access on the variant. + Unrestricted, + /// Visibility must be checked per-resource + /// against the permissions table. + Fine(Box), +} + +pub struct FineListPermits { + required: PermissionLevelAndSpecifics, + base: PermissionLevelAndSpecifics, + permission_by_resource_id: HashMap, + additional_specific_cache: + HashMap>, + user: User, +} + +pub async fn load_list_permits( user: &User, - permission: PermissionLevelAndSpecifics, -) -> anyhow::Result>> { + required: PermissionLevelAndSpecifics, +) -> anyhow::Result { // Check admin if user.admin { - return list_all_resources::(filters).await; + return Ok(ListPermits::Unrestricted); } let mut base = PermissionLevelAndSpecifics { @@ -196,8 +218,8 @@ pub async fn list_resources_for_user( }; // 'transparent_mode' early return. - if base.fulfills(&permission) { - return list_all_resources::(filters).await; + if base.fulfills(&required) { + return Ok(ListPermits::Unrestricted); } let resource_type = T::resource_type(); @@ -206,8 +228,8 @@ pub async fn list_resources_for_user( if let Some(all_permission) = user.all.get(&resource_type) { base.elevate(all_permission); // 'user.all' early return. - if base.fulfills(&permission) { - return list_all_resources::(filters).await; + if base.fulfills(&required) { + return Ok(ListPermits::Unrestricted); } } @@ -217,25 +239,23 @@ pub async fn list_resources_for_user( if let Some(all_permission) = group.all.get(&resource_type) { base.elevate(all_permission); // 'group.all' early return. - if base.fulfills(&permission) { - return list_all_resources::(filters).await; + if base.fulfills(&required) { + return Ok(ListPermits::Unrestricted); } } } - let (all, permissions) = tokio::try_join!( - list_all_resources::(filters), - // And any ids using the permissions table - find_collect( - &db_client().permissions, - doc! { - "$or": user_target_query(&user.id, &groups)?, - "resource_target.type": resource_type.as_ref(), - }, - None, - ) - .map(|res| res.context("failed to query permissions on db")) - )?; + // Pull any permissions on the variant using the permissions table + let permissions = find_collect( + &db_client().permissions, + doc! { + "$or": user_target_query(&user.id, &groups)?, + "resource_target.type": resource_type.as_ref(), + }, + None, + ) + .await + .context("failed to query permissions on db")?; let permission_by_resource_id = permissions .into_iter() @@ -247,34 +267,54 @@ pub async fn list_resources_for_user( }) .collect::>(); - let mut resources = Vec::new(); - let mut additional_specific_cache = - HashMap::>::new(); + Ok(ListPermits::Fine( + FineListPermits { + required, + base, + permission_by_resource_id, + additional_specific_cache: Default::default(), + user: user.clone(), + } + .into(), + )) +} + +impl ListPermits { + /// Check whether the user can see the given resource. + pub async fn permitted( + &mut self, + resource: &Resource, + ) -> anyhow::Result { + let ListPermits::Fine(fine) = self else { + return Ok(true); + }; - for resource in all { let mut perm = if let Some(perm) = - permission_by_resource_id.get(&resource.id) + fine.permission_by_resource_id.get(&resource.id) { - base.join(perm) + fine.base.join(perm) } else { - base.clone() + fine.base.clone() }; + // Add in the resource level base permissions, + // matching [get_user_permission_on_resource]. + perm.elevate(&resource.base_permission); // Check if already fulfils - if perm.fulfills(&permission) { - resources.push(resource); - continue; + if perm.fulfills(&fine.required) { + return Ok(true); } // Also check if fulfills with inherited specific let additional_target = if let Some(additional_target) = - T::inherit_specific_permissions_from(&resource) + T::inherit_specific_permissions_from(resource) && !additional_target.is_empty() { additional_target } else { - continue; + return Ok(false); }; - let additional_specific = match additional_specific_cache + let additional_specific = match fine + .additional_specific_cache .get(&additional_target) .cloned() { @@ -283,21 +323,99 @@ pub async fn list_resources_for_user( let specific = GetPermission { target: additional_target.clone(), } - .resolve(&ReadArgs { user: user.clone() }) + .resolve(&ReadArgs { + user: fine.user.clone(), + }) .await .map_err(|e| e.error) .context( "failed to get user permission on additional target", )? .specific; - additional_specific_cache + fine + .additional_specific_cache .insert(additional_target, specific.clone()); specific } }; perm.specific.extend(additional_specific); - if perm.fulfills(&permission) { - resources.push(resource); + Ok(perm.fulfills(&fine.required)) + } +} + +pub async fn list_resources_for_user( + filters: impl Into>, + limit: impl Into>, + skip: impl Into>, + user: &User, + permission: PermissionLevelAndSpecifics, +) -> anyhow::Result>> { + let mut permits = load_list_permits::(user, permission).await?; + + if let ListPermits::Unrestricted = permits { + return list_all_resources::(filters, limit, skip).await; + } + + list_resources_with_permits::( + &mut permits, + filters.into(), + limit.into(), + skip.into(), + |resource| async move { Some(resource) }, + ) + .await +} + +/// Drive the cursor directly, checking each resource against the +/// user permissions and the additional `filter` (eg. by state +/// computed from the in memory caches), with limit / skip applied +/// in memory after the filters. Stops pulling from the cursor as +/// soon as the limit is reached, and avoids collecting resources +/// the user cannot see. +/// +/// `filter` receives each resource by value, and returns +/// `Some(resource)` to keep it in the results, or `None` to drop it. +pub async fn list_resources_with_permits( + permits: &mut ListPermits, + filters: Option, + limit: Option, + skip: Option, + filter: impl Fn(Resource) -> F, +) -> anyhow::Result>> +where + F: Future>> + Send, +{ + let mut cursor = T::coll() + .find(filters.unwrap_or_default()) + .sort(doc! { "name": 1 }) + .await + .with_context(|| { + format!("Failed to query db for {}s", T::resource_type()) + })?; + + let limit = limit.unwrap_or_default().max(0) as usize; + let skip = skip.unwrap_or_default(); + let mut skipped = 0; + let mut resources = Vec::new(); + + while let Some(resource) = cursor + .try_next() + .await + .context("Failed to pull next resource from db cursor")? + { + if !permits.permitted::(&resource).await? { + continue; + } + let Some(resource) = filter(resource).await else { + continue; + }; + if skipped < skip { + skipped += 1; + continue; + } + resources.push(resource); + if limit != 0 && resources.len() >= limit { + break; } } @@ -307,55 +425,28 @@ pub async fn list_resources_for_user( /// Returns None if still no need to filter by resource id (eg transparent mode, group membership with all access). pub async fn list_resource_ids_for_user( filters: Option, + limit: impl Into>, + skip: impl Into>, user: &User, permission: PermissionLevelAndSpecifics, ) -> anyhow::Result>> { - // Check admin - if user.admin { - return Ok(None); - } + let mut permits = load_list_permits::(user, permission).await?; - let mut base = PermissionLevelAndSpecifics { - level: if core_config().transparent_mode { - PermissionLevel::Read - } else { - PermissionLevel::None - }, - specific: Default::default(), - }; - - // 'transparent_mode' early return. - if base.fulfills(&permission) { + if let ListPermits::Unrestricted = permits { return Ok(None); } - let resource_type = T::resource_type(); - - if let Some(all) = user.all.get(&resource_type) { - base.elevate(all); - // 'user.all' early return. - if base.fulfills(&permission) { - return Ok(None); - } - } - - // Check user groups 'all' on variant - let groups = get_user_user_groups(&user.id).await?; - for group in &groups { - if let Some(all) = group.all.get(&resource_type) { - base.elevate(all); - // 'group.all' early return. - if base.fulfills(&permission) { - return Ok(None); - } - } - } - - let ids = list_resources_for_user::(filters, user, permission) - .await? - .into_iter() - .map(|resource| resource.id) - .collect(); + let ids = list_resources_with_permits::( + &mut permits, + filters, + limit.into(), + skip.into(), + |resource| async move { Some(resource) }, + ) + .await? + .into_iter() + .map(|resource| resource.id) + .collect(); Ok(Some(ids)) } @@ -369,6 +460,8 @@ pub async fn user_resource_target_query( Ok(incoming_query) } else { let swarm_query = list_resource_ids_for_user::( + None, + None, None, user, PermissionLevel::Read.into(), @@ -384,6 +477,8 @@ pub async fn user_resource_target_query( .unwrap_or_else(|| doc! { "target.type": "Swarm" }); let server_query = list_resource_ids_for_user::( + None, + None, None, user, PermissionLevel::Read.into(), @@ -397,6 +492,8 @@ pub async fn user_resource_target_query( .unwrap_or_else(|| doc! { "target.type": "Server" }); let stack_query = list_resource_ids_for_user::( + None, + None, None, user, PermissionLevel::Read.into(), @@ -410,6 +507,8 @@ pub async fn user_resource_target_query( .unwrap_or_else(|| doc! { "target.type": "Stack" }); let deployment_query = list_resource_ids_for_user::( + None, + None, None, user, PermissionLevel::Read.into(), @@ -423,6 +522,8 @@ pub async fn user_resource_target_query( .unwrap_or_else(|| doc! { "target.type": "Deployment" }); let build_query = list_resource_ids_for_user::( + None, + None, None, user, PermissionLevel::Read.into(), @@ -436,6 +537,8 @@ pub async fn user_resource_target_query( .unwrap_or_else(|| doc! { "target.type": "Build" }); let repo_query = list_resource_ids_for_user::( + None, + None, None, user, PermissionLevel::Read.into(), @@ -449,6 +552,8 @@ pub async fn user_resource_target_query( .unwrap_or_else(|| doc! { "target.type": "Repo" }); let procedure_query = list_resource_ids_for_user::( + None, + None, None, user, PermissionLevel::Read.into(), @@ -462,6 +567,8 @@ pub async fn user_resource_target_query( .unwrap_or_else(|| doc! { "target.type": "Procedure" }); let action_query = list_resource_ids_for_user::( + None, + None, None, user, PermissionLevel::Read.into(), @@ -475,6 +582,8 @@ pub async fn user_resource_target_query( .unwrap_or_else(|| doc! { "target.type": "Action" }); let builder_query = list_resource_ids_for_user::( + None, + None, None, user, PermissionLevel::Read.into(), @@ -488,6 +597,8 @@ pub async fn user_resource_target_query( .unwrap_or_else(|| doc! { "target.type": "Builder" }); let alerter_query = list_resource_ids_for_user::( + None, + None, None, user, PermissionLevel::Read.into(), @@ -502,6 +613,8 @@ pub async fn user_resource_target_query( let resource_sync_query = list_resource_ids_for_user::( + None, + None, None, user, PermissionLevel::Read.into(), diff --git a/bin/core/src/reporting.rs b/bin/core/src/reporting.rs new file mode 100644 index 0000000000..352a4ecd0c --- /dev/null +++ b/bin/core/src/reporting.rs @@ -0,0 +1,144 @@ +use std::time::{SystemTime, UNIX_EPOCH}; + +use anyhow::Context; +use async_timing_util::{Timelength, wait_until_timelength}; +use komodo_client::entities::{ + ResourceTargetVariant, core_report::CoreReport, +}; +use mogh_pki::{ + PkiKind, RotatableKeyPair, one_way::OneWayNoiseHandshake, +}; + +use crate::{config::core_config, state::db_client}; + +pub fn spawn_reporting_loop() { + let config = core_config(); + + if !config.reporting_enabled { + return; + } + + let keys = match RotatableKeyPair::from_private_key_spec( + PkiKind::OneWay, + &config.reporting_private_key, + ) { + Ok(keys) => keys, + Err(e) => { + warn!( + "Failed to initialize reporting key pair. Reporting is disabled. {e:#}" + ); + return; + } + }; + + let client = reqwest::Client::default(); + + tokio::spawn(async move { + loop { + wait_until_timelength(Timelength::OneDay, 1100).await; + if let Err(e) = report(&client, &keys).await { + warn!("Reporting failed. {e:#}"); + } + } + }); +} + +async fn report( + client: &reqwest::Client, + keys: &RotatableKeyPair, +) -> anyhow::Result<()> { + let db = db_client(); + + let endpoint_public_key = client + .get("https://mogh.tech/report/public_key") + .send() + .await + .context("Failed to query for reporting endpoint public key for signature.")? + .error_for_status() + .context("Failed response for reporting endpoint public key for signature.")? + .text() + .await + .context("Failed to get reporting endpoint public key for signature.")?; + + let users = db + .users + .count_documents(Default::default()) + .await + .context("Failed to query database for users")?; + let servers = db + .servers + .count_documents(Default::default()) + .await + .context("Failed to query database for servers")?; + let swarms = db + .swarms + .count_documents(Default::default()) + .await + .context("Failed to query database for swarms")?; + let stacks = db + .stacks + .count_documents(Default::default()) + .await + .context("Failed to query database for stacks")?; + let deployments = db + .deployments + .count_documents(Default::default()) + .await + .context("Failed to query database for deployments")?; + let builds = db + .builds + .count_documents(Default::default()) + .await + .context("Failed to query database for builds")?; + + let count = [ + (ResourceTargetVariant::Server, servers), + (ResourceTargetVariant::Swarm, swarms), + (ResourceTargetVariant::Stack, stacks), + (ResourceTargetVariant::Deployment, deployments), + (ResourceTargetVariant::Build, builds), + ] + .into_iter() + .collect(); + + let keys = keys.load(); + + let report = CoreReport { + report_public_key: keys.public().to_string(), + users, + count, + }; + + let serialized = serde_json::to_string(&report) + .context("Failed to serialize report JSON")?; + + let timestamp = + SystemTime::now().duration_since(UNIX_EPOCH)?.as_millis() as i64; + + let prologue = + format!("post|/report/komodo|{serialized}|{}", timestamp); + + let mut handshake = OneWayNoiseHandshake::new_initiator( + keys.private().as_bytes(), + endpoint_public_key.as_bytes(), + prologue.as_bytes(), + )?; + + let signature = handshake + .generate_signature() + .context("Failed to generate report signature")?; + + client + .post("https://mogh.tech/report/komodo") + .header("x-api-signature", signature) + .header("x-api-timestamp", timestamp) + .header("content-type", "application/json") + .body(serialized) + .send() + .await + .context("Failed to post report.")? + .error_for_status() + .context("Failed response for report post.")?; + + Ok(()) +} diff --git a/bin/core/src/resource/build.rs b/bin/core/src/resource/build.rs index 2319761c7d..9b544ac2b5 100644 --- a/bin/core/src/resource/build.rs +++ b/bin/core/src/resource/build.rs @@ -74,8 +74,9 @@ impl super::KomodoResource for Build { build.config.repo, build.config.branch, build.config.git_https, + String::new(), ); - let (git_provider, repo, branch, git_https) = + let (git_provider, repo, branch, git_https, linked_repo_name) = if build.config.linked_repo.is_empty() { default_git } else { @@ -89,6 +90,7 @@ impl super::KomodoResource for Build { r.config.repo.clone(), r.config.branch.clone(), r.config.git_https, + r.name.clone(), ) }) .unwrap_or(default_git) @@ -107,6 +109,7 @@ impl super::KomodoResource for Build { files_on_host: build.config.files_on_host, dockerfile_contents: !build.config.dockerfile.is_empty(), linked_repo: build.config.linked_repo, + linked_repo_name, repo_link: repo_link( &git_provider, &repo, @@ -297,7 +300,9 @@ async fn validate_config( Ok(()) } -async fn get_build_state(id: &String) -> BuildState { +/// The Build state as computed for the build list items, +/// from the in memory action states / state cache. +pub async fn get_build_state(id: &String) -> BuildState { if action_states() .build .get(id) diff --git a/bin/core/src/resource/builder.rs b/bin/core/src/resource/builder.rs index ebee6399d3..ebc30d5152 100644 --- a/bin/core/src/resource/builder.rs +++ b/bin/core/src/resource/builder.rs @@ -54,7 +54,7 @@ impl super::KomodoResource for Builder { } BuilderConfig::Server(config) => ( BuilderConfigVariant::Server.to_string(), - Some(config.server_id), + Some(config.server_ids.join(", ")), ), BuilderConfig::Aws(config) => ( BuilderConfigVariant::Aws.to_string(), @@ -184,10 +184,11 @@ async fn validate_config( config: &mut PartialBuilderConfig, user: &User, ) -> anyhow::Result<()> { - match config { - PartialBuilderConfig::Server(PartialServerBuilderConfig { - server_id: Some(server_id), - }) if !server_id.is_empty() => { + if let PartialBuilderConfig::Server(PartialServerBuilderConfig { + server_ids: Some(server_ids), + }) = config + { + for server_id in server_ids { let server = super::get_check_permissions::( server_id, user, @@ -196,7 +197,6 @@ async fn validate_config( .await?; *server_id = server.id; } - _ => {} } Ok(()) } diff --git a/bin/core/src/resource/deployment.rs b/bin/core/src/resource/deployment.rs index be0aebeabd..be3cd2facc 100644 --- a/bin/core/src/resource/deployment.rs +++ b/bin/core/src/resource/deployment.rs @@ -1,5 +1,7 @@ -use anyhow::Context; -use database::mungos::mongodb::Collection; +use anyhow::{Context, anyhow}; +use database::mungos::{ + by_id::update_one_by_id, mongodb::Collection, mongodb::bson::doc, +}; use formatting::format_serror; use indexmap::IndexSet; use komodo_client::entities::{ @@ -11,7 +13,7 @@ use komodo_client::entities::{ DeploymentListItemInfo, DeploymentQuerySpecifics, DeploymentState, PartialDeploymentConfig, conversions_from_str, }, - environment_vars_from_str, + environment_vars_from_str, optional_string, permission::{ PermissionLevel, PermissionLevelAndSpecifics, SpecificPermission, }, @@ -23,7 +25,8 @@ use komodo_client::entities::{ user::User, }; use periphery_client::api::{ - container::RemoveContainer, swarm::RemoveSwarmServices, + container::{RemoveContainer, RenameContainer}, + swarm::RemoveSwarmServices, }; use crate::{ @@ -34,7 +37,10 @@ use crate::{ swarm::swarm_request, }, monitor::{refresh_server_cache, refresh_swarm_cache}, - state::{action_states, db_client, deployment_status_cache}, + state::{ + action_states, all_resources_cache, db_client, + deployment_status_cache, + }, }; use super::get_check_permissions; @@ -90,31 +96,31 @@ impl super::KomodoResource for Deployment { deployment: Resource, ) -> Self::ListItem { let status = deployment_status_cache().get(&deployment.id).await; - let state = if action_states() - .deployment - .get(&deployment.id) + let state = get_deployment_state(&deployment.id) .await - .map(|s| s.get().map(|s| s.deploying)) - .transpose() - .ok() - .flatten() - .unwrap_or_default() - { - DeploymentState::Deploying - } else { - status.as_ref().map(|s| s.curr.state).unwrap_or_default() - }; + .unwrap_or_default(); + let all = all_resources_cache().load(); + let server_name = all + .servers + .get(&deployment.config.server_id) + .map(|server| server.name.clone()) + .unwrap_or_default(); + let swarm_name = all + .swarms + .get(&deployment.config.swarm_id) + .map(|swarm| swarm.name.clone()) + .unwrap_or_default(); let (build_image, build_id) = match deployment.config.image { DeploymentImage::Build { build_id, version } => { - let (build_name, build_id, build_version) = - super::get::(&build_id) - .await - .map(|b| (b.name, b.id, b.config.version)) - .unwrap_or(( - String::from("unknown"), - String::new(), - Default::default(), - )); + let (build_name, build_id, build_version) = all + .builds + .get(&build_id) + .map(|b| (b.name.clone(), b.id.clone(), b.config.version)) + .unwrap_or(( + String::from("unknown"), + String::new(), + Default::default(), + )); let version = if version.is_none() { build_version.to_string() } else { @@ -168,10 +174,14 @@ impl super::KomodoResource for Deployment { status: status.as_ref().and_then(|s| { s.curr.container.as_ref().and_then(|c| c.status.to_owned()) }), + custom_name: optional_string(deployment.info.deployed_name) + .unwrap_or(deployment.config.custom_name), image, update_available, swarm_id: deployment.config.swarm_id, + swarm_name, server_id: deployment.config.server_id, + server_name, build_id, }, } @@ -244,11 +254,12 @@ impl super::KomodoResource for Deployment { } async fn validate_update_config( - _id: &str, + id: &str, config: &mut Self::PartialConfig, user: &User, ) -> anyhow::Result<()> { - validate_config(config, user).await + validate_config(config, user).await?; + handle_custom_name_update(id, config).await } async fn post_update( @@ -314,7 +325,7 @@ impl super::KomodoResource for Deployment { SwarmOrServer::Swarm(swarm) => match swarm_request( &swarm.config.server_ids, RemoveSwarmServices { - services: vec![deployment.name.clone()], + services: vec![deployment.deployed_name().to_string()], }, ) .await @@ -352,7 +363,7 @@ impl super::KomodoResource for Deployment { }; match periphery .request(RemoveContainer { - name: deployment.name.clone(), + name: deployment.deployed_name().to_string(), signal: deployment.config.termination_signal.into(), time: deployment.config.termination_timeout.into(), }) @@ -436,12 +447,100 @@ async fn validate_config( environment_vars_from_str(environment) .context("Invalid environment")?; } + if let Some(custom_name) = &config.custom_name + && !custom_name.trim().is_empty() + { + config.custom_name = + Some(to_container_compatible_name(custom_name)); + } if let Some(extra_args) = &mut config.extra_args { extra_args.retain(|v| !empty_or_only_spaces(v)) } Ok(()) } +/// Handles a config update which changes the custom container name: +/// - Server mode: the container is renamed to keep the Deployment +/// matched to it. This runs before the update is written to the +/// database, so a failed rename rejects the update and nothing +/// is left mismatched. +/// - Swarm mode: services cannot be renamed, so the current +/// service name is pinned in info.deployed_name, and stays +/// in use until the next deploy recreates the service. +async fn handle_custom_name_update( + id: &str, + config: &PartialDeploymentConfig, +) -> anyhow::Result<()> { + let Some(custom_name) = &config.custom_name else { + return Ok(()); + }; + let deployment = super::get::(id).await?; + // custom_name is already made container compatible by validate_config. + let new_container_name = if custom_name.is_empty() { + deployment.name.as_str() + } else { + custom_name.as_str() + }; + if deployment.custom_name() == new_container_name { + return Ok(()); + } + if deployment.config.swarm_id.is_empty() + && !deployment.config.server_id.is_empty() + { + let container_state = + get_deployment_state(&deployment.id).await?; + if container_state == DeploymentState::Unknown { + return Err(anyhow!( + "Cannot change custom container name when container status is unknown" + )); + } + if container_state == DeploymentState::NotDeployed { + return Ok(()); + } + let server = + super::get::(&deployment.config.server_id).await?; + periphery_client(&server) + .await? + .request(RenameContainer { + curr_name: deployment.deployed_name().to_string(), + new_name: new_container_name.to_string(), + }) + .await + .context("Failed to rename container to the new custom name")?; + // Keep the tracked deployed name matched to the renamed container. + update_one_by_id( + &db_client().deployments, + &deployment.id, + database::mungos::update::Update::Set( + doc! { "info.deployed_name": new_container_name }, + ), + None, + ) + .await + .context( + "Failed to update deployed name on db after container rename", + )?; + } else if !deployment.config.swarm_id.is_empty() + && deployment.info.deployed_name.is_empty() + // Includes the Unknown state, in case a service + // is still running under the current name. + && get_deployment_state(&deployment.id).await? + != DeploymentState::NotDeployed + { + update_one_by_id( + &db_client().deployments, + &deployment.id, + database::mungos::update::Update::Set( + doc! { "info.deployed_name": deployment.deployed_name() }, + ), + None, + ) + .await + .context("Failed to pin deployed service name on db")?; + } + Ok(()) +} + pub async fn setup_deployment_execution( deployment: &str, user: &User, diff --git a/bin/core/src/resource/mod.rs b/bin/core/src/resource/mod.rs index a8c44ee6f0..b17bcecc9c 100644 --- a/bin/core/src/resource/mod.rs +++ b/bin/core/src/resource/mod.rs @@ -1,5 +1,6 @@ use std::{ collections::{HashMap, HashSet}, + future::Future, str::FromStr, }; @@ -17,7 +18,7 @@ use database::{ }, }; use formatting::format_serror; -use futures_util::future::join_all; +use futures_util::{TryStreamExt, future::join_all}; use indexmap::IndexSet; use komodo_client::{ api::{read::ExportResourcesToToml, write::CreateTag}, @@ -70,7 +71,8 @@ pub use action::{ refresh_action_state_cache, spawn_action_state_refresh_loop, }; pub use build::{ - refresh_build_state_cache, spawn_build_state_refresh_loop, + get_build_state, refresh_build_state_cache, + spawn_build_state_refresh_loop, }; pub use deployment::setup_deployment_execution; pub use procedure::{ @@ -82,7 +84,8 @@ pub use refresh::{ spawn_resource_refresh_loop, }; pub use repo::{ - refresh_repo_state_cache, spawn_repo_state_refresh_loop, + get_repo_state, refresh_repo_state_cache, + spawn_repo_state_refresh_loop, }; pub use server::{rotate_server_keys, update_server_public_key}; @@ -262,59 +265,182 @@ pub async fn get( /// Get full resource list with no permissions check. pub async fn list_all_resources( filters: impl Into>, + limit: impl Into>, + skip: impl Into>, ) -> anyhow::Result>> { - find_collect( - T::coll(), - filters, - FindOptions::builder().sort(doc! { "name": 1 }).build(), - ) - .await - .with_context(|| { - format!("Failed to pull {}s from mongo", T::resource_type()) - }) + let options = FindOptions::builder() + .sort(doc! { "name": 1 }) + .limit(limit) + .skip(skip); + find_collect(T::coll(), filters, options.build()) + .await + .with_context(|| { + format!("Failed to pull {}s from mongo", T::resource_type()) + }) } -pub async fn list_for_user( +pub struct ListItemsQueryOptions { + pub limit: u64, + pub page: u64, + pub sort_desc: bool, + pub sort_by: ListItemSort, +} + +/// List item pagination for the `List` apis, driving the +/// mongo cursor directly instead of collecting the full resource +/// list in memory. Each resource pulled from the cursor is checked +/// against the user permissions and converted to its list item, and +/// `filter` is applied before the item counts toward `limit` / `page`. +/// This is required because some list item fields (eg. state, +/// update available) are computed from in-memory caches rather than +/// stored on the database, so they cannot be part of the db query. +/// Stops pulling from the cursor as soon as the page is full. +pub async fn list_items_for_user( mut query: ResourceQuery, + ListItemsQueryOptions { + limit, + page, + sort_desc, + sort_by, + }: ListItemsQueryOptions, user: &User, permission: PermissionLevelAndSpecifics, all_tags: &[Tag], + filter: impl Fn(&T::ListItem) -> bool, ) -> anyhow::Result> { validate_resource_query_tags(&mut query, all_tags)?; let mut filters = Document::new(); query.add_filters(&mut filters); - list_for_user_using_document::(filters, user, permission).await + + let mut permits = + crate::permission::load_list_permits::(user, permission) + .await?; + + let direction = if sort_desc { -1 } else { 1 }; + let sort = match &sort_by { + ListItemSort::Name => doc! { "name": direction }, + // Db field sorts use ascending name as the secondary sort. + ListItemSort::DbField(field) => { + doc! { *field: direction, "name": 1 } + } + // In-memory sorts pull items in ascending name db order, + // keeping name as the stable tiebreak for equal sort keys. + ListItemSort::InMemory(_) => doc! { "name": 1 }, + }; + + let mut cursor = + T::coll().find(filters).sort(sort).await.with_context(|| { + format!("Failed to query db for {}s", T::resource_type()) + })?; + + let skip = page.saturating_mul(limit) as usize; + let take = if limit == 0 { + usize::MAX + } else { + limit as usize + }; + let mut items = Vec::new(); + + if let ListItemSort::InMemory(compare) = sort_by { + // The sort can only be applied after all matching items + // are collected, so pagination also happens after the sort. + while let Some(resource) = cursor + .try_next() + .await + .context("Failed to pull next resource from db cursor")? + { + if !permits.permitted::(&resource).await? { + continue; + } + let item = T::to_list_item(resource).await; + if !filter(&item) { + continue; + } + items.push(item); + } + // Stable sort keeps equal sort keys in ascending + // name order for both directions. + if sort_desc { + items.sort_by(|a, b| compare(b, a)); + } else { + items.sort_by(|a, b| compare(a, b)); + } + Ok(items.into_iter().skip(skip).take(take).collect()) + } else { + let mut skipped = 0; + while let Some(resource) = cursor + .try_next() + .await + .context("Failed to pull next resource from db cursor")? + { + if !permits.permitted::(&resource).await? { + continue; + } + let item = T::to_list_item(resource).await; + if !filter(&item) { + continue; + } + if skipped < skip { + skipped += 1; + continue; + } + items.push(item); + if take != usize::MAX && items.len() >= take { + break; + } + } + Ok(items) + } +} + +/// How the `List` apis sort the list items. +pub enum ListItemSort { + /// Sort by name at the db level. Default. + Name, + /// Sort on a db stored field, keeping db level sort + /// and streaming pagination. Only usable when the db field + /// exactly matches the displayed list item field. + DbField(&'static str), + /// Compare list items in memory, required for fields computed + /// from the in memory caches (eg. state), or which diverge + /// from the db field (eg. linked repo sources). + /// Collects all matching items before applying pagination. + InMemory(InMemoryListItemSortFn), } -// // pub async fn list_for_user_using_pattern( -// pattern: &str, -// query: ResourceQuery, -// user: &User, -// permissions: PermissionLevelAndSpecifics, -// all_tags: &[Tag], -// ) -> anyhow::Result> { -// let list = list_full_for_user_using_pattern::( -// pattern, -// query, -// user, -// permissions, -// all_tags, -// ) -// .await? -// .into_iter() -// .map(|resource| T::to_list_item(resource)); -// Ok(join_all(list).await) -// } +pub type InMemoryListItemSortFn = + Box std::cmp::Ordering + Send>; + +pub async fn list_for_user( + mut query: ResourceQuery, + limit: impl Into>, + skip: impl Into>, + user: &User, + permission: PermissionLevelAndSpecifics, + all_tags: &[Tag], +) -> anyhow::Result> { + validate_resource_query_tags(&mut query, all_tags)?; + let mut filters = Document::new(); + query.add_filters(&mut filters); + list_for_user_using_document::( + filters, limit, skip, user, permission, + ) + .await +} pub async fn list_for_user_using_document( filters: Document, + limit: impl Into>, + skip: impl Into>, user: &User, permission: PermissionLevelAndSpecifics, ) -> anyhow::Result> { - let list = list_resources_for_user::(filters, user, permission) - .await? - .into_iter() - .map(|resource| T::to_list_item(resource)); + let list = list_resources_for_user::( + filters, limit, skip, user, permission, + ) + .await? + .into_iter() + .map(|resource| T::to_list_item(resource)); Ok(join_all(list).await) } @@ -329,13 +455,16 @@ pub async fn list_for_user_using_document( pub async fn list_full_for_user_using_pattern( pattern: &str, query: ResourceQuery, + limit: impl Into>, + skip: impl Into>, user: &User, permission: PermissionLevelAndSpecifics, all_tags: &[Tag], ) -> anyhow::Result>> { - let resources = - list_full_for_user::(query, user, permission, all_tags) - .await?; + let resources = list_full_for_user::( + query, limit, skip, user, permission, all_tags, + ) + .await?; let patterns = parse_string_list(pattern); let mut names = HashSet::::new(); @@ -368,8 +497,45 @@ pub async fn list_full_for_user_using_pattern( ) } +/// Same as [list_full_for_user], but applies an additional in-memory +/// `filter` (eg. by state computed from the in memory caches), with +/// limit / page applied after the filter. Required because such +/// filters cannot be expressed in the db level query. +/// +/// `filter` receives each resource by value, and returns +/// `Some(resource)` to keep it in the results, or `None` to drop it. +pub async fn list_full_for_user_filtered( + mut query: ResourceQuery, + limit: u64, + page: u64, + user: &User, + permission: PermissionLevelAndSpecifics, + all_tags: &[Tag], + filter: impl Fn(Resource) -> F, +) -> anyhow::Result>> +where + F: Future>> + Send, +{ + validate_resource_query_tags(&mut query, all_tags)?; + let mut filters = Document::new(); + query.add_filters(&mut filters); + let mut permits = + crate::permission::load_list_permits::(user, permission) + .await?; + crate::permission::list_resources_with_permits::( + &mut permits, + Some(filters), + Some(limit as i64), + Some(page.saturating_mul(limit)), + filter, + ) + .await +} + pub async fn list_full_for_user( mut query: ResourceQuery, + limit: impl Into>, + skip: impl Into>, user: &User, permissions: PermissionLevelAndSpecifics, all_tags: &[Tag], @@ -377,7 +543,14 @@ pub async fn list_full_for_user( validate_resource_query_tags(&mut query, all_tags)?; let mut filters = Document::new(); query.add_filters(&mut filters); - list_resources_for_user::(filters, user, permissions).await + list_resources_for_user::( + filters, + limit, + skip, + user, + permissions, + ) + .await } pub type IdResourceMap = HashMap< diff --git a/bin/core/src/resource/procedure.rs b/bin/core/src/resource/procedure.rs index 3ee1f277f3..dd1c831466 100644 --- a/bin/core/src/resource/procedure.rs +++ b/bin/core/src/resource/procedure.rs @@ -272,6 +272,9 @@ async fn validate_config( execute: [ // Action (RunAction, Action, action), + (CancelAction, Action, action), + // Procedure + (CancelProcedure, Procedure, procedure), // Build (RunBuild, Build, build), (CancelBuild, Build, build), diff --git a/bin/core/src/resource/repo.rs b/bin/core/src/resource/repo.rs index 940b16204d..8949730f14 100644 --- a/bin/core/src/resource/repo.rs +++ b/bin/core/src/resource/repo.rs @@ -26,7 +26,8 @@ use crate::{ config::core_config, helpers::{periphery_client, repo_link}, state::{ - action_states, db_client, repo_state_cache, repo_status_cache, + action_states, all_resources_cache, db_client, repo_state_cache, + repo_status_cache, }, }; @@ -63,6 +64,12 @@ impl super::KomodoResource for Repo { let state = get_repo_state(&repo.id).await; let status = repo_status_cache().get(&repo.id).await.unwrap_or_default(); + let server_name = all_resources_cache() + .load() + .servers + .get(&repo.config.server_id) + .map(|server| server.name.clone()) + .unwrap_or_default(); RepoListItem { name: repo.name, id: repo.id, @@ -71,6 +78,7 @@ impl super::KomodoResource for Repo { resource_type: ResourceTargetVariant::Repo, info: RepoListItemInfo { server_id: repo.config.server_id, + server_name, builder_id: repo.config.builder_id, last_pulled_at: repo.info.last_pulled_at, last_built_at: repo.info.last_built_at, @@ -263,7 +271,9 @@ async fn validate_config( Ok(()) } -async fn get_repo_state(id: &String) -> RepoState { +/// The Repo state as computed for the repo list items, +/// from the in memory action states / state cache. +pub async fn get_repo_state(id: &String) -> RepoState { if let Some(state) = action_states() .repo .get(id) diff --git a/bin/core/src/resource/server.rs b/bin/core/src/resource/server.rs index 7b72790c2d..1613a53a94 100644 --- a/bin/core/src/resource/server.rs +++ b/bin/core/src/resource/server.rs @@ -68,14 +68,23 @@ impl super::KomodoResource for Server { server: Resource, ) -> Self::ListItem { let status = server_status_cache().get(&server.id).await; + let (periphery_info, system_info, system_stats) = status + .as_ref() + .map(|s| { + ( + s.periphery_info.as_ref(), + s.system_info.as_ref(), + s.system_stats.as_ref(), + ) + }) + .unwrap_or_default(); let ( version, public_key, public_ip, terminals_disabled, container_terminals_disabled, - ) = match status.as_ref().and_then(|s| s.periphery_info.as_ref()) - { + ) = match periphery_info { Some(info) => ( Some(info.version.clone()), Some(info.public_key.clone()), @@ -94,6 +103,10 @@ impl super::KomodoResource for Server { info: ServerListItemInfo { state: status.as_ref().map(|s| s.state).unwrap_or_default(), err: status.as_ref().and_then(|s| s.err.clone()), + stats: system_stats.map(Into::into), + core_count: system_info.and_then(|i| i.core_count), + logical_core_count: system_info + .and_then(|i| i.logical_core_count), region: server.config.region, address: optional_string(server.config.address), external_address: optional_string( diff --git a/bin/core/src/resource/stack.rs b/bin/core/src/resource/stack.rs index ac7e9f99e7..2f7208fd1a 100644 --- a/bin/core/src/resource/stack.rs +++ b/bin/core/src/resource/stack.rs @@ -31,7 +31,9 @@ use crate::{ config::core_config, helpers::{ periphery_client, - query::{get_stack_state, get_swarm_or_server}, + query::{ + get_cached_stack_state, get_stack_state, get_swarm_or_server, + }, repo_link, swarm::swarm_request, }, @@ -95,20 +97,7 @@ impl super::KomodoResource for Stack { stack: Resource, ) -> Self::ListItem { let status = stack_status_cache().get(&stack.id).await; - let state = if action_states() - .stack - .get(&stack.id) - .await - .map(|s| s.get().map(|s| s.deploying)) - .transpose() - .ok() - .flatten() - .unwrap_or_default() - { - StackState::Deploying - } else { - status.as_ref().map(|s| s.curr.state).unwrap_or_default() - }; + let state = get_cached_stack_state(&stack.id).await; let project_name = stack.project_name(false); let services = status .as_ref() @@ -156,8 +145,9 @@ impl super::KomodoResource for Stack { stack.config.repo, stack.config.branch, stack.config.git_https, + String::new(), ); - let (git_provider, repo, branch, git_https) = + let (git_provider, repo, branch, git_https, linked_repo_name) = if stack.config.linked_repo.is_empty() { default_git } else { @@ -171,6 +161,7 @@ impl super::KomodoResource for Stack { r.config.repo.clone(), r.config.branch.clone(), r.config.git_https, + r.name.clone(), ) }) .unwrap_or(default_git) @@ -205,6 +196,18 @@ impl super::KomodoResource for Stack { (false, None) }; + let all = all_resources_cache().load(); + let server_name = all + .servers + .get(&stack.config.server_id) + .map(|server| server.name.clone()) + .unwrap_or_default(); + let swarm_name = all + .swarms + .get(&stack.config.swarm_id) + .map(|swarm| swarm.name.clone()) + .unwrap_or_default(); + StackListItem { name: stack.name, id: stack.id, @@ -218,8 +221,11 @@ impl super::KomodoResource for Stack { project_missing, file_contents: !stack.config.file_contents.is_empty(), swarm_id: stack.config.swarm_id, + swarm_name, server_id: stack.config.server_id, + server_name, linked_repo: stack.config.linked_repo, + linked_repo_name, missing_files: stack.info.missing_files, files_on_host: stack.config.files_on_host, repo_link: repo_link( diff --git a/bin/core/src/resource/sync.rs b/bin/core/src/resource/sync.rs index bd15125951..8f209e6c59 100644 --- a/bin/core/src/resource/sync.rs +++ b/bin/core/src/resource/sync.rs @@ -57,8 +57,9 @@ impl super::KomodoResource for ResourceSync { resource_sync.config.repo, resource_sync.config.branch, resource_sync.config.git_https, + String::new(), ); - let (git_provider, repo, branch, git_https) = + let (git_provider, repo, branch, git_https, linked_repo_name) = if resource_sync.config.linked_repo.is_empty() { default_git } else { @@ -72,6 +73,7 @@ impl super::KomodoResource for ResourceSync { r.config.repo.clone(), r.config.branch.clone(), r.config.git_https, + r.name.clone(), ) }) .unwrap_or(default_git) @@ -88,6 +90,7 @@ impl super::KomodoResource for ResourceSync { files_on_host: resource_sync.config.files_on_host, managed: resource_sync.config.managed, linked_repo: resource_sync.config.linked_repo, + linked_repo_name, repo_link: repo_link( &git_provider, &repo, diff --git a/bin/core/src/stack/execute.rs b/bin/core/src/stack/execute.rs index 265687aefe..2f48cd665b 100644 --- a/bin/core/src/stack/execute.rs +++ b/bin/core/src/stack/execute.rs @@ -80,7 +80,7 @@ pub async fn execute_compose_with_stack_and_server< // Will check to ensure stack not already busy before updating, and return Err if so. // The returned guard will set the action state back to default when dropped. - let _action_guard = action_state.update(set_in_progress)?; + let action_guard = action_state.update(set_in_progress)?; // Send update here for UI to recheck action state update_update(update.clone()).await?; @@ -105,6 +105,10 @@ pub async fn execute_compose_with_stack_and_server< refresh_server_cache(&server, true).await; update.finalize(); + + // Drop action guard before updating + // clients to requery action state + drop(action_guard); update_update(update.clone()).await?; Ok(update) diff --git a/bin/core/src/startup.rs b/bin/core/src/startup.rs index 9c778f0fea..530bcd2fcb 100644 --- a/bin/core/src/startup.rs +++ b/bin/core/src/startup.rs @@ -281,7 +281,7 @@ async fn ensure_first_server_and_builder() { name: name.to_string(), config: PartialBuilderConfig::Server( PartialServerBuilderConfig { - server_id: Some(server.id), + server_ids: Some(vec![server.id]), }, ), }) diff --git a/bin/core/src/state.rs b/bin/core/src/state.rs index 9c2fd47467..d413c99ee0 100644 --- a/bin/core/src/state.rs +++ b/bin/core/src/state.rs @@ -19,13 +19,14 @@ use komodo_client::entities::{ swarm::SwarmState, }; use mogh_cache::CloneCache; +use tokio_util::sync::CancellationToken; use crate::{ config::core_config, connection::PeripheryConnections, helpers::{ action_state::ActionStates, all_resources::AllResourcesById, - image_digest::ImageDigestCache, + builder::BuilderUsage, image_digest::ImageDigestCache, }, }; @@ -208,6 +209,7 @@ pub fn action_state_cache() -> &'static ActionStateCache { ACTION_STATE_CACHE.get_or_init(Default::default) } +/// Store all resources in local cache for fast lookup pub fn all_resources_cache() -> &'static ArcSwap { static ALL_RESOURCES: OnceLock> = OnceLock::new(); @@ -222,3 +224,30 @@ pub fn image_digest_cache() -> &'static ImageDigestCache { OnceLock::new(); IMAGE_DIGEST_CACHE.get_or_init(ImageDigestCache::new) } + +/// Maps Builder id => downstream count map (eg server id => active count) +type BuilderUsageCache = CloneCache>; + +/// For builders with multiple downstream machines to choose. +/// Stores active build count for each downstream. +/// Maps Builder id => downstream count map +pub fn builder_usage_cache() -> &'static BuilderUsageCache { + static BUILDER_USAGE_CACHE: OnceLock = + OnceLock::new(); + BUILDER_USAGE_CACHE.get_or_init(Default::default) +} + +type CancelCache = CloneCache; + +/// Maps procedure id => CancellationToken +pub fn procedure_cancel_cache() -> &'static CancelCache { + static PROCEDURE_CANCEL_CACHE: OnceLock = + OnceLock::new(); + PROCEDURE_CANCEL_CACHE.get_or_init(Default::default) +} + +/// Maps update id => CancellationToken +pub fn action_cancel_cache() -> &'static CancelCache { + static ACTION_CANCEL_CACHE: OnceLock = OnceLock::new(); + ACTION_CANCEL_CACHE.get_or_init(Default::default) +} diff --git a/bin/core/src/sync/deploy.rs b/bin/core/src/sync/deploy.rs index 3eec59b3e3..7b6fb3eb03 100644 --- a/bin/core/src/sync/deploy.rs +++ b/bin/core/src/sync/deploy.rs @@ -338,9 +338,7 @@ fn build_cache_for_deployment<'a>( // Makes sure things that aren't defined in toml (come through as None) actually get removed. let config: DeploymentConfig = deployment.config.clone().into(); - let mut config: PartialDeploymentConfig = config.into(); - - Deployment::validate_partial_config(&mut config); + let config: PartialDeploymentConfig = config.into(); let mut diff = Deployment::get_diff(original.config.clone(), config)?; @@ -572,9 +570,7 @@ fn build_cache_for_stack<'a>( // Merge toml resource config (partial) onto default resource config. // Makes sure things that aren't defined in toml (come through as None) actually get removed. let config: StackConfig = stack.config.clone().into(); - let mut config: PartialStackConfig = config.into(); - - Stack::validate_partial_config(&mut config); + let config: PartialStackConfig = config.into(); let mut diff = Stack::get_diff(original.config.clone(), config)?; diff --git a/bin/core/src/sync/execute.rs b/bin/core/src/sync/execute.rs index 4f9e527180..1b09bf5535 100644 --- a/bin/core/src/sync/execute.rs +++ b/bin/core/src/sync/execute.rs @@ -74,7 +74,7 @@ pub async fn get_updates_for_execution< let config: Resource::Config = resource.config.into(); resource.config = config.into(); - Resource::validate_partial_config(&mut resource.config); + // Resource::validate_partial_config(&mut resource.config); let mut diff = Resource::get_diff( original.config.clone(), diff --git a/bin/core/src/sync/mod.rs b/bin/core/src/sync/mod.rs index fb59471ffa..978558165d 100644 --- a/bin/core/src/sync/mod.rs +++ b/bin/core/src/sync/mod.rs @@ -7,6 +7,7 @@ use komodo_client::entities::{ tag::Tag, toml::{ResourceToml, ResourcesToml}, }; +use partial_derive2::PartialDiff as _; use toml::ToToml; use crate::resource::KomodoResource; @@ -15,6 +16,7 @@ pub mod deploy; pub mod execute; pub mod file; pub mod remote; +pub mod replace_ids; pub mod resources; pub mod toml; pub mod user_groups; @@ -87,16 +89,19 @@ pub trait ResourceSyncTrait: ToToml + Sized { ) } - /// Apply any changes to incoming toml partial config - /// before it is diffed against existing config - fn validate_partial_config(_config: &mut Self::PartialConfig) {} + // /// Apply any changes to incoming toml partial config + // /// before it is diffed against existing config + // fn validate_partial_config(_config: &mut Self::PartialConfig) {} /// Diffs the declared toml (partial) against the full existing config. /// Removes all fields from toml (partial) that haven't changed. fn get_diff( - original: Self::Config, + mut original: Self::Config, update: Self::PartialConfig, - ) -> anyhow::Result; + ) -> anyhow::Result { + Self::replace_ids(&mut original); + Ok(original.partial_diff(update)) + } /// Apply any changes to computed config diff /// before logging diff --git a/bin/core/src/sync/replace_ids.rs b/bin/core/src/sync/replace_ids.rs new file mode 100644 index 0000000000..c390896040 --- /dev/null +++ b/bin/core/src/sync/replace_ids.rs @@ -0,0 +1,252 @@ +use komodo_client::entities::{ + action::Action, + alerter::Alerter, + build::Build, + builder::{Builder, BuilderConfig}, + deployment::{Deployment, DeploymentImage}, + procedure::Procedure, + repo::Repo, + server::Server, + stack::Stack, + swarm::Swarm, + sync::ResourceSync, +}; + +use crate::{ + helpers::procedure::replace_procedure_stage_ids_with_names, + resource::KomodoResource, state::all_resources_cache, +}; + +/// Trait implementing replacing of linked resource ids with names for Resource Sync TOML. +pub trait ReplaceIds: KomodoResource { + /// Replace linked ids (server_id, build_id, etc) with the resource name. + fn replace_ids(_config: &mut Self::Config) {} +} + +// These have no linked resource ids to replace +impl ReplaceIds for Server {} +impl ReplaceIds for Action {} + +impl ReplaceIds for ResourceSync { + fn replace_ids(config: &mut Self::Config) { + let all = all_resources_cache().load(); + config.linked_repo.clone_from( + all + .repos + .get(&config.linked_repo) + .map(|r| &r.name) + .unwrap_or(&String::new()), + ); + } +} + +impl ReplaceIds for Swarm { + fn replace_ids(config: &mut Self::Config) { + let all = all_resources_cache().load(); + + config.server_ids.iter_mut().for_each(|server_id| { + *server_id = all + .servers + .get(server_id) + .map(|s| s.name.clone()) + .unwrap_or_default(); + }); + let mut res = Vec::with_capacity(config.server_ids.capacity()); + for server_id in &config.server_ids { + res.push( + all + .servers + .get(server_id) + .map(|s| s.name.clone()) + .unwrap_or_default(), + ); + } + } +} + +impl ReplaceIds for Stack { + fn replace_ids(config: &mut Self::Config) { + let all = all_resources_cache().load(); + + config.swarm_id.clone_from( + all + .swarms + .get(&config.swarm_id) + .map(|s| &s.name) + .unwrap_or(&String::new()), + ); + + config.server_id.clone_from( + all + .servers + .get(&config.server_id) + .map(|s| &s.name) + .unwrap_or(&String::new()), + ); + + config.linked_repo.clone_from( + all + .repos + .get(&config.linked_repo) + .map(|r| &r.name) + .unwrap_or(&String::new()), + ); + } +} + +impl ReplaceIds for Deployment { + fn replace_ids(config: &mut Self::Config) { + let all = all_resources_cache().load(); + + config.swarm_id.clone_from( + all + .swarms + .get(&config.swarm_id) + .map(|s| &s.name) + .unwrap_or(&String::new()), + ); + + config.server_id.clone_from( + all + .servers + .get(&config.server_id) + .map(|s| &s.name) + .unwrap_or(&String::new()), + ); + + if let DeploymentImage::Build { build_id, .. } = &mut config.image + { + build_id.clone_from( + all + .builds + .get(build_id) + .map(|b| &b.name) + .unwrap_or(&String::new()), + ); + } + } +} + +impl ReplaceIds for Build { + fn replace_ids(config: &mut Self::Config) { + let all = all_resources_cache().load(); + config.builder_id.clone_from( + all + .builders + .get(&config.builder_id) + .map(|s| &s.name) + .unwrap_or(&String::new()), + ); + config.linked_repo.clone_from( + all + .repos + .get(&config.linked_repo) + .map(|r| &r.name) + .unwrap_or(&String::new()), + ); + } +} + +impl ReplaceIds for Repo { + fn replace_ids(config: &mut Self::Config) { + let all = all_resources_cache().load(); + config.server_id.clone_from( + all + .servers + .get(&config.server_id) + .map(|s| &s.name) + .unwrap_or(&String::new()), + ); + config.builder_id.clone_from( + all + .builders + .get(&config.builder_id) + .map(|s| &s.name) + .unwrap_or(&String::new()), + ); + } +} + +impl ReplaceIds for Builder { + fn replace_ids(config: &mut Self::Config) { + if let BuilderConfig::Server(config) = config { + let all = all_resources_cache().load(); + for server_id in &mut config.server_ids { + *server_id = all + .servers + .get(server_id) + .map(|s| s.name.clone()) + .unwrap_or_default(); + } + } + } +} + +impl ReplaceIds for Procedure { + fn replace_ids(config: &mut Self::Config) { + replace_procedure_stage_ids_with_names(&mut config.stages); + } +} + +/// Replaces the inner ids of [ResourceTarget] variants with the +/// referenced resource's name, looking each up in [AllResourcesById]. +/// The `System` variant carries no id and is a no-op. +macro_rules! replace_resource_target_ids { + ($target:expr, $all:expr, { $( $variant:ident => $field:ident ),* $(,)? }) => { + match $target { + komodo_client::entities::ResourceTarget::System(_) => {} + $( + komodo_client::entities::ResourceTarget::$variant(id) => { + *id = $all + .$field + .get(id) + .map(|r| r.name.clone()) + .unwrap_or_default(); + } + )* + } + }; +} + +impl ReplaceIds for Alerter { + fn replace_ids(config: &mut Self::Config) { + if config.resources.is_empty() + && config.except_resources.is_empty() + { + return; + } + + let all = all_resources_cache().load(); + + for resource in &mut config.resources { + replace_resource_target_ids!(resource, all, { + Swarm => swarms, + Server => servers, + Stack => stacks, + Deployment => deployments, + Build => builds, + Repo => repos, + Procedure => procedures, + Action => actions, + Builder => builders, + Alerter => alerters, + ResourceSync => syncs, + }); + } + for resource in &mut config.except_resources { + replace_resource_target_ids!(resource, all, { + Swarm => swarms, + Server => servers, + Stack => stacks, + Deployment => deployments, + Build => builds, + Repo => repos, + Procedure => procedures, + Action => actions, + Builder => builders, + Alerter => alerters, + ResourceSync => syncs, + }); + } + } +} diff --git a/bin/core/src/sync/resources.rs b/bin/core/src/sync/resources.rs index ddba0cf0f5..ee3b7f2471 100644 --- a/bin/core/src/sync/resources.rs +++ b/bin/core/src/sync/resources.rs @@ -2,28 +2,16 @@ use std::collections::HashMap; use formatting::{Color, bold, colored, muted}; use komodo_client::entities::{ - ResourceTargetVariant, - action::Action, - alerter::Alerter, - build::Build, - builder::{Builder, BuilderConfig}, - deployment::{Deployment, DeploymentImage}, - procedure::Procedure, - repo::Repo, - server::Server, - stack::Stack, - swarm::Swarm, - sync::ResourceSync, - tag::Tag, - update::Log, + ResourceTargetVariant, action::Action, alerter::Alerter, + build::Build, builder::Builder, deployment::Deployment, + procedure::Procedure, repo::Repo, server::Server, stack::Stack, + swarm::Swarm, sync::ResourceSync, tag::Tag, update::Log, user::sync_user, }; -use partial_derive2::{MaybeNone, PartialDiff}; +use partial_derive2::MaybeNone; use crate::{ - helpers::procedure::replace_procedure_stage_ids_with_names, resource::{KomodoResource, ResourceMetaUpdate}, - state::all_resources_cache, sync::{ToUpdateItem, execute::run_update_meta}, }; @@ -33,127 +21,19 @@ use super::{ include_resource_by_tags, }; -impl ResourceSyncTrait for Server { - fn get_diff( - original: Self::Config, - update: Self::PartialConfig, - ) -> anyhow::Result { - Ok(original.partial_diff(update)) - } -} - +impl ResourceSyncTrait for Server {} impl ExecuteResourceSync for Server {} -impl ResourceSyncTrait for Swarm { - fn get_diff( - mut original: Self::Config, - update: Self::PartialConfig, - ) -> anyhow::Result { - let all = all_resources_cache().load(); - - original.server_ids.iter_mut().for_each(|server_id| { - *server_id = all - .servers - .get(server_id) - .map(|s| s.name.clone()) - .unwrap_or_default(); - }); - - Ok(original.partial_diff(update)) - } -} - +impl ResourceSyncTrait for Swarm {} impl ExecuteResourceSync for Swarm {} -impl ResourceSyncTrait for Deployment { - fn get_diff( - mut original: Self::Config, - update: Self::PartialConfig, - ) -> anyhow::Result { - let all = all_resources_cache().load(); - - original.swarm_id = all - .swarms - .get(&original.swarm_id) - .map(|s| s.name.clone()) - .unwrap_or_default(); - - original.server_id = all - .servers - .get(&original.server_id) - .map(|s| s.name.clone()) - .unwrap_or_default(); - - if let DeploymentImage::Build { build_id, version } = - &original.image - { - original.image = DeploymentImage::Build { - build_id: all - .builds - .get(build_id) - .map(|b| b.name.clone()) - .unwrap_or_default(), - version: *version, - }; - } - - Ok(original.partial_diff(update)) - } -} - +impl ResourceSyncTrait for Deployment {} impl ExecuteResourceSync for Deployment {} -impl ResourceSyncTrait for Stack { - fn get_diff( - mut original: Self::Config, - update: Self::PartialConfig, - ) -> anyhow::Result { - let all = all_resources_cache().load(); - - original.swarm_id = all - .swarms - .get(&original.swarm_id) - .map(|s| s.name.clone()) - .unwrap_or_default(); - - original.server_id = all - .servers - .get(&original.server_id) - .map(|s| s.name.clone()) - .unwrap_or_default(); - - original.linked_repo = all - .repos - .get(&original.linked_repo) - .map(|r| r.name.clone()) - .unwrap_or_default(); - - Ok(original.partial_diff(update)) - } -} - +impl ResourceSyncTrait for Stack {} impl ExecuteResourceSync for Stack {} impl ResourceSyncTrait for Build { - fn get_diff( - mut original: Self::Config, - update: Self::PartialConfig, - ) -> anyhow::Result { - let all = all_resources_cache().load(); - original.builder_id = all - .builders - .get(&original.builder_id) - .map(|b| b.name.clone()) - .unwrap_or_default(); - original.linked_repo = all - .repos - .get(&original.linked_repo) - .map(|r| r.name.clone()) - .unwrap_or_default(); - - Ok(original.partial_diff(update)) - } - fn validate_diff(diff: &mut Self::ConfigDiff) { if let Some((_, to)) = &diff.version { // When setting a build back to "latest" version, @@ -165,76 +45,18 @@ impl ResourceSyncTrait for Build { } } } - impl ExecuteResourceSync for Build {} -impl ResourceSyncTrait for Repo { - fn get_diff( - mut original: Self::Config, - update: Self::PartialConfig, - ) -> anyhow::Result { - let all = all_resources_cache().load(); - // Need to replace server id with name - original.server_id = all - .servers - .get(&original.server_id) - .map(|s| s.name.clone()) - .unwrap_or_default(); - - // Need to replace builder id with name - original.builder_id = all - .builders - .get(&original.builder_id) - .map(|s| s.name.clone()) - .unwrap_or_default(); - - Ok(original.partial_diff(update)) - } -} - +impl ResourceSyncTrait for Repo {} impl ExecuteResourceSync for Repo {} -impl ResourceSyncTrait for Alerter { - fn get_diff( - original: Self::Config, - update: Self::PartialConfig, - ) -> anyhow::Result { - Ok(original.partial_diff(update)) - } -} - +impl ResourceSyncTrait for Alerter {} impl ExecuteResourceSync for Alerter {} -impl ResourceSyncTrait for Builder { - fn get_diff( - mut original: Self::Config, - update: Self::PartialConfig, - ) -> anyhow::Result { - // need to replace server builder id with name - if let BuilderConfig::Server(config) = &mut original { - let all = all_resources_cache().load(); - config.server_id = all - .servers - .get(&config.server_id) - .map(|s| s.name.clone()) - .unwrap_or_default(); - } - - Ok(original.partial_diff(update)) - } -} - +impl ResourceSyncTrait for Builder {} impl ExecuteResourceSync for Builder {} -impl ResourceSyncTrait for Action { - fn get_diff( - original: Self::Config, - update: Self::PartialConfig, - ) -> anyhow::Result { - Ok(original.partial_diff(update)) - } -} - +impl ResourceSyncTrait for Action {} impl ExecuteResourceSync for Action {} impl ResourceSyncTrait for ResourceSync { @@ -316,33 +138,11 @@ impl ResourceSyncTrait for ResourceSync { // The sync must be files on host mode OR git repo mode (files_on_host || !config.repo.as_deref().unwrap_or_default().is_empty() || !config.linked_repo.as_deref().unwrap_or_default().is_empty()) } - - fn get_diff( - mut original: Self::Config, - update: Self::PartialConfig, - ) -> anyhow::Result { - let all = all_resources_cache().load(); - original.linked_repo = all - .repos - .get(&original.linked_repo) - .map(|r| r.name.clone()) - .unwrap_or_default(); - - Ok(original.partial_diff(update)) - } } impl ExecuteResourceSync for ResourceSync {} -impl ResourceSyncTrait for Procedure { - fn get_diff( - mut original: Self::Config, - update: Self::PartialConfig, - ) -> anyhow::Result { - replace_procedure_stage_ids_with_names(&mut original.stages); - Ok(original.partial_diff(update)) - } -} +impl ResourceSyncTrait for Procedure {} impl ExecuteResourceSync for Procedure { async fn execute_sync_updates( diff --git a/bin/core/src/sync/toml.rs b/bin/core/src/sync/toml.rs index 1b0eb50254..45c9bc1ebf 100644 --- a/bin/core/src/sync/toml.rs +++ b/bin/core/src/sync/toml.rs @@ -6,7 +6,7 @@ use komodo_client::entities::{ action::Action, alerter::Alerter, build::Build, - builder::{Builder, BuilderConfig, PartialBuilderConfig}, + builder::{Builder, PartialBuilderConfig}, deployment::{Deployment, DeploymentImage}, procedure::Procedure, repo::Repo, @@ -21,8 +21,7 @@ use komodo_client::entities::{ use partial_derive2::{MaybeNone, PartialDiff}; use crate::{ - helpers::procedure::replace_procedure_stage_ids_with_names, - resource::KomodoResource, state::all_resources_cache, + resource::KomodoResource, sync::replace_ids::ReplaceIds, }; pub const TOML_PRETTY_OPTIONS: toml_pretty::Options = @@ -35,11 +34,7 @@ pub const TOML_PRETTY_OPTIONS: toml_pretty::Options = inline_array: false, }; -pub trait ToToml: KomodoResource { - /// Replace linked ids (server_id, build_id, etc) with the resource name. - fn replace_ids(_resource: &mut Resource) { - } - +pub trait ToToml: ReplaceIds { fn edit_config_object( _resource: &ResourceToml, config: IndexMap, @@ -108,7 +103,7 @@ pub fn resource_push_to_toml( toml: &mut String, all_tags: &HashMap, ) -> anyhow::Result<()> { - R::replace_ids(&mut resource); + R::replace_ids(&mut resource.config); if !toml.is_empty() { toml.push_str("\n\n##\n\n"); } @@ -157,48 +152,12 @@ pub fn convert_resource( } } -// These have no linked resource ids to replace -impl ToToml for Alerter {} impl ToToml for Server {} impl ToToml for Action {} - -impl ToToml for ResourceSync { - fn replace_ids(resource: &mut Resource) { - let all = all_resources_cache().load(); - resource.config.linked_repo.clone_from( - all - .repos - .get(&resource.config.linked_repo) - .map(|r| &r.name) - .unwrap_or(&String::new()), - ); - } -} +impl ToToml for Alerter {} +impl ToToml for ResourceSync {} impl ToToml for Swarm { - fn replace_ids(resource: &mut Resource) { - let all = all_resources_cache().load(); - - resource.config.server_ids.iter_mut().for_each(|server_id| { - *server_id = all - .servers - .get(server_id) - .map(|s| s.name.clone()) - .unwrap_or_default(); - }); - let mut res = - Vec::with_capacity(resource.config.server_ids.capacity()); - for server_id in &resource.config.server_ids { - res.push( - all - .servers - .get(server_id) - .map(|s| s.name.clone()) - .unwrap_or_default(), - ); - } - } - fn edit_config_object( _resource: &ResourceToml, config: IndexMap, @@ -220,34 +179,6 @@ impl ToToml for Swarm { } impl ToToml for Stack { - fn replace_ids(resource: &mut Resource) { - let all = all_resources_cache().load(); - - resource.config.swarm_id.clone_from( - all - .swarms - .get(&resource.config.swarm_id) - .map(|s| &s.name) - .unwrap_or(&String::new()), - ); - - resource.config.server_id.clone_from( - all - .servers - .get(&resource.config.server_id) - .map(|s| &s.name) - .unwrap_or(&String::new()), - ); - - resource.config.linked_repo.clone_from( - all - .repos - .get(&resource.config.linked_repo) - .map(|r| &r.name) - .unwrap_or(&String::new()), - ); - } - fn edit_config_object( _resource: &ResourceToml, config: IndexMap, @@ -268,38 +199,6 @@ impl ToToml for Stack { } impl ToToml for Deployment { - fn replace_ids(resource: &mut Resource) { - let all = all_resources_cache().load(); - - resource.config.swarm_id.clone_from( - all - .swarms - .get(&resource.config.swarm_id) - .map(|s| &s.name) - .unwrap_or(&String::new()), - ); - - resource.config.server_id.clone_from( - all - .servers - .get(&resource.config.server_id) - .map(|s| &s.name) - .unwrap_or(&String::new()), - ); - - if let DeploymentImage::Build { build_id, .. } = - &mut resource.config.image - { - build_id.clone_from( - all - .builds - .get(build_id) - .map(|b| &b.name) - .unwrap_or(&String::new()), - ); - } - } - fn edit_config_object( resource: &ResourceToml, config: IndexMap, @@ -341,24 +240,6 @@ impl ToToml for Deployment { } impl ToToml for Build { - fn replace_ids(resource: &mut Resource) { - let all = all_resources_cache().load(); - resource.config.builder_id.clone_from( - all - .builders - .get(&resource.config.builder_id) - .map(|s| &s.name) - .unwrap_or(&String::new()), - ); - resource.config.linked_repo.clone_from( - all - .repos - .get(&resource.config.linked_repo) - .map(|r| &r.name) - .unwrap_or(&String::new()), - ); - } - fn edit_config_object( resource: &ResourceToml, config: IndexMap, @@ -391,24 +272,6 @@ impl ToToml for Build { } impl ToToml for Repo { - fn replace_ids(resource: &mut Resource) { - let all = all_resources_cache().load(); - resource.config.server_id.clone_from( - all - .servers - .get(&resource.config.server_id) - .map(|s| &s.name) - .unwrap_or(&String::new()), - ); - resource.config.builder_id.clone_from( - all - .builders - .get(&resource.config.builder_id) - .map(|s| &s.name) - .unwrap_or(&String::new()), - ); - } - fn edit_config_object( _resource: &ResourceToml, config: IndexMap, @@ -430,19 +293,6 @@ impl ToToml for Repo { } impl ToToml for Builder { - fn replace_ids(resource: &mut Resource) { - if let BuilderConfig::Server(config) = &mut resource.config { - let all = all_resources_cache().load(); - config.server_id.clone_from( - all - .servers - .get(&config.server_id) - .map(|s| &s.name) - .unwrap_or(&String::new()), - ) - } - } - fn push_additional( resource: ResourceToml, toml: &mut String, @@ -476,8 +326,8 @@ impl ToToml for Builder { .into_iter() .map(|(key, value)| { match key.as_str() { - "server_id" => { - return (String::from("server"), value); + "server_ids" => { + return (String::from("servers"), value); } _ => {} } @@ -496,12 +346,6 @@ impl ToToml for Builder { } impl ToToml for Procedure { - fn replace_ids(resource: &mut Resource) { - replace_procedure_stage_ids_with_names( - &mut resource.config.stages, - ); - } - fn push_to_toml_string( mut resource: ResourceToml, toml: &mut String, diff --git a/bin/core/src/sync/view.rs b/bin/core/src/sync/view.rs index be6f59c252..d6dcea8e50 100644 --- a/bin/core/src/sync/view.rs +++ b/bin/core/src/sync/view.rs @@ -84,9 +84,9 @@ pub async fn push_updates_for_view( proposed_resource.config.into(); proposed_resource.config = propsed_config.into(); - Resource::validate_partial_config( - &mut proposed_resource.config, - ); + // Resource::validate_partial_config( + // &mut proposed_resource.config, + // ); let proposed = super::toml::resource_toml_to_toml_string::< Resource, diff --git a/bin/periphery/Cargo.toml b/bin/periphery/Cargo.toml index f814f2d3ea..586db85359 100644 --- a/bin/periphery/Cargo.toml +++ b/bin/periphery/Cargo.toml @@ -2,7 +2,6 @@ name = "komodo_periphery" version.workspace = true edition.workspace = true -authors.workspace = true license.workspace = true homepage.workspace = true repository.workspace = true @@ -34,6 +33,7 @@ git.workspace = true mogh_error = { workspace = true, features = ["axum"] } async_timing_util.workspace = true mogh_resolver.workspace = true +svi.workspace = true # external hickory-resolver.workspace = true serde_yaml_ng.workspace = true diff --git a/bin/periphery/aio.Dockerfile b/bin/periphery/aio.Dockerfile index affb4d89ee..0e045fa8da 100644 --- a/bin/periphery/aio.Dockerfile +++ b/bin/periphery/aio.Dockerfile @@ -1,6 +1,6 @@ ## All in one, multi stage compile + runtime Docker build for your architecture. -FROM rust:1.95.0-trixie AS builder +FROM rust:1.97.1-trixie AS builder RUN cargo install cargo-strip WORKDIR /builder diff --git a/bin/periphery/src/api/build/mod.rs b/bin/periphery/src/api/build/mod.rs index b9fb37c22c..6bba92b612 100644 --- a/bin/periphery/src/api/build/mod.rs +++ b/bin/periphery/src/api/build/mod.rs @@ -5,13 +5,13 @@ use std::{ use anyhow::{Context, anyhow}; use command::{ - KomodoCommandMode, run_komodo_command_with_sanitization, - run_komodo_standard_command, + CommandOptions, KomodoCommandMode, + run_komodo_command_with_sanitization, run_komodo_standard_command, }; use formatting::format_serror; use interpolate::Interpolator; use komodo_client::entities::{ - EnvironmentVar, all_logs_success, + EnvironmentVar, NoData, all_logs_success, build::{Build, BuildConfig}, environment_vars_from_str, optional_string, to_path_compatible_name, @@ -19,17 +19,19 @@ use komodo_client::entities::{ }; use mogh_resolver::Resolve; use periphery_client::api::build::{ - self, GetDockerfileContentsOnHost, + self, CancelBuild, GetDockerfileContentsOnHost, GetDockerfileContentsOnHostResponse, PruneBuilders, PruneBuildx, WriteDockerfileContentsToHost, }; use tokio::fs; +use tokio_util::sync::CancellationToken; use tracing::Instrument; use crate::{ config::periphery_config, docker::docker_login, helpers::{format_extra_args, format_labels}, + state::build_cancel_cache, }; mod helpers; @@ -133,7 +135,8 @@ impl Resolve for build::Build { fields( id = args.id.to_string(), core = args.core, - build = self.build.name, + build_id = self.build.id, + build_name = self.build.name, repo = self.repo.as_ref().map(|repo| &repo.name), ) )] @@ -263,14 +266,21 @@ impl Resolve for build::Build { } }; + let cancel = CancellationToken::new(); + build_cancel_cache() + .insert(build.id.clone(), cancel.clone()) + .await; + // Pre Build if !pre_build.is_none() { let pre_build_path = build_path.join(&pre_build.path); let span = info_span!("RunPreBuild"); if let Some(log) = run_komodo_command_with_sanitization( "Pre Build", - pre_build_path.as_path(), &pre_build.command, + CommandOptions::default() + .path(pre_build_path.as_path()) + .cancel(cancel.clone()), if pre_build.shell_mode { KomodoCommandMode::Shell } else { @@ -284,53 +294,73 @@ impl Resolve for build::Build { let success = log.success; logs.push(log); if !success { + build_cancel_cache().remove(&build.id).await; return Ok(logs); } } } // Get command parts + // Do this in fallible block to ensure + // cancel token removed from cache on failure. + let command = async { + // Add VERSION to build args (if not already there) + let mut build_args = environment_vars_from_str(build_args) + .context("Invalid build_args")?; + if !build_args.iter().any(|a| a.variable == "VERSION") { + build_args.push(EnvironmentVar { + variable: String::from("VERSION"), + value: build.config.version.to_string(), + }); + } + let build_args = parse_build_args(&build_args); - // Add VERSION to build args (if not already there) - let mut build_args = environment_vars_from_str(build_args) - .context("Invalid build_args")?; - if !build_args.iter().any(|a| a.variable == "VERSION") { - build_args.push(EnvironmentVar { - variable: String::from("VERSION"), - value: build.config.version.to_string(), - }); - } - let build_args = parse_build_args(&build_args); + let secret_args = environment_vars_from_str(secret_args) + .context("Invalid secret_args")?; + let command_secret_args = + parse_secret_args(&secret_args, &build_path).await?; - let secret_args = environment_vars_from_str(secret_args) - .context("Invalid secret_args")?; - let command_secret_args = - parse_secret_args(&secret_args, &build_path).await?; + let labels = format_labels( + &environment_vars_from_str(labels) + .context("Invalid labels")?, + ); - let labels = format_labels( - &environment_vars_from_str(labels).context("Invalid labels")?, - ); + let extra_args = format_extra_args(extra_args); - let extra_args = format_extra_args(extra_args); + let buildx = if *use_buildx { " buildx" } else { "" }; - let buildx = if *use_buildx { " buildx" } else { "" }; + let image_tags = build + .get_image_tags_as_arg( + commit_hash.as_deref(), + &additional_tags, + ) + .context("Failed to parse image tags into command")?; + + let maybe_push = if should_push { " --push" } else { "" }; - let image_tags = build - .get_image_tags_as_arg(commit_hash.as_deref(), &additional_tags) - .context("Failed to parse image tags into command")?; + // Construct command + let command = format!( + "docker{buildx} build{build_args}{command_secret_args}{extra_args}{labels}{image_tags}{maybe_push} -f {dockerfile_path} .", + ); - let maybe_push = if should_push { " --push" } else { "" }; + anyhow::Ok(command) + }.await; - // Construct command - let command = format!( - "docker{buildx} build{build_args}{command_secret_args}{extra_args}{labels}{image_tags}{maybe_push} -f {dockerfile_path} .", - ); + let command = match command { + Ok(command) => command, + Err(e) => { + build_cancel_cache().remove(&build.id).await; + return Err(e); + } + }; let span = info_span!("RunDockerBuild"); if let Some(build_log) = run_komodo_command_with_sanitization( "Docker Build", - build_path.as_ref(), command, + CommandOptions::default() + .path(build_path.as_path()) + .cancel(cancel), KomodoCommandMode::Shell, &replacers, ) @@ -340,12 +370,39 @@ impl Resolve for build::Build { logs.push(build_log); }; + build_cancel_cache().remove(&build.id).await; + Ok(logs) } } // +impl Resolve for CancelBuild { + #[instrument( + "CancelBuild", + skip_all, + fields( + build_id = self.id, + id = args.id.to_string(), + core = args.core, + ) + )] + async fn resolve( + self, + args: &crate::api::Args, + ) -> anyhow::Result { + build_cancel_cache() + .get(&self.id) + .await + .context("No running build found")? + .cancel(); + Ok(NoData {}) + } +} + +// + impl Resolve for PruneBuilders { #[instrument( "PruneBuilders", @@ -361,8 +418,12 @@ impl Resolve for PruneBuilders { ) -> anyhow::Result { let command = String::from("docker builder prune -a -f"); Ok( - run_komodo_standard_command("Prune Builders", None, command) - .await, + run_komodo_standard_command( + "Prune Builders", + command, + CommandOptions::default(), + ) + .await, ) } } @@ -384,8 +445,12 @@ impl Resolve for PruneBuildx { ) -> anyhow::Result { let command = String::from("docker buildx prune -a -f"); Ok( - run_komodo_standard_command("Prune Buildx", None, command) - .await, + run_komodo_standard_command( + "Prune Buildx", + command, + CommandOptions::default(), + ) + .await, ) } } diff --git a/bin/periphery/src/api/compose.rs b/bin/periphery/src/api/compose.rs index d53e5016cc..15ae236052 100644 --- a/bin/periphery/src/api/compose.rs +++ b/bin/periphery/src/api/compose.rs @@ -1,9 +1,13 @@ -use std::{borrow::Cow, fmt::Write, path::PathBuf}; +use std::{ + borrow::Cow, fmt::Write, path::PathBuf, sync::LazyLock, + time::Duration, +}; use anyhow::{Context, anyhow}; use command::{ - KomodoCommandMode, run_komodo_command_with_sanitization, - run_komodo_shell_command, run_komodo_standard_command, + CommandOptions, KomodoCommandMode, + run_komodo_command_with_sanitization, run_komodo_shell_command, + run_komodo_standard_command, }; use formatting::format_serror; use git::write_commit_file; @@ -11,11 +15,7 @@ use interpolate::Interpolator; use komodo_client::{ entities::{ FileContents, RepoExecutionResponse, all_logs_success, - stack::{ - AdditionalEnvFile, ComposeFile, ComposeService, - ComposeServiceDeploy, StackRemoteFileContents, - StackServiceNames, - }, + stack::{AdditionalEnvFile, StackRemoteFileContents}, to_path_compatible_name, update::Log, }, @@ -28,7 +28,7 @@ use tracing::Instrument; use crate::{ config::periphery_config, - docker::compose::docker_compose, + docker::compose::{docker_compose, parse_compose_services}, helpers::{format_extra_args, format_log_grep}, stack::{ maybe_login_registry, pull_or_clone_stack, validate_files, @@ -54,12 +54,16 @@ impl Resolve for GetComposeLog { Default::default() }; let command = format!( - "{docker_compose} -p {project} logs --tail {tail}{timestamps} {}", + "{docker_compose} -p {project} logs --tail {tail}{timestamps} -- {}", services.join(" ") ); Ok( - run_komodo_standard_command("Get Stack Log", None, command) - .await, + run_komodo_standard_command( + "Get Stack Log", + command, + CommandOptions::default().timeout(Duration::from_secs(3)), + ) + .await, ) } } @@ -85,12 +89,21 @@ impl Resolve for GetComposeLogSearch { Default::default() }; let command = format!( - "{docker_compose} -p {project} logs --tail 5000{timestamps} {} 2>&1 | {grep}", - services.join(" ") + "{docker_compose} -p {} logs --tail 5000{timestamps} -- {} 2>&1 | {grep}", + escape(project.into()), + services + .iter() + .map(|service| escape(service.into())) + .collect::>() + .join(" ") ); Ok( - run_komodo_shell_command("Search Stack Log", None, command) - .await, + run_komodo_shell_command( + "Search Stack Log", + command, + CommandOptions::default().timeout(Duration::from_secs(3)), + ) + .await, ) } } @@ -340,7 +353,8 @@ impl Resolve for ComposePull { let service_args = if services.is_empty() { String::new() } else { - format!(" {}", services.join(" ")) + // `--` so a service beginning with `-` is not parsed as a flag. + format!(" -- {}", services.join(" ")) }; let file_args = stack.compose_file_paths().join(" -f "); @@ -390,8 +404,8 @@ impl Resolve for ComposePull { }; let Some(log) = run_komodo_command_with_sanitization( "Compose Pull", - run_directory.as_path(), pull_command, + CommandOptions::default().path(run_directory.as_path()), mode, &replacers, ) @@ -488,8 +502,8 @@ impl Resolve for ComposeUp { let span = info_span!("ExecutePreDeploy"); if let Some(log) = run_komodo_command_with_sanitization( "Pre Deploy", - pre_deploy_path.as_path(), &stack.config.pre_deploy.command, + CommandOptions::default().path(pre_deploy_path.as_path()), if stack.config.pre_deploy.shell_mode { KomodoCommandMode::Shell } else { @@ -512,7 +526,8 @@ impl Resolve for ComposeUp { let service_args = if services.is_empty() { String::new() } else { - format!(" {}", services.join(" ")) + // `--` so a service beginning with `-` is not parsed as a flag. + format!(" -- {}", services.join(" ")) }; let file_args = stack.compose_file_paths().join(" -f "); @@ -559,70 +574,55 @@ impl Resolve for ComposeUp { return Ok(res); } }; - let mode = if wrapped { - KomodoCommandMode::Shell - } else { - KomodoCommandMode::Standard - }; let span = info_span!("GetComposeConfig", command); - let Some(config_log) = run_komodo_command_with_sanitization( - "Compose Config", - run_directory.as_path(), - command, - mode, - &replacers, - ) - .instrument(span) - .await - else { - unreachable!() + let mut config_log = if wrapped { + run_komodo_shell_command( + "Compose Config", + command, + CommandOptions::default().path(run_directory.as_path()), + ) + .instrument(span) + .await + } else { + run_komodo_standard_command( + "Compose Config", + command, + CommandOptions::default().path(run_directory.as_path()), + ) + .instrument(span) + .await }; + if !config_log.success { + config_log.sanitize(&replacers); res.logs.push(config_log); return Ok(res); } - let compose = - serde_yaml_ng::from_str::(&config_log.stdout) - .context("Failed to parse compose contents")?; - // Store sanitized compose config output - res.merged_config = Some(config_log.stdout); - for ( - service_name, - ComposeService { - container_name, - deploy, - image, - }, - ) in compose.services - { - let image = image.unwrap_or_default(); - match deploy { - Some(ComposeServiceDeploy { - replicas: Some(replicas), - }) if replicas > 1 => { - for i in 1..1 + replicas { - res.services.push(StackServiceNames { - container_name: format!( - "{project_name}-{service_name}-{i}" - ), - service_name: format!("{service_name}-{i}"), - image: image.clone(), - image_digest: None, - }); - } - } - _ => { - res.services.push(StackServiceNames { - container_name: container_name.unwrap_or_else(|| { - format!("{project_name}-{service_name}") - }), - service_name, - image, - image_digest: None, - }); - } - } + + // attach sanitized merged config in any case. + res.merged_config = + svi::replace_in_string(&config_log.stdout, &replacers).into(); + + if let Err(e) = parse_compose_services( + &config_log.stdout, + &project_name, + &mut res.services, + ) { + config_log.sanitize(&replacers); + res.logs.push(config_log); + res.logs.push(Log::error( + "Parse Compose Services", + format_serror(&e.into()), + )); + // early return with error log + // including sanitized config log + // and parse error for clear view + // of what the issue might be. + return Ok(res); } + + config_log.sanitize(&replacers); + res.logs.push(config_log); } if stack.config.run_build { @@ -651,8 +651,8 @@ impl Resolve for ComposeUp { let span = info_span!("ExecuteComposeBuild"); let Some(log) = run_komodo_command_with_sanitization( "Compose Build", - run_directory.as_path(), command, + CommandOptions::default().path(run_directory.as_path()), mode, &replacers, ) @@ -694,8 +694,8 @@ impl Resolve for ComposeUp { let span = info_span!("RunComposePull"); let Some(log) = run_komodo_command_with_sanitization( "Compose Pull", - run_directory.as_path(), command, + CommandOptions::default().path(run_directory.as_path()), mode, &replacers, ) @@ -742,8 +742,8 @@ impl Resolve for ComposeUp { let span = info_span!("ExecuteComposeUp"); let Some(log) = run_komodo_command_with_sanitization( "Compose Up", - run_directory.as_path(), command, + CommandOptions::default().path(run_directory.as_path()), KomodoCommandMode::Shell, &replacers, ) @@ -762,8 +762,8 @@ impl Resolve for ComposeUp { let span = info_span!("ExecutePostDeploy"); if let Some(log) = run_komodo_command_with_sanitization( "Post Deploy", - post_deploy_path.as_path(), &stack.config.post_deploy.command, + CommandOptions::default().path(post_deploy_path.as_path()), if stack.config.post_deploy.shell_mode { KomodoCommandMode::Shell } else { @@ -803,8 +803,8 @@ impl Resolve for ComposeExecution { let docker_compose = docker_compose(); let log = run_komodo_standard_command( "Compose Command", - None, format!("{docker_compose} -p {project} {command}"), + CommandOptions::default(), ) .await; Ok(log) @@ -897,23 +897,28 @@ impl Resolve for ComposeRun { let project_name = stack.project_name(true); + // Comes straight off the request and interpolated into a + // command string which is handed to `bash -c`, so must be escaped. + let service = escape(Cow::Borrowed(&service)); + // Parse wrapper configuration let compose_cmd_wrapper = parse_multiline_command(&stack.config.compose_cmd_wrapper); // If wrapper_include is empty but wrapper is set, use default ["up"] for backward compatibility - let default_include = vec![String::from("up")]; + static DEFAULT_INCLUDE: LazyLock> = + LazyLock::new(|| vec![String::from("up")]); let wrapper_include = if stack.config.compose_cmd_wrapper_include.is_empty() && !compose_cmd_wrapper.is_empty() { - &default_include + &DEFAULT_INCLUDE } else { &stack.config.compose_cmd_wrapper_include }; if pull.unwrap_or_default() { let pull_command = format!( - "{docker_compose} -p {project_name} -f {file_args}{env_file_args} pull {service}", + "{docker_compose} -p {project_name} -f {file_args}{env_file_args} pull -- {service}", ); let (pull_command, wrapped) = match maybe_wrap_command( pull_command, @@ -931,8 +936,8 @@ impl Resolve for ComposeRun { }; let Some(pull_log) = run_komodo_command_with_sanitization( "Compose Pull", - run_directory.as_path(), pull_command, + CommandOptions::default().path(run_directory.as_path()), mode, &replacers, ) @@ -959,17 +964,25 @@ impl Resolve for ComposeRun { run_flags.push_str(" --service-ports"); } if let Some(dir) = workdir.as_ref() { - run_flags.push_str(&format!(" --workdir {dir}")); + run_flags + .push_str(&format!(" --workdir {}", escape(dir.into()))); } if let Some(user) = user.as_ref() { - run_flags.push_str(&format!(" --user {user}")); + run_flags.push_str(&format!(" --user {}", escape(user.into()))); } if let Some(entrypoint) = entrypoint.as_ref() { - run_flags.push_str(&format!(" --entrypoint {entrypoint}")); + run_flags.push_str(&format!( + " --entrypoint {}", + escape(entrypoint.into()) + )); } if let Some(env) = env { for (k, v) in env { - run_flags.push_str(&format!(" -e {}={} ", k, v)); + // Escaped as a single KEY=VALUE token, which is what compose expects. + run_flags.push_str(&format!( + " -e {}", + escape(format!("{k}={v}").into()) + )); } } @@ -979,7 +992,7 @@ impl Resolve for ComposeRun { .map(|argv| { let joined = argv .iter() - .map(|s| escape(Cow::Borrowed(s)).into_owned()) + .map(|s| escape(s.into()).into_owned()) .collect::>() .join(" "); format!(" {joined}") @@ -987,7 +1000,7 @@ impl Resolve for ComposeRun { .unwrap_or_default(); let run_command = format!( - "{docker_compose} -p {project_name} -f {file_args}{env_file_args} run{run_flags} {service}{command_args}", + "{docker_compose} -p {project_name} -f {file_args}{env_file_args} run{run_flags} -- {service}{command_args}", ); let (run_command, _) = match maybe_wrap_command( run_command, @@ -1002,8 +1015,8 @@ impl Resolve for ComposeRun { let span = info_span!("RunComposeRun", run_command); let Some(log) = run_komodo_command_with_sanitization( "Compose Run", - run_directory.as_path(), run_command, + CommandOptions::default().path(run_directory.as_path()), KomodoCommandMode::Shell, &replacers, ) @@ -1081,12 +1094,13 @@ async fn compose_down( let service_args = if services.is_empty() { String::new() } else { - format!(" {}", services.join(" ")) + // `--` so a service beginning with `-` is not parsed as a flag. + format!(" -- {}", services.join(" ")) }; let log = run_komodo_standard_command( "Compose Down", - None, format!("{docker_compose} -p {project} down{service_args}"), + CommandOptions::default(), ) .await; let success = log.success; diff --git a/bin/periphery/src/api/container/mod.rs b/bin/periphery/src/api/container/mod.rs index 2ecac8aad9..893f192dd7 100644 --- a/bin/periphery/src/api/container/mod.rs +++ b/bin/periphery/src/api/container/mod.rs @@ -1,6 +1,9 @@ +use std::time::Duration; + use anyhow::Context; use command::{ - run_komodo_shell_command, run_komodo_standard_command, + CommandOptions, run_komodo_shell_command, + run_komodo_standard_command, }; use futures_util::future::join_all; use komodo_client::entities::{ @@ -12,6 +15,7 @@ use komodo_client::entities::{ }; use mogh_resolver::Resolve; use periphery_client::api::container::*; +use shell_escape::unix::escape; use crate::{ docker::{stats::get_container_stats, stop_container_command}, @@ -58,11 +62,16 @@ impl Resolve for GetContainerLog { } else { Default::default() }; + // `--` so a name beginning with `-` is not parsed as a flag. let command = - format!("docker logs {name} --tail {tail}{timestamps}"); + format!("docker logs --tail {tail}{timestamps} -- {name}"); Ok( - run_komodo_standard_command("Get container log", None, command) - .await, + run_komodo_standard_command( + "Get container log", + command, + CommandOptions::default().timeout(Duration::from_secs(3)), + ) + .await, ) } } @@ -87,14 +96,15 @@ impl Resolve for GetContainerLogSearch { } else { Default::default() }; + let name = escape(name.into()); let command = format!( "docker logs {name} --tail 5000{timestamps} 2>&1 | {grep}" ); Ok( run_komodo_shell_command( "Get container log grep", - None, command, + CommandOptions::default().timeout(Duration::from_secs(3)), ) .await, ) @@ -163,8 +173,8 @@ impl Resolve for StartContainer { Ok( run_komodo_standard_command( "Docker Start", - None, - format!("docker start {}", self.name), + format!("docker start -- {}", self.name), + CommandOptions::default(), ) .await, ) @@ -190,8 +200,8 @@ impl Resolve for RestartContainer { Ok( run_komodo_standard_command( "Docker Restart", - None, - format!("docker restart {}", self.name), + format!("docker restart -- {}", self.name), + CommandOptions::default(), ) .await, ) @@ -217,8 +227,8 @@ impl Resolve for PauseContainer { Ok( run_komodo_standard_command( "Docker Pause", - None, - format!("docker pause {}", self.name), + format!("docker pause -- {}", self.name), + CommandOptions::default(), ) .await, ) @@ -242,8 +252,8 @@ impl Resolve for UnpauseContainer { Ok( run_komodo_standard_command( "Docker Unpause", - None, - format!("docker unpause {}", self.name), + format!("docker unpause -- {}", self.name), + CommandOptions::default(), ) .await, ) @@ -268,13 +278,20 @@ impl Resolve for StopContainer { ) -> anyhow::Result { let StopContainer { name, signal, time } = self; let command = stop_container_command(&name, signal, time); - let log = - run_komodo_standard_command("Docker Stop", None, command).await; + let log = run_komodo_standard_command( + "Docker Stop", + command, + CommandOptions::default(), + ) + .await; if log.stderr.contains("unknown flag: --signal") { let command = stop_container_command(&name, None, time); - let mut log = - run_komodo_standard_command("Docker Stop", None, command) - .await; + let mut log = run_komodo_standard_command( + "Docker Stop", + command, + CommandOptions::default(), + ) + .await; log.stderr = format!( "old docker version: unable to use --signal flag{}", if !log.stderr.is_empty() { @@ -307,23 +324,24 @@ impl Resolve for RemoveContainer { args: &crate::api::Args, ) -> anyhow::Result { let RemoveContainer { name, signal, time } = self; + let name = escape(name.into()); let stop_command = stop_container_command(&name, signal, time); let command = - format!("{stop_command} && docker container rm {name}"); + format!("{stop_command} && docker container rm -- {name}"); let log = run_komodo_shell_command( "Docker Stop and Remove", - None, command, + CommandOptions::default(), ) .await; if log.stderr.contains("unknown flag: --signal") { let stop_command = stop_container_command(&name, None, time); let command = - format!("{stop_command} && docker container rm {name}"); + format!("{stop_command} && docker container rm -- {name}"); let mut log = run_komodo_shell_command( "Docker Stop and Remove", - None, command, + CommandOptions::default(), ) .await; log.stderr = format!( @@ -362,10 +380,14 @@ impl Resolve for RenameContainer { curr_name, new_name, } = self; - let command = format!("docker rename {curr_name} {new_name}"); + let command = format!("docker rename -- {curr_name} {new_name}"); Ok( - run_komodo_standard_command("Docker Rename", None, command) - .await, + run_komodo_standard_command( + "Docker Rename", + command, + CommandOptions::default(), + ) + .await, ) } } @@ -387,8 +409,12 @@ impl Resolve for PruneContainers { ) -> anyhow::Result { let command = String::from("docker container prune -f"); Ok( - run_komodo_standard_command("Prune Containers", None, command) - .await, + run_komodo_standard_command( + "Prune Containers", + command, + CommandOptions::default(), + ) + .await, ) } } @@ -424,10 +450,14 @@ impl Resolve for StartAllContainers { { return None; } - let command = format!("docker start {name}"); + let command = format!("docker start -- {name}"); Some(async move { - run_komodo_standard_command(&command.clone(), None, command) - .await + run_komodo_standard_command( + &command.clone(), + command, + CommandOptions::default(), + ) + .await }) }, ); @@ -466,10 +496,14 @@ impl Resolve for RestartAllContainers { { return None; } - let command = format!("docker restart {name}"); + let command = format!("docker restart -- {name}"); Some(async move { - run_komodo_standard_command(&command.clone(), None, command) - .await + run_komodo_standard_command( + &command.clone(), + command, + CommandOptions::default(), + ) + .await }) }, ); @@ -508,10 +542,14 @@ impl Resolve for PauseAllContainers { { return None; } - let command = format!("docker pause {name}"); + let command = format!("docker pause -- {name}"); Some(async move { - run_komodo_standard_command(&command.clone(), None, command) - .await + run_komodo_standard_command( + &command.clone(), + command, + CommandOptions::default(), + ) + .await }) }, ); @@ -550,10 +588,14 @@ impl Resolve for UnpauseAllContainers { { return None; } - let command = format!("docker unpause {name}"); + let command = format!("docker unpause -- {name}"); Some(async move { - run_komodo_standard_command(&command.clone(), None, command) - .await + run_komodo_standard_command( + &command.clone(), + command, + CommandOptions::default(), + ) + .await }) }, ); @@ -595,8 +637,8 @@ impl Resolve for StopAllContainers { Some(async move { run_komodo_standard_command( &format!("Docker stop {name}"), - None, stop_container_command(name, None, None), + CommandOptions::default(), ) .await }) diff --git a/bin/periphery/src/api/container/run.rs b/bin/periphery/src/api/container/run.rs index b409562c62..3b889b31d7 100644 --- a/bin/periphery/src/api/container/run.rs +++ b/bin/periphery/src/api/container/run.rs @@ -2,7 +2,8 @@ use std::fmt::Write; use anyhow::Context; use command::{ - KomodoCommandMode, run_komodo_command_with_sanitization, + CommandOptions, KomodoCommandMode, + run_komodo_command_with_sanitization, }; use formatting::format_serror; use interpolate::Interpolator; @@ -92,8 +93,10 @@ impl Resolve for RunContainer { let _ = pull_image(image).await; debug!("image pulled"); + // Remove the existing container under the previously deployed name, + // in case the name configuration changed since the last deploy. let _ = (RemoveContainer { - name: deployment.name.clone(), + name: deployment.deployed_name().to_string(), signal: stop_signal, time: stop_time, }) @@ -107,8 +110,8 @@ impl Resolve for RunContainer { let span = info_span!("ExecuteDockerRun"); let Some(log) = run_komodo_command_with_sanitization( "Docker Run", - None, command, + CommandOptions::default(), KomodoCommandMode::Shell, &replacers, ) @@ -125,24 +128,21 @@ impl Resolve for RunContainer { } fn docker_run_command( - Deployment { - name, - config: - DeploymentConfig { - volumes, - ports, - network, - command, - restart, - environment, - labels, - extra_args, - .. - }, - .. - }: &Deployment, + deployment: &Deployment, image: &str, ) -> anyhow::Result { + let name = deployment.custom_name(); + let DeploymentConfig { + volumes, + ports, + network, + command, + restart, + environment, + labels, + extra_args, + .. + } = &deployment.config; let mut res = format!("docker run -d --name {name} --network {network}"); diff --git a/bin/periphery/src/api/docker.rs b/bin/periphery/src/api/docker.rs index 4f3eeae943..93da4332f9 100644 --- a/bin/periphery/src/api/docker.rs +++ b/bin/periphery/src/api/docker.rs @@ -1,7 +1,7 @@ use std::sync::OnceLock; use anyhow::Context; -use command::run_komodo_standard_command; +use command::{CommandOptions, run_komodo_standard_command}; use komodo_client::entities::{ deployment::extract_registry_domain, docker::{ @@ -132,8 +132,8 @@ impl Resolve for PullImage { anyhow::Ok( run_komodo_standard_command( "Docker Pull", - None, - format!("docker pull {name}"), + format!("docker pull -- {name}"), + CommandOptions::default(), ) .await, ) @@ -164,10 +164,14 @@ impl Resolve for DeleteImage { self, args: &crate::api::Args, ) -> anyhow::Result { - let command = format!("docker image rm {}", self.name); + let command = format!("docker image rm -- {}", self.name); Ok( - run_komodo_standard_command("Delete Image", None, command) - .await, + run_komodo_standard_command( + "Delete Image", + command, + CommandOptions::default(), + ) + .await, ) } } @@ -189,8 +193,12 @@ impl Resolve for PruneImages { ) -> anyhow::Result { let command = String::from("docker image prune -a -f"); Ok( - run_komodo_standard_command("Prune Images", None, command) - .await, + run_komodo_standard_command( + "Prune Images", + command, + CommandOptions::default(), + ) + .await, ) } } @@ -235,10 +243,14 @@ impl Resolve for CreateNetwork { Some(driver) => format!(" -d {driver}"), None => String::new(), }; - let command = format!("docker network create{driver} {name}"); + let command = format!("docker network create{driver} -- {name}"); Ok( - run_komodo_standard_command("Create Network", None, command) - .await, + run_komodo_standard_command( + "Create Network", + command, + CommandOptions::default(), + ) + .await, ) } } @@ -259,10 +271,14 @@ impl Resolve for DeleteNetwork { self, args: &crate::api::Args, ) -> anyhow::Result { - let command = format!("docker network rm {}", self.name); + let command = format!("docker network rm -- {}", self.name); Ok( - run_komodo_standard_command("Delete Network", None, command) - .await, + run_komodo_standard_command( + "Delete Network", + command, + CommandOptions::default(), + ) + .await, ) } } @@ -284,8 +300,12 @@ impl Resolve for PruneNetworks { ) -> anyhow::Result { let command = String::from("docker network prune -f"); Ok( - run_komodo_standard_command("Prune Networks", None, command) - .await, + run_komodo_standard_command( + "Prune Networks", + command, + CommandOptions::default(), + ) + .await, ) } } @@ -324,10 +344,14 @@ impl Resolve for DeleteVolume { self, args: &crate::api::Args, ) -> anyhow::Result { - let command = format!("docker volume rm {}", self.name); + let command = format!("docker volume rm -- {}", self.name); Ok( - run_komodo_standard_command("Delete Volume", None, command) - .await, + run_komodo_standard_command( + "Delete Volume", + command, + CommandOptions::default(), + ) + .await, ) } } @@ -349,8 +373,12 @@ impl Resolve for PruneVolumes { ) -> anyhow::Result { let command = String::from("docker volume prune -a -f"); Ok( - run_komodo_standard_command("Prune Volumes", None, command) - .await, + run_komodo_standard_command( + "Prune Volumes", + command, + CommandOptions::default(), + ) + .await, ) } } diff --git a/bin/periphery/src/api/mod.rs b/bin/periphery/src/api/mod.rs index ab9c19382f..d9da3a164e 100644 --- a/bin/periphery/src/api/mod.rs +++ b/bin/periphery/src/api/mod.rs @@ -1,7 +1,7 @@ -use command::run_komodo_standard_command; +use command::{CommandOptions, run_komodo_standard_command}; use encoding::{EncodedJsonMessage, EncodedResponse}; use komodo_client::entities::{ - config::{DockerRegistry, GitProvider}, + config::{GitProvider, ImageRegistry}, stats::SystemProcess, update::Log, }; @@ -54,7 +54,7 @@ pub enum PeripheryRequest { // Config (Read) ListGitProviders(ListGitProviders), - ListDockerRegistries(ListDockerRegistries), + ListImageRegistries(ListImageRegistries), ListSecrets(ListSecrets), // Repo (Write) @@ -68,6 +68,7 @@ pub enum PeripheryRequest { GetDockerfileContentsOnHost(GetDockerfileContentsOnHost), WriteDockerfileContentsToHost(WriteDockerfileContentsToHost), Build(Build), + CancelBuild(CancelBuild), PruneBuilders(PruneBuilders), PruneBuildx(PruneBuildx), @@ -225,12 +226,12 @@ impl Resolve for ListGitProviders { } } -impl Resolve for ListDockerRegistries { +impl Resolve for ListImageRegistries { async fn resolve( self, _: &Args, - ) -> anyhow::Result> { - Ok(periphery_config().docker_registries.0.clone()) + ) -> anyhow::Result> { + Ok(periphery_config().image_registries.0.clone()) } } @@ -260,8 +261,12 @@ impl Resolve for PruneSystem { async fn resolve(self, args: &Args) -> anyhow::Result { let command = String::from("docker system prune -a -f --volumes"); Ok( - run_komodo_standard_command("Prune System", None, command) - .await, + run_komodo_standard_command( + "Prune System", + command, + CommandOptions::default(), + ) + .await, ) } } diff --git a/bin/periphery/src/api/swarm/mod.rs b/bin/periphery/src/api/swarm/mod.rs index 4e30a219a7..cbc025f60e 100644 --- a/bin/periphery/src/api/swarm/mod.rs +++ b/bin/periphery/src/api/swarm/mod.rs @@ -1,5 +1,5 @@ use anyhow::Context as _; -use command::run_komodo_standard_command; +use command::{CommandOptions, run_komodo_standard_command}; use komodo_client::entities::{ docker::{SwarmLists, node::SwarmNode, task::SwarmTask}, update::Log, @@ -112,12 +112,17 @@ impl Resolve for UpdateSwarmNode { } } - command += " "; + // `--` so a node beginning with `-` is not parsed as a flag. + command += " -- "; command += &self.node; Ok( - run_komodo_standard_command("Update Swarm Node", None, command) - .await, + run_komodo_standard_command( + "Update Swarm Node", + command, + CommandOptions::default(), + ) + .await, ) } } @@ -141,6 +146,8 @@ impl Resolve for RemoveSwarmNodes { if self.force { command += " --force" } + // `--` so a node beginning with `-` is not parsed as a flag. + command += " --"; for node in self.nodes { command += " "; command += &node; @@ -148,8 +155,8 @@ impl Resolve for RemoveSwarmNodes { Ok( run_komodo_standard_command( "Remove Swarm Nodes", - None, command, + CommandOptions::default(), ) .await, ) diff --git a/bin/periphery/src/api/swarm/service.rs b/bin/periphery/src/api/swarm/service.rs index 679e46bdb9..151a6620a8 100644 --- a/bin/periphery/src/api/swarm/service.rs +++ b/bin/periphery/src/api/swarm/service.rs @@ -1,9 +1,10 @@ -use std::fmt::Write; +use std::{fmt::Write, time::Duration}; use anyhow::Context as _; use command::{ - KomodoCommandMode, run_komodo_command_with_sanitization, - run_komodo_shell_command, run_komodo_standard_command, + CommandOptions, KomodoCommandMode, + run_komodo_command_with_sanitization, run_komodo_shell_command, + run_komodo_standard_command, }; use formatting::format_serror; use interpolate::Interpolator; @@ -22,6 +23,7 @@ use periphery_client::api::swarm::{ InspectSwarmService, RemoveSwarmServices, RollbackSwarmService, UpdateSwarmService, }; +use shell_escape::unix::escape; use tracing::Instrument; use crate::{ @@ -82,13 +84,13 @@ impl Resolve for GetSwarmServiceLog { Default::default() }; let command = format!( - "docker service logs --tail {tail}{timestamps}{no_task_ids}{no_resolve}{details} {service}", + "docker service logs --tail {tail}{timestamps}{no_task_ids}{no_resolve}{details} -- {service}", ); Ok( run_komodo_standard_command( "Get Swarm Service Log", - None, command, + CommandOptions::default().timeout(Duration::from_secs(3)), ) .await, ) @@ -131,14 +133,15 @@ impl Resolve for GetSwarmServiceLogSearch { Default::default() }; let grep = format_log_grep(&terms, combinator, invert); + let service = escape(service.into()); let command = format!( "docker service logs --tail 5000{timestamps}{no_task_ids}{no_resolve}{details} {service} 2>&1 | {grep}", ); Ok( run_komodo_shell_command( "Search Swarm Service Log", - None, command, + CommandOptions::default().timeout(Duration::from_secs(3)), ) .await, ) @@ -160,6 +163,8 @@ impl Resolve for RemoveSwarmServices { args: &crate::api::Args, ) -> anyhow::Result { let mut command = String::from("docker service rm"); + // `--` so a service beginning with `-` is not parsed as a flag. + command += " --"; for service in self.services { command += " "; command += &service; @@ -167,8 +172,8 @@ impl Resolve for RemoveSwarmServices { Ok( run_komodo_standard_command( "Remove Swarm Services", - None, command, + CommandOptions::default(), ) .await, ) @@ -192,8 +197,8 @@ impl Resolve for RollbackSwarmService { Ok( run_komodo_standard_command( "Rollback Swarm Service", - None, - format!("docker service rollback {}", self.service), + format!("docker service rollback -- {}", self.service), + CommandOptions::default(), ) .await, ) @@ -267,8 +272,10 @@ impl Resolve for CreateSwarmService { } }; + // Remove the existing service under the previously deployed name, + // in case the name configuration changed since the last deploy. let log = (RemoveSwarmServices { - services: vec![deployment.name.clone()], + services: vec![deployment.deployed_name().to_string()], }) .resolve(args) .await; @@ -293,8 +300,8 @@ impl Resolve for CreateSwarmService { let span = info_span!("ExecuteDockerServiceCreate"); if let Some(log) = run_komodo_command_with_sanitization( "Docker Service Create", - None, command, + CommandOptions::default(), KomodoCommandMode::Shell, &replacers, ) @@ -309,24 +316,21 @@ impl Resolve for CreateSwarmService { } fn docker_service_create_command( - Deployment { - name, - config: - DeploymentConfig { - volumes, - ports, - network, - command, - environment, - labels, - extra_args, - .. - }, - .. - }: &Deployment, + deployment: &Deployment, image: &str, use_with_registry_auth: bool, ) -> anyhow::Result { + let name = deployment.custom_name(); + let DeploymentConfig { + volumes, + ports, + network, + command, + environment, + labels, + extra_args, + .. + } = &deployment.config; let mut res = format!( "docker service create --name {name} --network {network}" ); @@ -430,13 +434,14 @@ impl Resolve for UpdateSwarmService { push_extra_args(&mut command, &extra_args)?; - write!(&mut command, " {service}")?; + // `--` so a name beginning with `-` is not parsed as a flag. + write!(&mut command, " -- {service}")?; let span = info_span!("ExecuteDockerServiceCreate"); let log = run_komodo_standard_command( "Docker Service Create", - None, command, + CommandOptions::default(), ) .instrument(span) .await; diff --git a/bin/periphery/src/api/swarm/stack.rs b/bin/periphery/src/api/swarm/stack.rs index b1fbc17628..8ce8db0933 100644 --- a/bin/periphery/src/api/swarm/stack.rs +++ b/bin/periphery/src/api/swarm/stack.rs @@ -2,8 +2,8 @@ use std::fmt::Write; use anyhow::{Context as _, anyhow}; use command::{ - KomodoCommandMode, run_komodo_command_with_sanitization, - run_komodo_standard_command, + CommandOptions, KomodoCommandMode, + run_komodo_command_with_sanitization, run_komodo_standard_command, }; use formatting::format_serror; use interpolate::Interpolator; @@ -92,6 +92,8 @@ impl Resolve for RemoveSwarmStacks { if !self.detach { command += " --detach=false" } + // `--` so a stack beginning with `-` is not parsed as a flag. + command += " --"; for stack in self.stacks { command += " "; command += &stack; @@ -99,8 +101,8 @@ impl Resolve for RemoveSwarmStacks { Ok( run_komodo_standard_command( "Remove Swarm Stacks", - None, command, + CommandOptions::default(), ) .await, ) @@ -185,8 +187,8 @@ impl Resolve for DeploySwarmStack { let span = info_span!("ExecutePreDeploy"); if let Some(log) = run_komodo_command_with_sanitization( "Pre Deploy", - pre_deploy_path.as_path(), &stack.config.pre_deploy.command, + CommandOptions::default().path(pre_deploy_path.as_path()), KomodoCommandMode::Multiline, &replacers, ) @@ -251,8 +253,8 @@ impl Resolve for DeploySwarmStack { let span = info_span!("GetStackConfig", command); let Some(config_log) = run_komodo_command_with_sanitization( "Stack Config", - run_directory.as_path(), command, + CommandOptions::default().path(run_directory.as_path()), mode, &replacers, ) @@ -304,7 +306,8 @@ impl Resolve for DeploySwarmStack { command += " --with-registry-auth"; } push_extra_args(&mut command, &stack.config.extra_args)?; - write!(&mut command, " {project_name}")?; + // `--` so a name beginning with `-` is not parsed as a flag. + write!(&mut command, " -- {project_name}")?; // Apply compose cmd wrapper if configured let (command, _) = match maybe_wrap_command( @@ -323,8 +326,8 @@ impl Resolve for DeploySwarmStack { let span = info_span!("ExecuteStackDeploy"); let Some(log) = run_komodo_command_with_sanitization( "Deploy Swarm Stack", - run_directory.as_path(), command, + CommandOptions::default().path(run_directory.as_path()), KomodoCommandMode::Shell, &replacers, ) @@ -343,8 +346,8 @@ impl Resolve for DeploySwarmStack { let span = info_span!("ExecutePostDeploy"); if let Some(log) = run_komodo_command_with_sanitization( "Post Deploy", - post_deploy_path.as_path(), &stack.config.post_deploy.command, + CommandOptions::default().path(post_deploy_path.as_path()), KomodoCommandMode::Multiline, &replacers, ) @@ -403,8 +406,8 @@ async fn remove_stack( ) -> anyhow::Result<()> { let log = run_komodo_standard_command( "Remove Stack", - None, format!("docker stack rm --detach=false {stack}"), + CommandOptions::default(), ) .await; let success = log.success; diff --git a/bin/periphery/src/api/terminal.rs b/bin/periphery/src/api/terminal.rs index 6a1d44cf30..56aad10476 100644 --- a/bin/periphery/src/api/terminal.rs +++ b/bin/periphery/src/api/terminal.rs @@ -74,6 +74,7 @@ impl Resolve for CreateServerTerminal { .map(|terminal| Terminal { name: terminal.name.clone(), target: TerminalTarget::Server { server: None }, + target_name: None, command: terminal.command.clone(), stored_size_kb: terminal.history.size_kb(), created_at: terminal.created_at, @@ -131,6 +132,7 @@ impl Resolve for CreateContainerExecTerminal { .map(|terminal| Terminal { name: terminal.name.clone(), target: terminal.target.clone(), + target_name: None, command: terminal.command.clone(), stored_size_kb: terminal.history.size_kb(), created_at: terminal.created_at, @@ -185,6 +187,7 @@ impl Resolve for CreateContainerAttachTerminal { .map(|terminal| Terminal { name: terminal.name.clone(), target: terminal.target.clone(), + target_name: None, command: terminal.command.clone(), stored_size_kb: terminal.history.size_kb(), created_at: terminal.created_at, @@ -463,8 +466,15 @@ async fn setup_execute_command_on_terminal( tokio_util::codec::LinesCodec::new(), ); + // Build the command as a single physical line: use `\n` escapes that `printf` + // expands, rather than real newline bytes. A multi-line command is echoed back + // by the PTY (local echo is on for interactive use) with the sentinels on their + // own lines, and the reader below matches those *echoed* lines instead of the + // real command output (issue #1289). A single-line command cannot produce a bare + // sentinel line in its echo, while `printf` still emits the sentinels on their + // own lines in the actual output. let full_command = format!( - "printf '\n{START_OF_OUTPUT}\n\n'; {command}; rc=$?; printf '\n{KOMODO_EXIT_CODE}%d\n{END_OF_OUTPUT}\n' \"$rc\"\n" + "printf '\\n{START_OF_OUTPUT}\\n\\n'; {command}; rc=$?; printf '\\n{KOMODO_EXIT_CODE}%d\\n{END_OF_OUTPUT}\\n' \"$rc\"\n" ); terminal diff --git a/bin/periphery/src/config.rs b/bin/periphery/src/config.rs index d67623fa3f..6e043caad1 100644 --- a/bin/periphery/src/config.rs +++ b/bin/periphery/src/config.rs @@ -180,7 +180,7 @@ pub fn periphery_config() -> &'static PeripheryConfig { .or(config.ssl_cert_file), secrets: config.secrets, git_providers: config.git_providers, - docker_registries: config.docker_registries, + image_registries: config.image_registries, } }) } diff --git a/bin/periphery/src/connection/mod.rs b/bin/periphery/src/connection/mod.rs index 27960e101c..4c3190c3c8 100644 --- a/bin/periphery/src/connection/mod.rs +++ b/bin/periphery/src/connection/mod.rs @@ -172,7 +172,7 @@ fn handle_request( let ping_in_progress = async { loop { - tokio::time::sleep(Duration::from_secs(5)).await; + tokio::time::sleep(Duration::from_secs(4)).await; if let Err(e) = sender.send_in_progress(channel).await { error!("Failed to ping in progress over channel | {e:?}"); } diff --git a/bin/periphery/src/docker/compose.rs b/bin/periphery/src/docker/compose.rs index 996a249a4d..0c1be080d5 100644 --- a/bin/periphery/src/docker/compose.rs +++ b/bin/periphery/src/docker/compose.rs @@ -1,6 +1,8 @@ +use std::time::Duration; + use anyhow::{Context, anyhow}; -use command::run_komodo_standard_command; -use komodo_client::entities::stack::ComposeProject; +use command::{CommandOptions, run_komodo_standard_command}; +use komodo_client::entities::stack::*; use serde::{Deserialize, Serialize}; use crate::config::periphery_config; @@ -18,8 +20,8 @@ pub async fn list_compose_projects() let docker_compose = docker_compose(); let res = run_komodo_standard_command( "List Projects", - None, format!("{docker_compose} ls --all --format json"), + CommandOptions::default().timeout(Duration::from_secs(1)), ) .await; @@ -67,3 +69,52 @@ pub struct DockerComposeLsItem { #[serde(default, alias = "ConfigFiles")] pub config_files: String, } + +pub fn parse_compose_services( + raw_config: &str, + project_name: &str, + services: &mut Vec, +) -> anyhow::Result<()> { + let compose = serde_yaml_ng::from_str::(raw_config) + .context("Failed to parse compose contents")?; + + for ( + service_name, + ComposeService { + container_name, + deploy, + image, + }, + ) in compose.services + { + let image = image.unwrap_or_default(); + match deploy { + Some(ComposeServiceDeploy { + replicas: Some(replicas), + }) if replicas > 1 => { + for i in 1..1 + replicas { + services.push(StackServiceNames { + container_name: format!( + "{project_name}-{service_name}-{i}" + ), + service_name: format!("{service_name}-{i}"), + image: image.clone(), + image_digest: None, + }); + } + } + _ => { + services.push(StackServiceNames { + container_name: container_name.unwrap_or_else(|| { + format!("{project_name}-{service_name}") + }), + service_name, + image, + image_digest: None, + }); + } + } + } + + Ok(()) +} diff --git a/bin/periphery/src/docker/config.rs b/bin/periphery/src/docker/config.rs index 056f580de8..5d3cf3e0ac 100644 --- a/bin/periphery/src/docker/config.rs +++ b/bin/periphery/src/docker/config.rs @@ -1,8 +1,9 @@ -use std::fmt::Write; +use std::{fmt::Write, time::Duration}; use anyhow::{Context, anyhow}; use command::{ - run_komodo_shell_command, run_komodo_standard_command, + CommandOptions, run_komodo_shell_command, + run_komodo_standard_command, }; use data_encoding::BASE64URL; use futures_util::{TryStreamExt as _, stream::FuturesUnordered}; @@ -15,6 +16,7 @@ use komodo_client::entities::{ random_string, }; use periphery_client::api::swarm::CreateSwarmConfig; +use shell_escape::unix::escape; use super::*; @@ -23,8 +25,8 @@ pub async fn list_swarm_configs( ) -> anyhow::Result> { let res = run_komodo_standard_command( "List Swarm Configs", - None, "docker config ls --format json", + CommandOptions::default().timeout(Duration::from_secs(1)), ) .await; @@ -68,8 +70,10 @@ pub async fn inspect_swarm_config( ) -> anyhow::Result { let res = run_komodo_standard_command( "Inspect Swarm Config", - None, - format!(r#"docker config inspect "{config}""#), + // `--` so a name beginning with `-` is not parsed as a flag. + // The quotes alone give no protection, as they are stripped when lexed. + format!(r#"docker config inspect -- "{config}""#), + CommandOptions::default().timeout(Duration::from_secs(3)), ) .await; @@ -115,26 +119,32 @@ pub async fn create_swarm_config( template_driver, }: &CreateSwarmConfig, ) -> anyhow::Result { - let mut command = String::from("docker config create"); + let mut flags = String::new(); for label in labels { - write!(&mut command, " --label {label}")?; + write!(&mut flags, " --label {}", escape(label.into()))?; } if let Some(driver) = template_driver { - write!(&mut command, " --template-driver {driver}")?; + write!( + &mut flags, + " --template-driver {}", + escape(driver.into()) + )?; } - write!( - &mut command, - r#" {name} - <<'EOF' -{} -EOF"#, - data.trim() - )?; + let command = format!( + "printf '%s\\n' {} | docker config create{flags} {} -", + escape(data.trim().into()), + escape(name.into()) + ); - let log = - run_komodo_shell_command("Create Config", None, command).await; + let log = run_komodo_shell_command( + "Create Config", + command, + CommandOptions::default(), + ) + .await; Ok(log) } @@ -143,12 +153,18 @@ pub async fn remove_swarm_configs( configs: impl Iterator, ) -> Log { let mut command = String::from("docker config rm"); + // `--` so a name beginning with `-` is not parsed as a flag. + command += " --"; for config in configs { command += " "; command += config; } - run_komodo_standard_command("Remove Swarm Configs", None, command) - .await + run_komodo_standard_command( + "Remove Swarm Configs", + command, + CommandOptions::default(), + ) + .await } pub async fn recreate_swarm_config( @@ -319,12 +335,13 @@ async fn switch_service_config( write!(&mut command, ",mode={mode}")?; } - write!(&mut command, " {service}")?; + // `--` so a name beginning with `-` is not parsed as a flag. + write!(&mut command, " -- {service}")?; let log = run_komodo_standard_command( "Switch Service Config", - None, command, + CommandOptions::default(), ) .await; diff --git a/bin/periphery/src/docker/container.rs b/bin/periphery/src/docker/container.rs index 0ac4fc128a..5c111142da 100644 --- a/bin/periphery/src/docker/container.rs +++ b/bin/periphery/src/docker/container.rs @@ -37,6 +37,7 @@ impl DockerClient { let stats = stats.get(&name).cloned(); anyhow::Ok(ContainerListItem { server_id: None, + server_name: None, name, stats, id: container.id, diff --git a/bin/periphery/src/docker/image.rs b/bin/periphery/src/docker/image.rs index 2fa542661a..f054268210 100644 --- a/bin/periphery/src/docker/image.rs +++ b/bin/periphery/src/docker/image.rs @@ -1,6 +1,8 @@ +use std::time::Duration; + use anyhow::Context; use bollard::query_parameters::ListImagesOptions; -use command::run_komodo_standard_command; +use command::{CommandOptions, run_komodo_standard_command}; use komodo_client::entities::docker::{ GraphDriverData, HealthConfig, container::ContainerListItem, image::*, @@ -193,7 +195,12 @@ pub async fn get_image_digest_from_registry( let command = String::from( r#"docker buildx imagetools inspect --format "{{json .Manifest}}" "#, ) + image; - let log = run_komodo_standard_command("", None, command).await; + let log = run_komodo_standard_command( + "", + command, + CommandOptions::default().timeout(Duration::from_secs(10)), + ) + .await; if !log.success { return Err(anyhow::Error::msg(log.combined())); } diff --git a/bin/periphery/src/docker/mod.rs b/bin/periphery/src/docker/mod.rs index 40599bc2e5..d526837fb5 100644 --- a/bin/periphery/src/docker/mod.rs +++ b/bin/periphery/src/docker/mod.rs @@ -1,11 +1,14 @@ use anyhow::{Context, anyhow}; use bollard::Docker; -use command::{run_komodo_standard_command, run_shell_command}; +use command::{ + CommandOptions, run_komodo_standard_command, run_standard_command, +}; use komodo_client::entities::{ TerminationSignal, docker::{task::*, *}, update::Log, }; +use shell_escape::unix::escape; pub mod compose; pub mod config; @@ -51,9 +54,18 @@ pub async fn docker_login( None => crate::helpers::registry_token(domain, account)?, }; - let log = run_shell_command(&format!( - "echo {registry_token} | docker login {domain} --username '{account}' --password-stdin", - ), None) + // The token is written to the child's stdin rather than interpolated into + // the command, so it never reaches the process arguments, where any user + // on the host could read it out of `ps`. This runs without a shell, and + // `--` keeps a domain beginning with `-` from being parsed as a flag. + let log = run_standard_command( + &format!( + "docker login --username {} --password-stdin -- {}", + escape(account.into()), + escape(domain.into()), + ), + CommandOptions::default().stdin(registry_token), + ) .await; if log.success() { @@ -76,8 +88,13 @@ pub async fn docker_login( #[instrument("PullImage")] pub async fn pull_image(image: &str) -> Log { - let command = format!("docker pull {image}"); - run_komodo_standard_command("Docker Pull", None, command).await + let command = format!("docker pull -- {image}"); + run_komodo_standard_command( + "Docker Pull", + command, + CommandOptions::default(), + ) + .await } pub fn stop_container_command( @@ -91,7 +108,8 @@ pub fn stop_container_command( let time = time .map(|time| format!(" --time {time}")) .unwrap_or_default(); - format!("docker stop{signal}{time} {container_name}") + // `--` so a name beginning with `-` is not parsed as a flag. + format!("docker stop{signal}{time} -- {container_name}") } fn convert_object_version( diff --git a/bin/periphery/src/docker/secret.rs b/bin/periphery/src/docker/secret.rs index 058310ab35..01194d844d 100644 --- a/bin/periphery/src/docker/secret.rs +++ b/bin/periphery/src/docker/secret.rs @@ -3,7 +3,8 @@ use std::fmt::Write; use anyhow::Context; use bollard::query_parameters::ListSecretsOptions; use command::{ - run_komodo_shell_command, run_komodo_standard_command, + CommandOptions, run_komodo_shell_command, + run_komodo_standard_command, }; use futures_util::{TryStreamExt as _, stream::FuturesUnordered}; use komodo_client::entities::{ @@ -17,6 +18,7 @@ use komodo_client::entities::{ update::Log, }; use periphery_client::api::swarm::CreateSwarmSecret; +use shell_escape::unix::escape; use super::DockerClient; @@ -118,39 +120,39 @@ pub async fn create_swarm_secret( template_driver, }: &CreateSwarmSecret, ) -> anyhow::Result { - let mut command = String::from("docker secret create"); + let mut flags = String::new(); if let Some(driver) = driver { - write!(&mut command, " --driver {driver}")?; + write!(&mut flags, " --driver {}", escape(driver.into()))?; } for label in labels { - write!(&mut command, " --label {label}")?; + write!(&mut flags, " --label {}", escape(label.into()))?; } if let Some(driver) = template_driver { - write!(&mut command, " --template-driver {driver}")?; + write!( + &mut flags, + " --template-driver {}", + escape(driver.into()) + )?; } - let mut sanitized_command = command.clone(); - - write!( - &mut command, - r#" {name} - <<'EOF' -{} -EOF"#, - data.trim() - )?; - - write!( - &mut sanitized_command, - r#" {name} - <<'EOF' - -EOF"# - )?; + let name = escape(name.into()); + let command = format!( + "printf '%s\\n' {} | docker secret create{flags} {name} -", + escape(data.trim().into()) + ); + let sanitized_command = format!( + "printf '%s\\n' | docker secret create{flags} {name} -" + ); - let mut log = - run_komodo_shell_command("Create Secret", None, command).await; + let mut log = run_komodo_shell_command( + "Create Secret", + command, + CommandOptions::default(), + ) + .await; log.command = sanitized_command; @@ -161,12 +163,18 @@ pub async fn remove_swarm_secrets( secrets: impl Iterator, ) -> Log { let mut command = String::from("docker secret rm"); + // `--` so a name beginning with `-` is not parsed as a flag. + command += " --"; for secret in secrets { command += " "; command += secret; } - run_komodo_standard_command("Remove Swarm Secrets", None, command) - .await + run_komodo_standard_command( + "Remove Swarm Secrets", + command, + CommandOptions::default(), + ) + .await } pub async fn recreate_swarm_secret( @@ -340,12 +348,13 @@ async fn switch_service_secret( write!(&mut command, ",mode={mode}")?; } - write!(&mut command, " {service}")?; + // `--` so a name beginning with `-` is not parsed as a flag. + write!(&mut command, " -- {service}")?; let log = run_komodo_standard_command( "Switch Service Secret", - None, command, + CommandOptions::default(), ) .await; diff --git a/bin/periphery/src/docker/stack.rs b/bin/periphery/src/docker/stack.rs index 0069e34963..eb9775ef70 100644 --- a/bin/periphery/src/docker/stack.rs +++ b/bin/periphery/src/docker/stack.rs @@ -1,5 +1,7 @@ +use std::time::Duration; + use anyhow::{Context, anyhow}; -use command::run_komodo_standard_command; +use command::{CommandOptions, run_komodo_standard_command}; use futures_util::{StreamExt, stream::FuturesOrdered}; use komodo_client::entities::{ docker::{ @@ -71,8 +73,8 @@ pub async fn list_swarm_stacks( ) -> anyhow::Result> { let res = run_komodo_standard_command( "List Swarm Stacks", - None, "docker stack ls --format json", + CommandOptions::default().timeout(Duration::from_secs(3)), ) .await; @@ -130,8 +132,8 @@ pub async fn list_swarm_stack_service_ids( ) -> anyhow::Result> { let res = run_komodo_standard_command( "List Swarm Stack Services", - None, - format!("docker stack services --format json {stack}"), + format!("docker stack services --format json -- {stack}"), + CommandOptions::default().timeout(Duration::from_secs(3)), ) .await; @@ -160,8 +162,8 @@ pub async fn list_swarm_stack_tasks( ) -> anyhow::Result> { let res = run_komodo_standard_command( "List Swarm Stack Tasks", - None, - format!("docker stack ps --format json --no-trunc {stack}"), + format!("docker stack ps --format json --no-trunc -- {stack}"), + CommandOptions::default().timeout(Duration::from_secs(3)), ) .await; diff --git a/bin/periphery/src/docker/stats.rs b/bin/periphery/src/docker/stats.rs index 1f9291bee7..a4e0749867 100644 --- a/bin/periphery/src/docker/stats.rs +++ b/bin/periphery/src/docker/stats.rs @@ -1,9 +1,9 @@ -use std::{collections::HashMap, sync::Arc}; +use std::{collections::HashMap, sync::Arc, time::Duration}; use anyhow::{Context, anyhow}; use async_timing_util::wait_until_timelength; use bollard::{models, query_parameters::StatsOptionsBuilder}; -use command::run_standard_command; +use command::{CommandOptions, run_standard_command}; use futures_util::StreamExt; use komodo_client::entities::docker::{ container::ContainerStats, @@ -59,13 +59,19 @@ pub async fn get_container_stats( \"Name\":\"{{ .Name }}\", \ \"NetIO\":\"{{ .NetIO }}\",\ \"PIDs\":\"{{ .PIDs }}\"}'"; + // `--` follows the flags, so a name beginning with `-` is not + // parsed as a flag and the flags are not parsed as container names. let container_name = match container_name { - Some(name) => format!(" {name}"), + Some(name) => format!(" -- {name}"), None => "".to_string(), }; let command = - format!("docker stats{container_name} --no-stream {format}"); - let output = run_standard_command(&command, None).await; + format!("docker stats --no-stream {format}{container_name}"); + let output = run_standard_command( + &command, + CommandOptions::default().timeout(Duration::from_secs(10)), + ) + .await; if output.success() { output .stdout diff --git a/bin/periphery/src/helpers.rs b/bin/periphery/src/helpers.rs index a17c052334..620caf0d42 100644 --- a/bin/periphery/src/helpers.rs +++ b/bin/periphery/src/helpers.rs @@ -5,8 +5,8 @@ use std::{ use anyhow::Context; use command::{ - KomodoCommandMode, run_komodo_command_with_sanitization, - run_standard_command, + CommandOptions, KomodoCommandMode, + run_komodo_command_with_sanitization, run_standard_command, }; use environment::write_env_file; use interpolate::Interpolator; @@ -19,6 +19,7 @@ use komodo_client::{ parsers::QUOTE_PATTERN, }; use periphery_client::api::git::PeripheryRepoExecutionResponse; +use shell_escape::unix::escape; use crate::config::periphery_config; @@ -121,12 +122,15 @@ pub fn format_log_grep( let maybe_invert = if invert { " -v" } else { Default::default() }; match combinator { SearchCombinator::Or => { - format!("grep{maybe_invert} -E '{}'", terms.join("|")) + format!( + "grep{maybe_invert} -E {}", + escape(terms.join("|").into()) + ) } SearchCombinator::And => { format!( - "grep{maybe_invert} -P '^(?=.*{})'", - terms.join(")(?=.*") + "grep{maybe_invert} -P {}", + escape(format!("^(?=.*{})", terms.join(")(?=.*")).into()) ) } } @@ -187,8 +191,8 @@ pub async fn handle_post_repo_execution( .collect::(); if let Some(log) = run_komodo_command_with_sanitization( "On Clone", - path.as_path(), on_clone.command, + CommandOptions::default().path(path.as_path()), if on_clone.shell_mode { KomodoCommandMode::Shell } else { @@ -216,8 +220,8 @@ pub async fn handle_post_repo_execution( .collect::(); if let Some(log) = run_komodo_command_with_sanitization( "On Pull", - path.as_path(), on_pull.command, + CommandOptions::default().path(path.as_path()), if on_pull.shell_mode { KomodoCommandMode::Shell } else { @@ -271,7 +275,7 @@ pub fn registry_token( account_username: &str, ) -> anyhow::Result<&'static str> { periphery_config() - .docker_registries + .image_registries .iter() .find(|registry| registry.domain == domain) .and_then(|registry| { @@ -367,7 +371,8 @@ async fn generate_self_signed_ssl_certs() { let command = format!( "openssl req -x509 -newkey rsa:4096 -keyout {key_path} -out {cert_path} -sha256 -days 3650 -nodes -subj \"/C=XX/CN=periphery\"" ); - let log = run_standard_command(&command, None).await; + let log = + run_standard_command(&command, CommandOptions::default()).await; if log.success() { info!("✅ SSL Certs generated"); diff --git a/bin/periphery/src/state.rs b/bin/periphery/src/state.rs index 08da16458d..c2457a900f 100644 --- a/bin/periphery/src/state.rs +++ b/bin/periphery/src/state.rs @@ -341,3 +341,11 @@ pub async fn host_public_ip() -> Option<&'static String> { .await .as_ref() } + +type CancelCache = CloneCache; + +/// Maps build id => CancellationToken +pub fn build_cancel_cache() -> &'static CancelCache { + static BUILD_CANCEL_CACHE: OnceLock = OnceLock::new(); + BUILD_CANCEL_CACHE.get_or_init(Default::default) +} diff --git a/bin/periphery/src/stats/mem.rs b/bin/periphery/src/stats/mem.rs new file mode 100644 index 0000000000..123f74e729 --- /dev/null +++ b/bin/periphery/src/stats/mem.rs @@ -0,0 +1,150 @@ +/// Memory breakdown, all fields in bytes. +#[derive(Clone, Copy)] +pub struct MemBreakdown { + /// Total physical memory. + pub total: u64, + /// Free memory (`MemFree`). + pub free: u64, + /// Reclaimable page cache + buffers. + pub buff_cache: u64, + /// Full ZFS ARC size (`0` when ZFS is not present). + pub zfs_arc: u64, + /// Real application-used memory: `(total - available) - zfs_arc`. + pub used: u64, +} + +/// Read the full memory breakdown. Prefers `/proc` on Linux, falling +/// back to `sysinfo` when the files are unavailable (non-Linux, or read +/// failure). +pub fn read(system: &sysinfo::System) -> MemBreakdown { + #[cfg(target_os = "linux")] + if let Some(breakdown) = read_linux() { + return breakdown; + } + read_sysinfo(system) +} + +/// Cross-platform fallback using sysinfo. No buff/cache or ARC breakdown +/// is available here. +fn read_sysinfo(system: &sysinfo::System) -> MemBreakdown { + let total = system.total_memory(); + let available = system.available_memory(); + MemBreakdown { + total, + free: system.free_memory(), + buff_cache: 0, + zfs_arc: 0, + used: total.saturating_sub(available), + } +} + +/// Compute the breakdown from the Linux `/proc` files. Returns `None` if +/// `/proc/meminfo` can't be read (then we fall back to sysinfo). +/// +/// `MemAvailable` does not count the ZFS ARC as reclaimable, so a large +/// ARC would inflate "used" and cause false high-memory alerts. ARC is +/// reclaimable, so we subtract it from used and report it separately. +#[cfg(target_os = "linux")] +fn read_linux() -> Option { + let meminfo = + parse_meminfo(&std::fs::read_to_string("/proc/meminfo").ok()?)?; + // Absent when ZFS isn't loaded => arc stays zero. + let zfs_arc = + std::fs::read_to_string("/proc/spl/kstat/zfs/arcstats") + .ok() + .map(|c| parse_zfs_arc_size(&c)) + .unwrap_or(0); + + // Cached + SReclaimable + Buffers - Shmem + let buff_cache = meminfo + .cached + .saturating_add(meminfo.s_reclaimable) + .saturating_add(meminfo.buffers) + .saturating_sub(meminfo.shmem); + + let raw_used = meminfo.total.saturating_sub(meminfo.available); + + Some(MemBreakdown { + total: meminfo.total, + free: meminfo.free, + buff_cache, + zfs_arc, + used: raw_used.saturating_sub(zfs_arc), + }) +} + +#[cfg(target_os = "linux")] +#[derive(Default)] +struct MemInfo { + total: u64, + free: u64, + available: u64, + buffers: u64, + cached: u64, + s_reclaimable: u64, + shmem: u64, +} + +/// Parse the relevant fields from `/proc/meminfo` contents. Values there +/// are in kibibytes; we convert to bytes. Returns `None` if `MemTotal` is +/// missing (i.e. not a real meminfo file). +#[cfg(target_os = "linux")] +fn parse_meminfo(contents: &str) -> Option { + let mut info = MemInfo::default(); + let mut have_total = false; + let mut have_available = false; + + for line in contents.lines() { + let Some((key, rest)) = line.split_once(':') else { + continue; + }; + // rest looks like " 12345 kB" + let Some(kb) = rest.split_whitespace().next() else { + continue; + }; + let Ok(kb) = kb.parse::() else { + continue; + }; + let bytes = kb.saturating_mul(1024); + match key { + "MemTotal" => { + info.total = bytes; + have_total = true; + } + "MemFree" => info.free = bytes, + "MemAvailable" => { + info.available = bytes; + have_available = true; + } + "Buffers" => info.buffers = bytes, + "Cached" => info.cached = bytes, + "SReclaimable" => info.s_reclaimable = bytes, + "Shmem" => info.shmem = bytes, + _ => {} + } + } + + if !have_total { + return None; + } + // Older kernels (<3.14) lack MemAvailable; fall back to free. + if !have_available { + info.available = info.free; + } + + Some(info) +} + +/// Parse the ZFS ARC `size` (bytes) from `/proc/spl/kstat/zfs/arcstats` +/// contents. The line looks like: `size 4 4294967296` (name, type, data). +/// Returns `0` if not found. +#[cfg(target_os = "linux")] +fn parse_zfs_arc_size(contents: &str) -> u64 { + for line in contents.lines() { + let mut fields = line.split_whitespace(); + if fields.next() == Some("size") { + return fields.nth(1).and_then(|v| v.parse().ok()).unwrap_or(0); + } + } + 0 +} diff --git a/bin/periphery/src/stats.rs b/bin/periphery/src/stats/mod.rs similarity index 90% rename from bin/periphery/src/stats.rs rename to bin/periphery/src/stats/mod.rs index 2a82a37912..eb70dafb00 100644 --- a/bin/periphery/src/stats.rs +++ b/bin/periphery/src/stats/mod.rs @@ -9,6 +9,8 @@ use sysinfo::{ProcessRefreshKind, ProcessesToUpdate, System}; use crate::{config::periphery_config, state::stats_client}; +mod mem; + /// This should be called before starting the server in main.rs. /// Keeps the cached stats up to date pub fn spawn_polling_thread() { @@ -78,8 +80,7 @@ impl StatsClient { } pub fn get_system_stats(&self) -> SystemStats { - let total_mem = self.system.total_memory(); - let available_mem = self.system.available_memory(); + let memory = mem::read(&self.system); let mut network_ingress_bytes: u64 = 0; let mut network_egress_bytes: u64 = 0; @@ -98,9 +99,13 @@ impl StatsClient { five: load_avg.five, fifteen: load_avg.fifteen, }, - mem_free_gb: self.system.free_memory() as f64 / BYTES_PER_GB, - mem_used_gb: (total_mem - available_mem) as f64 / BYTES_PER_GB, - mem_total_gb: total_mem as f64 / BYTES_PER_GB, + mem_free_gb: memory.free as f64 / BYTES_PER_GB, + mem_used_gb: memory.used as f64 / BYTES_PER_GB, + mem_total_gb: memory.total as f64 / BYTES_PER_GB, + mem_buff_cache_gb: memory.buff_cache as f64 / BYTES_PER_GB, + mem_zfs_arc_gb: memory.zfs_arc as f64 / BYTES_PER_GB, + swap_total_gb: self.system.total_swap() as f64 / BYTES_PER_GB, + swap_used_gb: self.system.used_swap() as f64 / BYTES_PER_GB, network_ingress_bytes: network_ingress_bytes as f64, network_egress_bytes: network_egress_bytes as f64, disks: self.get_disks(), @@ -200,11 +205,13 @@ fn get_system_information( kernel: System::kernel_version(), host_name: System::host_name(), core_count: System::physical_core_count().map(|c| c as u32), + logical_core_count: Some(sys.cpus().len() as u32), cpu_brand: sys .cpus() .iter() .next() .map(|cpu| cpu.brand().to_string()) .unwrap_or_default(), + cpu_arch: System::cpu_arch(), } } diff --git a/bin/periphery/src/terminal.rs b/bin/periphery/src/terminal.rs index 445748e156..bfeea7e111 100644 --- a/bin/periphery/src/terminal.rs +++ b/bin/periphery/src/terminal.rs @@ -185,6 +185,7 @@ pub async fn list_terminals( .map(|terminal| Terminal { name: terminal.name.clone(), target: terminal.target.clone(), + target_name: None, command: terminal.command.clone(), stored_size_kb: terminal.history.size_kb(), created_at: terminal.created_at, diff --git a/client/core/rs/Cargo.toml b/client/core/rs/Cargo.toml index 2523df66d3..eb09895230 100644 --- a/client/core/rs/Cargo.toml +++ b/client/core/rs/Cargo.toml @@ -3,7 +3,6 @@ name = "komodo_client" description = "Client for the Komodo build and deployment system" version.workspace = true edition.workspace = true -authors.workspace = true license.workspace = true homepage.workspace = true repository.workspace = true @@ -16,9 +15,10 @@ mongo = ["dep:mongo_indexed"] utoipa = ["dep:utoipa", "mogh_auth_client/utoipa", "mogh_error/utoipa"] schemars = ["dep:schemars"] logger = ["dep:mogh_logger"] +svi = ["dep:svi"] cli = ["logger", "dep:mogh_pki",] -core = ["mongo", "utoipa", "logger", "dep:mogh_server"] -periphery = ["cli", "dep:ipnetwork", "dep:mogh_server"] +core = ["mongo", "utoipa", "logger", "svi", "dep:mogh_server"] +periphery = ["cli", "dep:ipnetwork", "svi", "dep:mogh_server"] [dependencies] # mogh @@ -27,6 +27,7 @@ mongo_indexed = { workspace = true, optional = true } mogh_logger = { workspace = true, optional = true } mogh_server = { workspace = true, optional = true } mogh_error = { workspace = true, features = ["axum"] } +svi = { workspace = true, optional = true } derive_default_builder.workspace = true async_timing_util.workspace = true mogh_auth_client.workspace = true @@ -53,6 +54,7 @@ bytes.workspace = true serde.workspace = true tokio.workspace = true strum.workspace = true +regex.workspace = true bson.workspace = true clap.workspace = true envy.workspace = true diff --git a/client/core/rs/src/api/execute/action.rs b/client/core/rs/src/api/execute/action.rs index 02fd43452e..964e4ad897 100644 --- a/client/core/rs/src/api/execute/action.rs +++ b/client/core/rs/src/api/execute/action.rs @@ -85,4 +85,43 @@ pub struct BatchRunAction { /// extra-action-1, extra-action-2 /// ``` pub pattern: String, + + /// Filter matches by tag. + /// If empty, skips tag filtering. + #[serde(default)] + pub tags: Vec, +} + +// + +#[cfg(feature = "utoipa")] +#[utoipa::path( + post, + path = "/CancelAction", + description = "Cancel an action run.", + request_body(content = CancelAction), + responses( + (status = 200, description = "The update", body = Update), + ), +)] +pub fn cancel_action() {} + +/// Cancels the target action run. Response: [Update] +#[typeshare] +#[derive( + Debug, Clone, PartialEq, Serialize, Deserialize, Resolve, Parser, +)] +#[cfg_attr(feature = "utoipa", derive(utoipa::ToSchema))] +#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))] +#[empty_traits(KomodoExecuteRequest)] +#[response(Update)] +#[error(mogh_error::Error)] +pub struct CancelAction { + /// Id or name + pub action: String, + /// The update id associated with the specific + /// run to cancel + /// Must provide either `action` + /// or `update_id` + pub update_id: Option, } diff --git a/client/core/rs/src/api/execute/build.rs b/client/core/rs/src/api/execute/build.rs index a0bc4a6f50..939286d774 100644 --- a/client/core/rs/src/api/execute/build.rs +++ b/client/core/rs/src/api/execute/build.rs @@ -84,6 +84,11 @@ pub struct BatchRunBuild { /// extra-build-1, extra-build-2 /// ``` pub pattern: String, + + /// Filter matches by tag. + /// If empty, skips tag filtering. + #[serde(default)] + pub tags: Vec, } // diff --git a/client/core/rs/src/api/execute/deployment.rs b/client/core/rs/src/api/execute/deployment.rs index a4099685e0..57725450c1 100644 --- a/client/core/rs/src/api/execute/deployment.rs +++ b/client/core/rs/src/api/execute/deployment.rs @@ -85,6 +85,11 @@ pub struct BatchDeploy { /// extra-deployment-1, extra-deployment-2 /// ``` pub pattern: String, + + /// Filter matches by tag. + /// If empty, skips tag filtering. + #[serde(default)] + pub tags: Vec, } // @@ -352,4 +357,9 @@ pub struct BatchDestroyDeployment { /// extra-deployment-1, extra-deployment-2 /// ``` pub pattern: String, + + /// Filter matches by tag. + /// If empty, skips tag filtering. + #[serde(default)] + pub tags: Vec, } diff --git a/client/core/rs/src/api/execute/mod.rs b/client/core/rs/src/api/execute/mod.rs index 963a035599..38b15d696f 100644 --- a/client/core/rs/src/api/execute/mod.rs +++ b/client/core/rs/src/api/execute/mod.rs @@ -139,12 +139,14 @@ pub enum Execution { #[clap(alias = "procedure", alias = "pr")] RunProcedure(RunProcedure), BatchRunProcedure(BatchRunProcedure), + CancelProcedure(CancelProcedure), // ACTION /// Run the target action. (alias: `action`, `ac`) #[clap(alias = "action", alias = "ac")] RunAction(RunAction), BatchRunAction(BatchRunAction), + CancelAction(CancelAction), // SYNC /// Execute a Resource Sync. (alias: `sync`) diff --git a/client/core/rs/src/api/execute/openapi.rs b/client/core/rs/src/api/execute/openapi.rs index d820c53d72..2ba6f02329 100644 --- a/client/core/rs/src/api/execute/openapi.rs +++ b/client/core/rs/src/api/execute/openapi.rs @@ -79,9 +79,11 @@ use crate::api::execute; // procedure execute::run_procedure, execute::batch_run_procedure, + execute::cancel_procedure, // action execute::run_action, execute::batch_run_action, + execute::cancel_action, // alerter execute::test_alerter, execute::send_alert, diff --git a/client/core/rs/src/api/execute/procedure.rs b/client/core/rs/src/api/execute/procedure.rs index c04ce92ae2..29d3781524 100644 --- a/client/core/rs/src/api/execute/procedure.rs +++ b/client/core/rs/src/api/execute/procedure.rs @@ -72,4 +72,44 @@ pub struct BatchRunProcedure { /// extra-procedure-1, extra-procedure-2 /// ``` pub pattern: String, + + /// Filter matches by tag. + /// If empty, skips tag filtering. + #[serde(default)] + pub tags: Vec, +} + +// + +#[cfg(feature = "utoipa")] +#[utoipa::path( + post, + path = "/CancelProcedure", + description = "Cancel an procedure run.", + request_body(content = CancelProcedure), + responses( + (status = 200, description = "The update", body = Update), + ), +)] +pub fn cancel_procedure() {} + +/// Cancels the target procedure run. Response: [Update] +#[typeshare] +#[derive( + Debug, Clone, PartialEq, Serialize, Deserialize, Resolve, Parser, +)] +#[cfg_attr(feature = "utoipa", derive(utoipa::ToSchema))] +#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))] +#[empty_traits(KomodoExecuteRequest)] +#[response(Update)] +#[error(mogh_error::Error)] +pub struct CancelProcedure { + /// Id or name + pub procedure: String, + /// The update id associated with the specific + /// run to cancel + /// Must provide either `action` + /// or `update_id` + #[arg(long, short = 'u')] + pub update_id: Option, } diff --git a/client/core/rs/src/api/execute/repo.rs b/client/core/rs/src/api/execute/repo.rs index 8af2a2f713..1b9316779f 100644 --- a/client/core/rs/src/api/execute/repo.rs +++ b/client/core/rs/src/api/execute/repo.rs @@ -80,6 +80,11 @@ pub struct BatchCloneRepo { /// extra-repo-1, extra-repo-2 /// ``` pub pattern: String, + + /// Filter matches by tag. + /// If empty, skips tag filtering. + #[serde(default)] + pub tags: Vec, } // @@ -152,6 +157,11 @@ pub struct BatchPullRepo { /// extra-repo-1, extra-repo-2 /// ``` pub pattern: String, + + /// Filter matches by tag. + /// If empty, skips tag filtering. + #[serde(default)] + pub tags: Vec, } // @@ -228,6 +238,11 @@ pub struct BatchBuildRepo { /// extra-repo-1, extra-repo-2 /// ``` pub pattern: String, + + /// Filter matches by tag. + /// If empty, skips tag filtering. + #[serde(default)] + pub tags: Vec, } // diff --git a/client/core/rs/src/api/execute/stack.rs b/client/core/rs/src/api/execute/stack.rs index a3fbc6da49..fc8b416ac3 100644 --- a/client/core/rs/src/api/execute/stack.rs +++ b/client/core/rs/src/api/execute/stack.rs @@ -81,6 +81,11 @@ pub struct BatchDeployStack { /// extra-stack-1, extra-stack-2 /// ``` pub pattern: String, + + /// Filter matches by tag. + /// If empty, skips tag filtering. + #[serde(default)] + pub tags: Vec, } // @@ -153,6 +158,11 @@ pub struct BatchDeployStackIfChanged { /// extra-stack-1, extra-stack-2 /// ``` pub pattern: String, + + /// Filter matches by tag. + /// If empty, skips tag filtering. + #[serde(default)] + pub tags: Vec, } // @@ -224,6 +234,11 @@ pub struct BatchPullStack { /// extra-stack-1, extra-stack-2 /// ``` pub pattern: String, + + /// Filter matches by tag. + /// If empty, skips tag filtering. + #[serde(default)] + pub tags: Vec, } // @@ -534,4 +549,9 @@ pub struct BatchDestroyStack { /// extra-stack-1, extra-stack-2 /// ``` pub pattern: String, + + /// Filter matches by tag. + /// If empty, skips tag filtering. + #[serde(default)] + pub tags: Vec, } diff --git a/client/core/rs/src/api/read/action.rs b/client/core/rs/src/api/read/action.rs index bc1a9f0352..e5bd65e6ef 100644 --- a/client/core/rs/src/api/read/action.rs +++ b/client/core/rs/src/api/read/action.rs @@ -2,8 +2,12 @@ use mogh_resolver::Resolve; use serde::{Deserialize, Serialize}; use typeshare::typeshare; -use crate::entities::action::{ - Action, ActionActionState, ActionListItem, ActionQuery, +use crate::entities::{ + U64, + action::{ + Action, ActionActionState, ActionListItem, ActionQuery, + ActionSortBy, + }, }; use super::KomodoReadRequest; @@ -63,6 +67,31 @@ pub struct ListActions { /// optional structured query to filter actions. #[serde(default)] pub query: ActionQuery, + + /// Retrieve more results by incrementing the page. + /// `page: 0` is default. + #[serde(default)] + pub page: U64, + + /// Set the limit for number of resources per-page. + /// If not provided, uses the Core config + /// `default_pagination_limit` (default: 30). + /// + /// Passing `limit: 0` returns all results (unlimited). + /// + /// Note: the page logic relies on this being consistent + /// across queries for more pages. + pub limit: Option, + + /// Sort the results by this field. + /// Defaults to Name. Non-Name sorts are applied in memory + /// after querying all matching resources. + #[serde(default)] + pub sort_by: ActionSortBy, + + /// Reverse the sort direction. + #[serde(default)] + pub sort_desc: bool, } #[typeshare] @@ -93,6 +122,21 @@ pub struct ListFullActions { /// optional structured query to filter actions. #[serde(default)] pub query: ActionQuery, + + /// Retrieve more results by incrementing the page. + /// `page: 0` is default. + #[serde(default)] + pub page: U64, + + /// Set the limit for number of resources per-page. + /// If not provided, uses the Core config + /// `default_pagination_limit` (default: 30). + /// + /// Passing `limit: 0` returns all results (unlimited). + /// + /// Note: the page logic relies on this being consistent + /// across queries for more pages. + pub limit: Option, } #[typeshare] diff --git a/client/core/rs/src/api/read/alerter.rs b/client/core/rs/src/api/read/alerter.rs index b72c1807fe..5905372f60 100644 --- a/client/core/rs/src/api/read/alerter.rs +++ b/client/core/rs/src/api/read/alerter.rs @@ -2,8 +2,9 @@ use mogh_resolver::Resolve; use serde::{Deserialize, Serialize}; use typeshare::typeshare; -use crate::entities::alerter::{ - Alerter, AlerterListItem, AlerterQuery, +use crate::entities::{ + U64, + alerter::{Alerter, AlerterListItem, AlerterQuery, AlerterSortBy}, }; use super::KomodoReadRequest; @@ -63,6 +64,31 @@ pub struct ListAlerters { /// Structured query to filter alerters. #[serde(default)] pub query: AlerterQuery, + + /// Retrieve more results by incrementing the page. + /// `page: 0` is default. + #[serde(default)] + pub page: U64, + + /// Set the limit for number of resources per-page. + /// If not provided, uses the Core config + /// `default_pagination_limit` (default: 30). + /// + /// Passing `limit: 0` returns all results (unlimited). + /// + /// Note: the page logic relies on this being consistent + /// across queries for more pages. + pub limit: Option, + + /// Sort the results by this field. + /// Defaults to Name. Non-Name sorts are applied in memory + /// after querying all matching resources. + #[serde(default)] + pub sort_by: AlerterSortBy, + + /// Reverse the sort direction. + #[serde(default)] + pub sort_desc: bool, } #[typeshare] @@ -93,6 +119,21 @@ pub struct ListFullAlerters { /// Structured query to filter alerters. #[serde(default)] pub query: AlerterQuery, + + /// Retrieve more results by incrementing the page. + /// `page: 0` is default. + #[serde(default)] + pub page: U64, + + /// Set the limit for number of resources per-page. + /// If not provided, uses the Core config + /// `default_pagination_limit` (default: 30). + /// + /// Passing `limit: 0` returns all results (unlimited). + /// + /// Note: the page logic relies on this being consistent + /// across queries for more pages. + pub limit: Option, } #[typeshare] diff --git a/client/core/rs/src/api/read/build.rs b/client/core/rs/src/api/read/build.rs index 11cc9d83b6..6447a9d3a9 100644 --- a/client/core/rs/src/api/read/build.rs +++ b/client/core/rs/src/api/read/build.rs @@ -5,8 +5,10 @@ use serde::{Deserialize, Serialize}; use typeshare::typeshare; use crate::entities::{ - I64, Version, - build::{Build, BuildActionState, BuildListItem, BuildQuery}, + I64, U64, Version, + build::{ + Build, BuildActionState, BuildListItem, BuildQuery, BuildSortBy, + }, }; use super::KomodoReadRequest; @@ -66,6 +68,31 @@ pub struct ListBuilds { /// optional structured query to filter builds. #[serde(default)] pub query: BuildQuery, + + /// Retrieve more results by incrementing the page. + /// `page: 0` is default. + #[serde(default)] + pub page: U64, + + /// Set the limit for number of resources per-page. + /// If not provided, uses the Core config + /// `default_pagination_limit` (default: 30). + /// + /// Passing `limit: 0` returns all results (unlimited). + /// + /// Note: the page logic relies on this being consistent + /// across queries for more pages. + pub limit: Option, + + /// Sort the results by this field. + /// Defaults to Name. Non-Name sorts are applied in memory + /// after querying all matching resources. + #[serde(default)] + pub sort_by: BuildSortBy, + + /// Reverse the sort direction. + #[serde(default)] + pub sort_desc: bool, } #[typeshare] @@ -96,6 +123,21 @@ pub struct ListFullBuilds { /// optional structured query to filter builds. #[serde(default)] pub query: BuildQuery, + + /// Retrieve more results by incrementing the page. + /// `page: 0` is default. + #[serde(default)] + pub page: U64, + + /// Set the limit for number of resources per-page. + /// If not provided, uses the Core config + /// `default_pagination_limit` (default: 30). + /// + /// Passing `limit: 0` returns all results (unlimited). + /// + /// Note: the page logic relies on this being consistent + /// across queries for more pages. + pub limit: Option, } #[typeshare] diff --git a/client/core/rs/src/api/read/builder.rs b/client/core/rs/src/api/read/builder.rs index c6824d8f42..7c18618a13 100644 --- a/client/core/rs/src/api/read/builder.rs +++ b/client/core/rs/src/api/read/builder.rs @@ -2,8 +2,9 @@ use mogh_resolver::Resolve; use serde::{Deserialize, Serialize}; use typeshare::typeshare; -use crate::entities::builder::{ - Builder, BuilderListItem, BuilderQuery, +use crate::entities::{ + U64, + builder::{Builder, BuilderListItem, BuilderQuery, BuilderSortBy}, }; use super::KomodoReadRequest; @@ -62,6 +63,31 @@ pub fn list_builders() {} pub struct ListBuilders { #[serde(default)] pub query: BuilderQuery, + + /// Retrieve more results by incrementing the page. + /// `page: 0` is default. + #[serde(default)] + pub page: U64, + + /// Set the limit for number of resources per-page. + /// If not provided, uses the Core config + /// `default_pagination_limit` (default: 30). + /// + /// Passing `limit: 0` returns all results (unlimited). + /// + /// Note: the page logic relies on this being consistent + /// across queries for more pages. + pub limit: Option, + + /// Sort the results by this field. + /// Defaults to Name. Non-Name sorts are applied in memory + /// after querying all matching resources. + #[serde(default)] + pub sort_by: BuilderSortBy, + + /// Reverse the sort direction. + #[serde(default)] + pub sort_desc: bool, } #[typeshare] @@ -91,6 +117,21 @@ pub fn list_full_builders() {} pub struct ListFullBuilders { #[serde(default)] pub query: BuilderQuery, + + /// Retrieve more results by incrementing the page. + /// `page: 0` is default. + #[serde(default)] + pub page: U64, + + /// Set the limit for number of resources per-page. + /// If not provided, uses the Core config + /// `default_pagination_limit` (default: 30). + /// + /// Passing `limit: 0` returns all results (unlimited). + /// + /// Note: the page logic relies on this being consistent + /// across queries for more pages. + pub limit: Option, } #[typeshare] diff --git a/client/core/rs/src/api/read/deployment.rs b/client/core/rs/src/api/read/deployment.rs index e7f50ac679..7f252f2331 100644 --- a/client/core/rs/src/api/read/deployment.rs +++ b/client/core/rs/src/api/read/deployment.rs @@ -6,7 +6,7 @@ use crate::entities::{ I64, SearchCombinator, U64, deployment::{ Deployment, DeploymentActionState, DeploymentListItem, - DeploymentQuery, DeploymentState, + DeploymentQuery, DeploymentSortBy, DeploymentState, }, docker::{ container::{Container, ContainerListItem, ContainerStats}, @@ -73,6 +73,31 @@ pub struct ListDeployments { /// optional structured query to filter deployments. #[serde(default)] pub query: DeploymentQuery, + + /// Retrieve more results by incrementing the page. + /// `page: 0` is default. + #[serde(default)] + pub page: U64, + + /// Set the limit for number of resources per-page. + /// If not provided, uses the Core config + /// `default_pagination_limit` (default: 30). + /// + /// Passing `limit: 0` returns all results (unlimited). + /// + /// Note: the page logic relies on this being consistent + /// across queries for more pages. + pub limit: Option, + + /// Sort the results by this field. + /// Defaults to Name. Non-Name sorts are applied in memory + /// after querying all matching resources. + #[serde(default)] + pub sort_by: DeploymentSortBy, + + /// Reverse the sort direction. + #[serde(default)] + pub sort_desc: bool, } #[typeshare] @@ -104,6 +129,21 @@ pub struct ListFullDeployments { /// optional structured query to filter deployments. #[serde(default)] pub query: DeploymentQuery, + + /// Retrieve more results by incrementing the page. + /// `page: 0` is default. + #[serde(default)] + pub page: U64, + + /// Set the limit for number of resources per-page. + /// If not provided, uses the Core config + /// `default_pagination_limit` (default: 30). + /// + /// Passing `limit: 0` returns all results (unlimited). + /// + /// Note: the page logic relies on this being consistent + /// across queries for more pages. + pub limit: Option, } #[typeshare] diff --git a/client/core/rs/src/api/read/docker.rs b/client/core/rs/src/api/read/docker.rs index 38cb76c979..6d27798b5a 100644 --- a/client/core/rs/src/api/read/docker.rs +++ b/client/core/rs/src/api/read/docker.rs @@ -5,7 +5,10 @@ use typeshare::typeshare; use crate::entities::{ ResourceTarget, SearchCombinator, U64, docker::{ - container::{Container, ContainerListItem}, + container::{ + Container, ContainerListItem, ContainerSortBy, + ContainerStateStatusEnum, + }, image::{Image, ImageHistoryResponseItem, ImageListItem}, network::{Network, NetworkListItem}, volume::{Volume, VolumeListItem}, @@ -21,30 +24,32 @@ use super::KomodoReadRequest; #[cfg(feature = "utoipa")] #[utoipa::path( post, - path = "/GetDockerContainersSummary", + path = "/GetContainersSummary", description = "Gets a summary of data relating to all containers.", - request_body(content = GetDockerContainersSummary), + request_body(content = GetContainersSummary), responses( - (status = 200, description = "The docker containers summary", body = GetDockerContainersSummaryResponse), + (status = 200, description = "The containers summary", body = GetContainersSummaryResponse), ), )] -pub fn get_docker_containers_summary() {} +pub fn get_containers_summary() {} /// Gets a summary of data relating to all containers. -/// Response: [GetDockerContainersSummaryResponse]. +/// Response: [GetContainersSummaryResponse]. +/// +/// Pre v2.3.0, called `GetDockerContainersSummary` #[typeshare] #[derive(Serialize, Deserialize, Debug, Clone, Resolve)] #[cfg_attr(feature = "utoipa", derive(utoipa::ToSchema))] #[empty_traits(KomodoReadRequest)] -#[response(GetDockerContainersSummaryResponse)] +#[response(GetContainersSummaryResponse)] #[error(mogh_error::Error)] -pub struct GetDockerContainersSummary {} +pub struct GetContainersSummary {} -/// Response for [GetDockerContainersSummary] +/// Response for [GetContainersSummary] #[typeshare] #[derive(Serialize, Deserialize, Debug, Clone, Default)] #[cfg_attr(feature = "utoipa", derive(utoipa::ToSchema))] -pub struct GetDockerContainersSummaryResponse { +pub struct GetContainersSummaryResponse { /// The total number of Containers pub total: u32, /// The number of Containers with Running state @@ -62,89 +67,128 @@ pub struct GetDockerContainersSummaryResponse { #[cfg(feature = "utoipa")] #[utoipa::path( post, - path = "/ListAllDockerContainers", - description = "List all docker containers on the target servers.", - request_body(content = ListAllDockerContainers), + path = "/ListAllContainers", + description = "List all containers on the target servers.", + request_body(content = ListAllContainers), responses( - (status = 200, description = "The list of containers", body = ListAllDockerContainersResponse), + (status = 200, description = "The list of containers", body = ListAllContainersResponse), ), )] -pub fn list_all_docker_containers() {} +pub fn list_all_containers() {} -/// List all docker containers on the target servers. -/// Response: [ListDockerContainersResponse]. +/// List all containers on the target servers. +/// Response: [ListAllContainersResponse]. +/// +/// Pre v2.3.0, called `ListAllDockerContainers` #[typeshare] #[derive(Serialize, Deserialize, Debug, Clone, Resolve)] #[cfg_attr(feature = "utoipa", derive(utoipa::ToSchema))] #[empty_traits(KomodoReadRequest)] -#[response(ListAllDockerContainersResponse)] +#[response(ListAllContainersResponse)] #[error(mogh_error::Error)] -pub struct ListAllDockerContainers { +pub struct ListAllContainers { /// Filter by server id or name. #[serde(default)] pub servers: Vec, + /// Filter servers by tag. + #[serde(default)] + pub tags: Vec, + /// Filter by container name. + /// Returned containers have names which contain all terms. + #[serde(default)] + pub terms: Vec, + + /// Filter by container state. #[serde(default)] - pub containers: Vec, + pub state: Vec, + + /// Retrieve more results by incrementing the page. + /// `page: 0` is default. + #[serde(default)] + pub page: U64, + + /// Set the limit for number of containers per-page. + /// If not provided, uses the Core config + /// `default_pagination_limit` (default: 30). + /// + /// Passing `limit: 0` returns all results (unlimited). + /// + /// Note: the page logic relies on this being consistent + /// across queries for more pages. + pub limit: Option, + + /// Sort the results by this field. + /// Defaults to Name. + #[serde(default)] + pub sort_by: ContainerSortBy, + + /// Reverse the sort direction. + #[serde(default)] + pub sort_desc: bool, } #[typeshare] -pub type ListAllDockerContainersResponse = Vec; +pub type ListAllContainersResponse = Vec; // #[cfg(feature = "utoipa")] #[utoipa::path( post, - path = "/ListDockerContainers", - description = "List all docker containers on the target server.", - request_body(content = ListDockerContainers), + path = "/ListContainers", + description = "List all containers on the target server.", + request_body(content = ListContainers), responses( - (status = 200, description = "The list of containers", body = ListDockerContainersResponse), + (status = 200, description = "The list of containers", body = ListContainersResponse), ), )] -pub fn list_docker_containers() {} +pub fn list_containers() {} -/// List all docker containers on the target server. -/// Response: [ListDockerContainersResponse]. +/// List all containers on the target server. +/// Response: [ListContainersResponse]. +/// +/// Pre v2.3.0, called `ListDockerContainers` #[typeshare] #[derive(Serialize, Deserialize, Debug, Clone, Resolve)] #[cfg_attr(feature = "utoipa", derive(utoipa::ToSchema))] #[empty_traits(KomodoReadRequest)] -#[response(ListDockerContainersResponse)] +#[response(ListContainersResponse)] #[error(mogh_error::Error)] -pub struct ListDockerContainers { +pub struct ListContainers { /// Id or name #[serde(alias = "id", alias = "name")] pub server: String, } #[typeshare] -pub type ListDockerContainersResponse = Vec; +pub type ListContainersResponse = Vec; // #[cfg(feature = "utoipa")] #[utoipa::path( post, - path = "/InspectDockerContainer", - description = "Inspect a docker container on the server.", - request_body(content = InspectDockerContainer), + path = "/InspectContainer", + description = "Inspect a container on the server.", + request_body(content = InspectContainer), responses( - (status = 200, description = "The container", body = InspectDockerContainerResponse), + (status = 200, description = "The container", body = InspectContainerResponse), ), )] -pub fn inspect_docker_container() {} +pub fn inspect_container() {} -/// Inspect a docker container on the server. Response: [Container]. +/// Inspect a container on the server. Response: [Container]. +/// +/// Pre v2.3.0, called `InspectDockerContainer` #[typeshare] #[derive(Serialize, Deserialize, Debug, Clone, Resolve)] #[cfg_attr(feature = "utoipa", derive(utoipa::ToSchema))] #[empty_traits(KomodoReadRequest)] -#[response(InspectDockerContainerResponse)] +#[response(InspectContainerResponse)] #[error(mogh_error::Error)] -pub struct InspectDockerContainer { +pub struct InspectContainer { /// Id or name #[serde(alias = "id", alias = "name")] pub server: String, @@ -153,7 +197,7 @@ pub struct InspectDockerContainer { } #[typeshare] -pub type InspectDockerContainerResponse = Container; +pub type InspectContainerResponse = Container; // @@ -294,7 +338,7 @@ pub type SearchContainerLogResponse = Log; #[utoipa::path( post, path = "/ListComposeProjects", - description = "List all docker compose projects on the target server.", + description = "List all compose projects on the target server.", request_body(content = ListComposeProjects), responses( (status = 200, description = "The list of compose projects", body = ListComposeProjectsResponse), @@ -302,7 +346,7 @@ pub type SearchContainerLogResponse = Log; )] pub fn list_compose_projects() {} -/// List all docker compose projects on the target server. +/// List all compose projects on the target server. /// Response: [ListComposeProjectsResponse]. #[typeshare] #[derive(Serialize, Deserialize, Debug, Clone, Resolve)] @@ -324,53 +368,57 @@ pub type ListComposeProjectsResponse = Vec; #[cfg(feature = "utoipa")] #[utoipa::path( post, - path = "/ListDockerNetworks", - description = "List the docker networks on the server.", - request_body(content = ListDockerNetworks), + path = "/ListNetworks", + description = "List the container networks on the server.", + request_body(content = ListNetworks), responses( - (status = 200, description = "The list of networks", body = ListDockerNetworksResponse), + (status = 200, description = "The list of networks", body = ListNetworksResponse), ), )] -pub fn list_docker_networks() {} +pub fn list_networks() {} -/// List the docker networks on the server. Response: [ListDockerNetworksResponse]. +/// List the container networks on the server. Response: [ListNetworksResponse]. +/// +/// Pre v2.3.0, called `ListDockerNetworks` #[typeshare] #[derive(Serialize, Deserialize, Debug, Clone, Resolve)] #[cfg_attr(feature = "utoipa", derive(utoipa::ToSchema))] #[empty_traits(KomodoReadRequest)] -#[response(ListDockerNetworksResponse)] +#[response(ListNetworksResponse)] #[error(mogh_error::Error)] -pub struct ListDockerNetworks { +pub struct ListNetworks { /// Id or name #[serde(alias = "id", alias = "name")] pub server: String, } #[typeshare] -pub type ListDockerNetworksResponse = Vec; +pub type ListNetworksResponse = Vec; // #[cfg(feature = "utoipa")] #[utoipa::path( post, - path = "/InspectDockerNetwork", - description = "Inspect a docker network on the server.", - request_body(content = InspectDockerNetwork), + path = "/InspectNetwork", + description = "Inspect a container network on the server.", + request_body(content = InspectNetwork), responses( - (status = 200, description = "The network", body = InspectDockerNetworkResponse), + (status = 200, description = "The network", body = InspectNetworkResponse), ), )] -pub fn inspect_docker_network() {} +pub fn inspect_network() {} -/// Inspect a docker network on the server. Response: [InspectDockerNetworkResponse]. +/// Inspect a container network on the server. Response: [InspectNetworkResponse]. +/// +/// Pre v2.3.0, called `InspectDockerNetwork` #[typeshare] #[derive(Serialize, Deserialize, Debug, Clone, Resolve)] #[cfg_attr(feature = "utoipa", derive(utoipa::ToSchema))] #[empty_traits(KomodoReadRequest)] -#[response(InspectDockerNetworkResponse)] +#[response(InspectNetworkResponse)] #[error(mogh_error::Error)] -pub struct InspectDockerNetwork { +pub struct InspectNetwork { /// Id or name #[serde(alias = "id", alias = "name")] pub server: String, @@ -379,61 +427,65 @@ pub struct InspectDockerNetwork { } #[typeshare] -pub type InspectDockerNetworkResponse = Network; +pub type InspectNetworkResponse = Network; // #[cfg(feature = "utoipa")] #[utoipa::path( post, - path = "/ListDockerImages", - description = "List the docker images locally cached on the target server.", - request_body(content = ListDockerImages), + path = "/ListImages", + description = "List the container images locally cached on the target server.", + request_body(content = ListImages), responses( - (status = 200, description = "The list of images", body = ListDockerImagesResponse), + (status = 200, description = "The list of images", body = ListImagesResponse), ), )] -pub fn list_docker_images() {} +pub fn list_images() {} -/// List the docker images locally cached on the target server. -/// Response: [ListDockerImagesResponse]. +/// List the container images locally cached on the target server. +/// Response: [ListImagesResponse]. +/// +/// Pre v2.3.0, called `ListDockerImages` #[typeshare] #[derive(Serialize, Deserialize, Debug, Clone, Resolve)] #[cfg_attr(feature = "utoipa", derive(utoipa::ToSchema))] #[empty_traits(KomodoReadRequest)] -#[response(ListDockerImagesResponse)] +#[response(ListImagesResponse)] #[error(mogh_error::Error)] -pub struct ListDockerImages { +pub struct ListImages { /// Id or name #[serde(alias = "id", alias = "name")] pub server: String, } #[typeshare] -pub type ListDockerImagesResponse = Vec; +pub type ListImagesResponse = Vec; // #[cfg(feature = "utoipa")] #[utoipa::path( post, - path = "/InspectDockerImage", - description = "Inspect a docker image on the server.", - request_body(content = InspectDockerImage), + path = "/InspectImage", + description = "Inspect a container image on the server.", + request_body(content = InspectImage), responses( - (status = 200, description = "The image", body = InspectDockerImageResponse), + (status = 200, description = "The image", body = InspectImageResponse), ), )] -pub fn inspect_docker_image() {} +pub fn inspect_image() {} -/// Inspect a docker image on the server. Response: [Image]. +/// Inspect a container image on the server. Response: [Image]. +/// +/// Pre v2.3.0, called `InspectDockerImage` #[typeshare] #[derive(Serialize, Deserialize, Debug, Clone, Resolve)] #[cfg_attr(feature = "utoipa", derive(utoipa::ToSchema))] #[empty_traits(KomodoReadRequest)] -#[response(InspectDockerImageResponse)] +#[response(InspectImageResponse)] #[error(mogh_error::Error)] -pub struct InspectDockerImage { +pub struct InspectImage { /// Id or name #[serde(alias = "id", alias = "name")] pub server: String, @@ -442,30 +494,32 @@ pub struct InspectDockerImage { } #[typeshare] -pub type InspectDockerImageResponse = Image; +pub type InspectImageResponse = Image; // #[cfg(feature = "utoipa")] #[utoipa::path( post, - path = "/ListDockerImageHistory", + path = "/ListImageHistory", description = "Get image history from the server.", - request_body(content = ListDockerImageHistory), + request_body(content = ListImageHistory), responses( - (status = 200, description = "The image history", body = ListDockerImageHistoryResponse), + (status = 200, description = "The image history", body = ListImageHistoryResponse), ), )] -pub fn list_docker_image_history() {} +pub fn list_image_history() {} -/// Get image history from the server. Response: [ListDockerImageHistoryResponse]. +/// Get image history from the server. Response: [ListImageHistoryResponse]. +/// +/// Pre v2.3.0, called `ListDockerImageHistory` #[typeshare] #[derive(Serialize, Deserialize, Debug, Clone, Resolve)] #[cfg_attr(feature = "utoipa", derive(utoipa::ToSchema))] #[empty_traits(KomodoReadRequest)] -#[response(ListDockerImageHistoryResponse)] +#[response(ListImageHistoryResponse)] #[error(mogh_error::Error)] -pub struct ListDockerImageHistory { +pub struct ListImageHistory { /// Id or name #[serde(alias = "id", alias = "name")] pub server: String, @@ -474,62 +528,65 @@ pub struct ListDockerImageHistory { } #[typeshare] -pub type ListDockerImageHistoryResponse = - Vec; +pub type ListImageHistoryResponse = Vec; // #[cfg(feature = "utoipa")] #[utoipa::path( post, - path = "/ListDockerVolumes", - description = "List all docker volumes on the target server.", - request_body(content = ListDockerVolumes), + path = "/ListVolumes", + description = "List all container volumes on the target server.", + request_body(content = ListVolumes), responses( - (status = 200, description = "The list of volumes", body = ListDockerVolumesResponse), + (status = 200, description = "The list of volumes", body = ListVolumesResponse), ), )] -pub fn list_docker_volumes() {} +pub fn list_volumes() {} -/// List all docker volumes on the target server. -/// Response: [ListDockerVolumesResponse]. +/// List all container volumes on the target server. +/// Response: [ListVolumesResponse]. +/// +/// Pre v2.3.0, called `ListDockerVolumes` #[typeshare] #[derive(Serialize, Deserialize, Debug, Clone, Resolve)] #[cfg_attr(feature = "utoipa", derive(utoipa::ToSchema))] #[empty_traits(KomodoReadRequest)] -#[response(ListDockerVolumesResponse)] +#[response(ListVolumesResponse)] #[error(mogh_error::Error)] -pub struct ListDockerVolumes { +pub struct ListVolumes { /// Id or name #[serde(alias = "id", alias = "name")] pub server: String, } #[typeshare] -pub type ListDockerVolumesResponse = Vec; +pub type ListVolumesResponse = Vec; // #[cfg(feature = "utoipa")] #[utoipa::path( post, - path = "/InspectDockerVolume", - description = "Inspect a docker volume on the server.", - request_body(content = InspectDockerVolume), + path = "/InspectVolume", + description = "Inspect a container volume on the server.", + request_body(content = InspectVolume), responses( - (status = 200, description = "The volume", body = InspectDockerVolumeResponse), + (status = 200, description = "The volume", body = InspectVolumeResponse), ), )] -pub fn inspect_docker_volume() {} +pub fn inspect_volume() {} -/// Inspect a docker volume on the server. Response: [Volume]. +/// Inspect a container volume on the server. Response: [Volume]. +/// +/// Pre v2.3.0, called `InspectDockerVolume` #[typeshare] #[derive(Serialize, Deserialize, Debug, Clone, Resolve)] #[cfg_attr(feature = "utoipa", derive(utoipa::ToSchema))] #[empty_traits(KomodoReadRequest)] -#[response(InspectDockerVolumeResponse)] +#[response(InspectVolumeResponse)] #[error(mogh_error::Error)] -pub struct InspectDockerVolume { +pub struct InspectVolume { /// Id or name #[serde(alias = "id", alias = "name")] pub server: String, @@ -538,4 +595,4 @@ pub struct InspectDockerVolume { } #[typeshare] -pub type InspectDockerVolumeResponse = Volume; +pub type InspectVolumeResponse = Volume; diff --git a/client/core/rs/src/api/read/mod.rs b/client/core/rs/src/api/read/mod.rs index 888e7ce426..a3b3e335d9 100644 --- a/client/core/rs/src/api/read/mod.rs +++ b/client/core/rs/src/api/read/mod.rs @@ -53,8 +53,8 @@ pub use user_group::*; pub use variable::*; use crate::entities::{ - ResourceTarget, Timelength, - config::{DockerRegistry, GitProvider}, + ResourceTarget, Timelength, U64, + config::{GitProvider, ImageRegistry}, }; #[cfg(feature = "utoipa")] @@ -146,6 +146,8 @@ pub struct GetCoreInfoResponse { pub timezone: String, /// Public key for Core / Periphery authentication. pub public_key: String, + /// Default pagination limit for the UI to use. + pub default_pagination_limit: U64, } // @@ -191,38 +193,40 @@ pub type ListGitProvidersFromConfigResponse = Vec; #[cfg(feature = "utoipa")] #[utoipa::path( post, - path = "/ListDockerRegistriesFromConfig", - description = "List the docker registry providers available in Core / Periphery config files.", - request_body(content = ListDockerRegistriesFromConfig), + path = "/ListImageRegistriesFromConfig", + description = "List the image registry providers available in Core / Periphery config files.", + request_body(content = ListImageRegistriesFromConfig), responses( - (status = 200, description = "The available docker registries", body = ListDockerRegistriesFromConfigResponse), + (status = 200, description = "The available image registries", body = ListImageRegistriesFromConfigResponse), (status = 400, description = "Target must be `Server` or `Builder`", body = mogh_error::Serror), (status = 500, description = "Failed", body = mogh_error::Serror), ), )] -pub fn list_docker_registries_from_config() {} +pub fn list_image_registries_from_config() {} -/// List the docker registry providers available in Core / Periphery config files. -/// Response: [ListDockerRegistriesFromConfigResponse]. +/// List the image registry providers available in Core / Periphery config files. +/// Response: [ListImageRegistriesFromConfigResponse]. /// /// Includes: /// - registries in core config /// - registries configured on builds, deployments /// - registries on the optional Server or Builder +/// +/// Pre v2.3.0, called `ListDockerRegistriesFromConfig` #[typeshare] #[derive(Serialize, Deserialize, Debug, Clone, Resolve)] #[cfg_attr(feature = "utoipa", derive(utoipa::ToSchema))] #[empty_traits(KomodoReadRequest)] -#[response(ListDockerRegistriesFromConfigResponse)] +#[response(ListImageRegistriesFromConfigResponse)] #[error(mogh_error::Error)] -pub struct ListDockerRegistriesFromConfig { +pub struct ListImageRegistriesFromConfig { /// Accepts an optional Server or Builder target to expand the core list with /// providers available on that specific resource. pub target: Option, } #[typeshare] -pub type ListDockerRegistriesFromConfigResponse = Vec; +pub type ListImageRegistriesFromConfigResponse = Vec; // diff --git a/client/core/rs/src/api/read/openapi.rs b/client/core/rs/src/api/read/openapi.rs index 4601da1961..5f469aacc8 100644 --- a/client/core/rs/src/api/read/openapi.rs +++ b/client/core/rs/src/api/read/openapi.rs @@ -15,9 +15,9 @@ use crate::api::read; read::get_git_provider_account, read::list_git_provider_accounts, read::list_git_providers_from_config, - read::get_docker_registry_account, - read::list_docker_registry_accounts, - read::list_docker_registries_from_config, + read::get_image_registry_account, + read::list_image_registry_accounts, + read::list_image_registries_from_config, // swarm read::list_swarms, read::list_full_swarms, @@ -59,6 +59,7 @@ use crate::api::read; read::get_stack_action_state, read::get_stacks_summary, read::list_stack_services, + read::list_all_stack_services, read::inspect_stack_container, read::inspect_stack_swarm_service, read::inspect_stack_swarm_info, @@ -125,22 +126,22 @@ use crate::api::read; // variable read::list_variables, read::get_variable, - // docker - read::get_docker_containers_summary, - read::list_all_docker_containers, - read::list_docker_containers, - read::inspect_docker_container, + // container + read::get_containers_summary, + read::list_all_containers, + read::list_containers, + read::inspect_container, read::get_resource_matching_container, read::get_container_log, read::search_container_log, read::list_compose_projects, - read::list_docker_networks, - read::inspect_docker_network, - read::list_docker_images, - read::inspect_docker_image, - read::list_docker_image_history, - read::list_docker_volumes, - read::inspect_docker_volume, + read::list_networks, + read::inspect_network, + read::list_images, + read::inspect_image, + read::list_image_history, + read::list_volumes, + read::inspect_volume, // toml read::export_all_resources_to_toml, read::export_resources_to_toml, diff --git a/client/core/rs/src/api/read/procedure.rs b/client/core/rs/src/api/read/procedure.rs index bd0b74b067..2e031950cd 100644 --- a/client/core/rs/src/api/read/procedure.rs +++ b/client/core/rs/src/api/read/procedure.rs @@ -2,8 +2,12 @@ use mogh_resolver::Resolve; use serde::{Deserialize, Serialize}; use typeshare::typeshare; -use crate::entities::procedure::{ - Procedure, ProcedureActionState, ProcedureListItem, ProcedureQuery, +use crate::entities::{ + U64, + procedure::{ + Procedure, ProcedureActionState, ProcedureListItem, + ProcedureQuery, ProcedureSortBy, + }, }; use super::KomodoReadRequest; @@ -63,6 +67,31 @@ pub struct ListProcedures { /// optional structured query to filter procedures. #[serde(default)] pub query: ProcedureQuery, + + /// Retrieve more results by incrementing the page. + /// `page: 0` is default. + #[serde(default)] + pub page: U64, + + /// Set the limit for number of resources per-page. + /// If not provided, uses the Core config + /// `default_pagination_limit` (default: 30). + /// + /// Passing `limit: 0` returns all results (unlimited). + /// + /// Note: the page logic relies on this being consistent + /// across queries for more pages. + pub limit: Option, + + /// Sort the results by this field. + /// Defaults to Name. Non-Name sorts are applied in memory + /// after querying all matching resources. + #[serde(default)] + pub sort_by: ProcedureSortBy, + + /// Reverse the sort direction. + #[serde(default)] + pub sort_desc: bool, } #[typeshare] @@ -93,6 +122,21 @@ pub struct ListFullProcedures { /// optional structured query to filter procedures. #[serde(default)] pub query: ProcedureQuery, + + /// Retrieve more results by incrementing the page. + /// `page: 0` is default. + #[serde(default)] + pub page: U64, + + /// Set the limit for number of resources per-page. + /// If not provided, uses the Core config + /// `default_pagination_limit` (default: 30). + /// + /// Passing `limit: 0` returns all results (unlimited). + /// + /// Note: the page logic relies on this being consistent + /// across queries for more pages. + pub limit: Option, } #[typeshare] diff --git a/client/core/rs/src/api/read/provider.rs b/client/core/rs/src/api/read/provider.rs index e84df7db68..d45d876a84 100644 --- a/client/core/rs/src/api/read/provider.rs +++ b/client/core/rs/src/api/read/provider.rs @@ -3,7 +3,7 @@ use serde::{Deserialize, Serialize}; use typeshare::typeshare; use crate::entities::provider::{ - DockerRegistryAccount, GitProviderAccount, + GitProviderAccount, ImageRegistryAccount, }; use super::KomodoReadRequest; @@ -74,53 +74,57 @@ pub type ListGitProviderAccountsResponse = Vec; #[cfg(feature = "utoipa")] #[utoipa::path( post, - path = "/GetDockerRegistryAccount", - description = "Get a specific docker registry account.", - request_body(content = GetDockerRegistryAccount), + path = "/GetImageRegistryAccount", + description = "Get a specific image registry account.", + request_body(content = GetImageRegistryAccount), responses( - (status = 200, description = "The docker registry account", body = GetDockerRegistryAccountResponse), + (status = 200, description = "The image registry account", body = GetImageRegistryAccountResponse), ), )] -pub fn get_docker_registry_account() {} +pub fn get_image_registry_account() {} -/// Get a specific docker registry account. -/// Response: [GetDockerRegistryAccountResponse]. +/// Get a specific image registry account. +/// Response: [GetImageRegistryAccountResponse]. +/// +/// Pre v2.3.0, called `GetDockerRegistryAccount` #[typeshare] #[derive(Serialize, Deserialize, Debug, Clone, Resolve)] #[cfg_attr(feature = "utoipa", derive(utoipa::ToSchema))] #[empty_traits(KomodoReadRequest)] -#[response(GetDockerRegistryAccountResponse)] +#[response(GetImageRegistryAccountResponse)] #[error(mogh_error::Error)] -pub struct GetDockerRegistryAccount { +pub struct GetImageRegistryAccount { pub id: String, } #[typeshare] -pub type GetDockerRegistryAccountResponse = DockerRegistryAccount; +pub type GetImageRegistryAccountResponse = ImageRegistryAccount; // #[cfg(feature = "utoipa")] #[utoipa::path( post, - path = "/ListDockerRegistryAccounts", - description = "List docker registry accounts matching optional query.", - request_body(content = ListDockerRegistryAccounts), + path = "/ListImageRegistryAccounts", + description = "List image registry accounts matching optional query.", + request_body(content = ListImageRegistryAccounts), responses( - (status = 200, description = "The list of docker registry accounts", body = ListDockerRegistryAccountsResponse), + (status = 200, description = "The list of image registry accounts", body = ListImageRegistryAccountsResponse), ), )] -pub fn list_docker_registry_accounts() {} +pub fn list_image_registry_accounts() {} -/// List docker registry accounts matching optional query. -/// Response: [ListDockerRegistryAccountsResponse]. +/// List image registry accounts matching optional query. +/// Response: [ListImageRegistryAccountsResponse]. +/// +/// Pre v2.3.0, called `ListDockerRegistryAccounts` #[typeshare] #[derive(Serialize, Deserialize, Debug, Clone, Default, Resolve)] #[cfg_attr(feature = "utoipa", derive(utoipa::ToSchema))] #[empty_traits(KomodoReadRequest)] -#[response(ListDockerRegistryAccountsResponse)] +#[response(ListImageRegistryAccountsResponse)] #[error(mogh_error::Error)] -pub struct ListDockerRegistryAccounts { +pub struct ListImageRegistryAccounts { /// Optionally filter by accounts with a specific domain. pub domain: Option, /// Optionally filter by accounts with a specific username. @@ -128,5 +132,5 @@ pub struct ListDockerRegistryAccounts { } #[typeshare] -pub type ListDockerRegistryAccountsResponse = - Vec; +pub type ListImageRegistryAccountsResponse = + Vec; diff --git a/client/core/rs/src/api/read/repo.rs b/client/core/rs/src/api/read/repo.rs index be576ad240..3b9db0290a 100644 --- a/client/core/rs/src/api/read/repo.rs +++ b/client/core/rs/src/api/read/repo.rs @@ -2,8 +2,11 @@ use mogh_resolver::Resolve; use serde::{Deserialize, Serialize}; use typeshare::typeshare; -use crate::entities::repo::{ - Repo, RepoActionState, RepoListItem, RepoQuery, +use crate::entities::{ + U64, + repo::{ + Repo, RepoActionState, RepoListItem, RepoQuery, RepoSortBy, + }, }; use super::KomodoReadRequest; @@ -63,6 +66,31 @@ pub struct ListRepos { /// optional structured query to filter repos. #[serde(default)] pub query: RepoQuery, + + /// Retrieve more results by incrementing the page. + /// `page: 0` is default. + #[serde(default)] + pub page: U64, + + /// Set the limit for number of resources per-page. + /// If not provided, uses the Core config + /// `default_pagination_limit` (default: 30). + /// + /// Passing `limit: 0` returns all results (unlimited). + /// + /// Note: the page logic relies on this being consistent + /// across queries for more pages. + pub limit: Option, + + /// Sort the results by this field. + /// Defaults to Name. Non-Name sorts are applied in memory + /// after querying all matching resources. + #[serde(default)] + pub sort_by: RepoSortBy, + + /// Reverse the sort direction. + #[serde(default)] + pub sort_desc: bool, } #[typeshare] @@ -93,6 +121,21 @@ pub struct ListFullRepos { /// optional structured query to filter repos. #[serde(default)] pub query: RepoQuery, + + /// Retrieve more results by incrementing the page. + /// `page: 0` is default. + #[serde(default)] + pub page: U64, + + /// Set the limit for number of resources per-page. + /// If not provided, uses the Core config + /// `default_pagination_limit` (default: 30). + /// + /// Passing `limit: 0` returns all results (unlimited). + /// + /// Note: the page logic relies on this being consistent + /// across queries for more pages. + pub limit: Option, } #[typeshare] diff --git a/client/core/rs/src/api/read/schedule.rs b/client/core/rs/src/api/read/schedule.rs index 5f1c2bb21d..fdefe22ae2 100644 --- a/client/core/rs/src/api/read/schedule.rs +++ b/client/core/rs/src/api/read/schedule.rs @@ -4,7 +4,11 @@ use typeshare::typeshare; use crate::{ deserializers::string_list_deserializer, - entities::{resource::TagQueryBehavior, schedule::Schedule}, + entities::{ + U64, + resource::TagQueryBehavior, + schedule::{Schedule, ScheduleSortBy}, + }, }; use super::KomodoReadRequest; @@ -26,7 +30,7 @@ pub fn list_schedules() {} /// List configured schedules. /// Response: [ListSchedulesResponse]. #[typeshare] -#[derive(Serialize, Deserialize, Debug, Clone, Resolve)] +#[derive(Debug, Clone, Default, Serialize, Deserialize, Resolve)] #[cfg_attr(feature = "utoipa", derive(utoipa::ToSchema))] #[empty_traits(KomodoReadRequest)] #[response(ListSchedulesResponse)] @@ -38,6 +42,35 @@ pub struct ListSchedules { /// 'All' or 'Any' #[serde(default)] pub tag_behavior: TagQueryBehavior, + + /// Filter by target name. + /// Returned schedules have names which contain all terms. + #[serde(default)] + pub terms: Vec, + + /// Retrieve more results by incrementing the page. + /// `page: 0` is default. + #[serde(default)] + pub page: U64, + + /// Set the limit for number of schedules per-page. + /// If not provided, uses the Core config + /// `default_pagination_limit` (default: 30). + /// + /// Passing `limit: 0` returns all results (unlimited). + /// + /// Note: the page logic relies on this being consistent + /// across queries for more pages. + pub limit: Option, + + /// Sort the results by this field. + /// Defaults to Name. + #[serde(default)] + pub sort_by: ScheduleSortBy, + + /// Reverse the sort direction. + #[serde(default)] + pub sort_desc: bool, } #[typeshare] diff --git a/client/core/rs/src/api/read/server.rs b/client/core/rs/src/api/read/server.rs index fa4ae67b59..d302240f8a 100644 --- a/client/core/rs/src/api/read/server.rs +++ b/client/core/rs/src/api/read/server.rs @@ -3,10 +3,10 @@ use serde::{Deserialize, Serialize}; use typeshare::typeshare; use crate::entities::{ - I64, Timelength, + I64, Timelength, U64, server::{ PeripheryInformation, Server, ServerActionState, ServerListItem, - ServerQuery, ServerState, + ServerQuery, ServerSortBy, ServerState, }, stats::{ SystemInformation, SystemProcess, SystemStats, SystemStatsRecord, @@ -70,6 +70,31 @@ pub struct ListServers { /// optional structured query to filter servers. #[serde(default)] pub query: ServerQuery, + + /// Retrieve more results by incrementing the page. + /// `page: 0` is default. + #[serde(default)] + pub page: U64, + + /// Set the limit for number of resources per-page. + /// If not provided, uses the Core config + /// `default_pagination_limit` (default: 30). + /// + /// Passing `limit: 0` returns all results (unlimited). + /// + /// Note: the page logic relies on this being consistent + /// across queries for more pages. + pub limit: Option, + + /// Sort the results by this field. + /// Defaults to Name. Non-Name sorts are applied in memory + /// after querying all matching resources. + #[serde(default)] + pub sort_by: ServerSortBy, + + /// Reverse the sort direction. + #[serde(default)] + pub sort_desc: bool, } #[typeshare] @@ -100,6 +125,21 @@ pub struct ListFullServers { /// optional structured query to filter servers. #[serde(default)] pub query: ServerQuery, + + /// Retrieve more results by incrementing the page. + /// `page: 0` is default. + #[serde(default)] + pub page: U64, + + /// Set the limit for number of resources per-page. + /// If not provided, uses the Core config + /// `default_pagination_limit` (default: 30). + /// + /// Passing `limit: 0` returns all results (unlimited). + /// + /// Note: the page logic relies on this being consistent + /// across queries for more pages. + pub limit: Option, } #[typeshare] diff --git a/client/core/rs/src/api/read/stack.rs b/client/core/rs/src/api/read/stack.rs index 15e70f2e3d..83d72fd457 100644 --- a/client/core/rs/src/api/read/stack.rs +++ b/client/core/rs/src/api/read/stack.rs @@ -9,6 +9,7 @@ use crate::entities::{ }, stack::{ Stack, StackActionState, StackListItem, StackQuery, StackService, + StackServiceState, StackSortBy, }, update::Log, }; @@ -47,6 +48,106 @@ pub type GetStackResponse = Stack; // +#[cfg(feature = "utoipa")] +#[utoipa::path( + post, + path = "/ListStacks", + description = "List stacks matching optional query.", + request_body(content = ListStacks), + responses( + (status = 200, description = "The list of stacks", body = ListStacksResponse), + ), +)] +pub fn list_stacks() {} + +/// List stacks matching optional query. Response: [ListStacksResponse]. +#[typeshare] +#[derive(Serialize, Deserialize, Debug, Clone, Default, Resolve)] +#[cfg_attr(feature = "utoipa", derive(utoipa::ToSchema))] +#[empty_traits(KomodoReadRequest)] +#[response(ListStacksResponse)] +#[error(mogh_error::Error)] +pub struct ListStacks { + /// optional structured query to filter stacks. + #[serde(default)] + pub query: StackQuery, + + /// Retrieve more results by incrementing the page. + /// `page: 0` is default. + #[serde(default)] + pub page: U64, + + /// Set the limit for number of resources per-page. + /// If not provided, uses the Core config + /// `default_pagination_limit` (default: 30). + /// + /// Passing `limit: 0` returns all results (unlimited). + /// + /// Note: the page logic relies on this being consistent + /// across queries for more pages. + pub limit: Option, + + /// Sort the results by this field. + /// Defaults to Name. Non-Name sorts are applied in memory + /// after querying all matching resources. + #[serde(default)] + pub sort_by: StackSortBy, + + /// Reverse the sort direction. + #[serde(default)] + pub sort_desc: bool, +} + +#[typeshare] +pub type ListStacksResponse = Vec; + +// + +#[cfg(feature = "utoipa")] +#[utoipa::path( + post, + path = "/ListFullStacks", + description = "List stacks matching optional query.", + request_body(content = ListFullStacks), + responses( + (status = 200, description = "The list of stacks", body = ListFullStacksResponse), + ), +)] +pub fn list_full_stacks() {} + +/// List stacks matching optional query. Response: [ListFullStacksResponse]. +#[typeshare] +#[derive(Serialize, Deserialize, Debug, Clone, Default, Resolve)] +#[cfg_attr(feature = "utoipa", derive(utoipa::ToSchema))] +#[empty_traits(KomodoReadRequest)] +#[response(ListFullStacksResponse)] +#[error(mogh_error::Error)] +pub struct ListFullStacks { + /// optional structured query to filter stacks. + #[serde(default)] + pub query: StackQuery, + + /// Retrieve more results by incrementing the page. + /// `page: 0` is default. + #[serde(default)] + pub page: U64, + + /// Set the limit for number of resources per-page. + /// If not provided, uses the Core config + /// `default_pagination_limit` (default: 30). + /// + /// Passing `limit: 0` returns all results (unlimited). + /// + /// Note: the page logic relies on this being consistent + /// across queries for more pages. + pub limit: Option, +} + +#[typeshare] +pub type ListFullStacksResponse = Vec; + +// + #[cfg(feature = "utoipa")] #[utoipa::path( post, @@ -77,6 +178,65 @@ pub type ListStackServicesResponse = Vec; // +#[cfg(feature = "utoipa")] +#[utoipa::path( + post, + path = "/ListAllStackServices", + description = "List all stack services on the target servers / stacks.", + request_body(content = ListAllStackServices), + responses( + (status = 200, description = "The list of stack services", body = ListAllStackServicesResponse), + ), +)] +pub fn list_all_stack_services() {} + +/// List all stack services part of the target stacks. +/// Response: [ListStackServicesResponse]. +#[typeshare] +#[derive(Serialize, Deserialize, Debug, Clone, Resolve)] +#[cfg_attr(feature = "utoipa", derive(utoipa::ToSchema))] +#[empty_traits(KomodoReadRequest)] +#[response(ListAllStackServicesResponse)] +#[error(mogh_error::Error)] +pub struct ListAllStackServices { + /// Filter by stack name. + #[serde(default)] + pub stacks: Vec, + + /// Filter stacks by tag. + #[serde(default)] + pub tags: Vec, + + /// Filter by service name. + /// Returned services have names which contain all terms. + #[serde(default)] + pub terms: Vec, + + /// Filter by service state. + #[serde(default)] + pub state: Vec, + + /// Retrieve more results by incrementing the page. + /// `page: 0` is default. + #[serde(default)] + pub page: U64, + + /// Set the limit for number of services per-page. + /// If not provided, uses the Core config + /// `default_pagination_limit` (default: 30). + /// + /// Passing `limit: 0` returns all results (unlimited). + /// + /// Note: the page logic relies on this being consistent + /// across queries for more pages. + pub limit: Option, +} + +#[typeshare] +pub type ListAllStackServicesResponse = Vec; + +// + #[cfg(feature = "utoipa")] #[utoipa::path( post, @@ -333,66 +493,6 @@ pub type ListCommonStackBuildExtraArgsResponse = Vec; // -#[cfg(feature = "utoipa")] -#[utoipa::path( - post, - path = "/ListStacks", - description = "List stacks matching optional query.", - request_body(content = ListStacks), - responses( - (status = 200, description = "The list of stacks", body = ListStacksResponse), - ), -)] -pub fn list_stacks() {} - -/// List stacks matching optional query. Response: [ListStacksResponse]. -#[typeshare] -#[derive(Serialize, Deserialize, Debug, Clone, Default, Resolve)] -#[cfg_attr(feature = "utoipa", derive(utoipa::ToSchema))] -#[empty_traits(KomodoReadRequest)] -#[response(ListStacksResponse)] -#[error(mogh_error::Error)] -pub struct ListStacks { - /// optional structured query to filter stacks. - #[serde(default)] - pub query: StackQuery, -} - -#[typeshare] -pub type ListStacksResponse = Vec; - -// - -#[cfg(feature = "utoipa")] -#[utoipa::path( - post, - path = "/ListFullStacks", - description = "List stacks matching optional query.", - request_body(content = ListFullStacks), - responses( - (status = 200, description = "The list of stacks", body = ListFullStacksResponse), - ), -)] -pub fn list_full_stacks() {} - -/// List stacks matching optional query. Response: [ListFullStacksResponse]. -#[typeshare] -#[derive(Serialize, Deserialize, Debug, Clone, Default, Resolve)] -#[cfg_attr(feature = "utoipa", derive(utoipa::ToSchema))] -#[empty_traits(KomodoReadRequest)] -#[response(ListFullStacksResponse)] -#[error(mogh_error::Error)] -pub struct ListFullStacks { - /// optional structured query to filter stacks. - #[serde(default)] - pub query: StackQuery, -} - -#[typeshare] -pub type ListFullStacksResponse = Vec; - -// - #[cfg(feature = "utoipa")] #[utoipa::path( post, diff --git a/client/core/rs/src/api/read/swarm.rs b/client/core/rs/src/api/read/swarm.rs index 6e9cf8cbae..eb71c764ef 100644 --- a/client/core/rs/src/api/read/swarm.rs +++ b/client/core/rs/src/api/read/swarm.rs @@ -14,7 +14,9 @@ use crate::entities::{ swarm::SwarmInspectInfo, task::{SwarmTask, SwarmTaskListItem}, }, - swarm::{Swarm, SwarmActionState, SwarmListItem, SwarmQuery}, + swarm::{ + Swarm, SwarmActionState, SwarmListItem, SwarmQuery, SwarmSortBy, + }, update::Log, }; @@ -75,6 +77,31 @@ pub struct ListSwarms { /// Optional structured query to filter Swarms. #[serde(default)] pub query: SwarmQuery, + + /// Retrieve more results by incrementing the page. + /// `page: 0` is default. + #[serde(default)] + pub page: U64, + + /// Set the limit for number of resources per-page. + /// If not provided, uses the Core config + /// `default_pagination_limit` (default: 30). + /// + /// Passing `limit: 0` returns all results (unlimited). + /// + /// Note: the page logic relies on this being consistent + /// across queries for more pages. + pub limit: Option, + + /// Sort the results by this field. + /// Defaults to Name. Non-Name sorts are applied in memory + /// after querying all matching resources. + #[serde(default)] + pub sort_by: SwarmSortBy, + + /// Reverse the sort direction. + #[serde(default)] + pub sort_desc: bool, } #[typeshare] @@ -105,6 +132,21 @@ pub struct ListFullSwarms { /// optional structured query to filter swarms. #[serde(default)] pub query: SwarmQuery, + + /// Retrieve more results by incrementing the page. + /// `page: 0` is default. + #[serde(default)] + pub page: U64, + + /// Set the limit for number of resources per-page. + /// If not provided, uses the Core config + /// `default_pagination_limit` (default: 30). + /// + /// Passing `limit: 0` returns all results (unlimited). + /// + /// Note: the page logic relies on this being consistent + /// across queries for more pages. + pub limit: Option, } #[typeshare] diff --git a/client/core/rs/src/api/read/sync.rs b/client/core/rs/src/api/read/sync.rs index afaccdcdab..ebd60848a6 100644 --- a/client/core/rs/src/api/read/sync.rs +++ b/client/core/rs/src/api/read/sync.rs @@ -2,9 +2,12 @@ use mogh_resolver::Resolve; use serde::{Deserialize, Serialize}; use typeshare::typeshare; -use crate::entities::sync::{ - ResourceSync, ResourceSyncActionState, ResourceSyncListItem, - ResourceSyncQuery, +use crate::entities::{ + U64, + sync::{ + ResourceSync, ResourceSyncActionState, ResourceSyncListItem, + ResourceSyncQuery, ResourceSyncSortBy, + }, }; use super::KomodoReadRequest; @@ -64,6 +67,31 @@ pub struct ListResourceSyncs { /// optional structured query to filter syncs. #[serde(default)] pub query: ResourceSyncQuery, + + /// Retrieve more results by incrementing the page. + /// `page: 0` is default. + #[serde(default)] + pub page: U64, + + /// Set the limit for number of resources per-page. + /// If not provided, uses the Core config + /// `default_pagination_limit` (default: 30). + /// + /// Passing `limit: 0` returns all results (unlimited). + /// + /// Note: the page logic relies on this being consistent + /// across queries for more pages. + pub limit: Option, + + /// Sort the results by this field. + /// Defaults to Name. Non-Name sorts are applied in memory + /// after querying all matching resources. + #[serde(default)] + pub sort_by: ResourceSyncSortBy, + + /// Reverse the sort direction. + #[serde(default)] + pub sort_desc: bool, } #[typeshare] @@ -94,6 +122,21 @@ pub struct ListFullResourceSyncs { /// optional structured query to filter syncs. #[serde(default)] pub query: ResourceSyncQuery, + + /// Retrieve more results by incrementing the page. + /// `page: 0` is default. + #[serde(default)] + pub page: U64, + + /// Set the limit for number of resources per-page. + /// If not provided, uses the Core config + /// `default_pagination_limit` (default: 30). + /// + /// Passing `limit: 0` returns all results (unlimited). + /// + /// Note: the page logic relies on this being consistent + /// across queries for more pages. + pub limit: Option, } #[typeshare] diff --git a/client/core/rs/src/api/read/terminal.rs b/client/core/rs/src/api/read/terminal.rs index c2d4f5fd87..8bde511a1e 100644 --- a/client/core/rs/src/api/read/terminal.rs +++ b/client/core/rs/src/api/read/terminal.rs @@ -2,7 +2,10 @@ use mogh_resolver::Resolve; use serde::{Deserialize, Serialize}; use typeshare::typeshare; -use crate::entities::terminal::{Terminal, TerminalTarget}; +use crate::entities::{ + U64, + terminal::{Terminal, TerminalSortBy, TerminalTarget}, +}; use super::KomodoReadRequest; @@ -34,6 +37,35 @@ pub struct ListTerminals { /// Return results with resource names instead of ids. #[serde(default)] pub use_names: bool, + + /// Filter by terminal name. + /// Returned terminals have names which contain all terms. + #[serde(default)] + pub terms: Vec, + + /// Retrieve more results by incrementing the page. + /// `page: 0` is default. + #[serde(default)] + pub page: U64, + + /// Set the limit for number of terminals per-page. + /// If not provided, uses the Core config + /// `default_pagination_limit` (default: 30). + /// + /// Passing `limit: 0` returns all results (unlimited). + /// + /// Note: the page logic relies on this being consistent + /// across queries for more pages. + pub limit: Option, + + /// Sort the results by this field. + /// Defaults to Name. + #[serde(default)] + pub sort_by: TerminalSortBy, + + /// Reverse the sort direction. + #[serde(default)] + pub sort_desc: bool, } #[typeshare] diff --git a/client/core/rs/src/api/write/deployment.rs b/client/core/rs/src/api/write/deployment.rs index 8c62881b48..3f6935a84a 100644 --- a/client/core/rs/src/api/write/deployment.rs +++ b/client/core/rs/src/api/write/deployment.rs @@ -277,6 +277,10 @@ pub struct BatchCheckDeploymentForUpdate { /// extra-deployment-1, extra-deployment-2 /// ``` pub pattern: String, + /// Filter matches by tag. + /// If empty, skips tag filtering. + #[serde(default)] + pub tags: Vec, /// Normally resources with 'auto_update' will be /// redeployed immediately if updates are found. /// With this enabled, convert this into an UpdateAvailable alert. diff --git a/client/core/rs/src/api/write/openapi.rs b/client/core/rs/src/api/write/openapi.rs index 542a80aecb..c6861669d7 100644 --- a/client/core/rs/src/api/write/openapi.rs +++ b/client/core/rs/src/api/write/openapi.rs @@ -136,9 +136,9 @@ use crate::api::write; write::create_git_provider_account, write::update_git_provider_account, write::delete_git_provider_account, - write::create_docker_registry_account, - write::update_docker_registry_account, - write::delete_docker_registry_account, + write::create_image_registry_account, + write::update_image_registry_account, + write::delete_image_registry_account, ), )] pub struct KomodoWriteApi; diff --git a/client/core/rs/src/api/write/provider.rs b/client/core/rs/src/api/write/provider.rs index 7b661929ad..54980eb85f 100644 --- a/client/core/rs/src/api/write/provider.rs +++ b/client/core/rs/src/api/write/provider.rs @@ -104,88 +104,94 @@ pub type DeleteGitProviderAccountResponse = GitProviderAccount; #[cfg(feature = "utoipa")] #[utoipa::path( post, - path = "/CreateDockerRegistryAccount", - description = "**Admin only.** Create a docker registry account.", - request_body(content = CreateDockerRegistryAccount), + path = "/CreateImageRegistryAccount", + description = "**Admin only.** Create an image registry account.", + request_body(content = CreateImageRegistryAccount), responses( - (status = 200, description = "The created account", body = CreateDockerRegistryAccountResponse), + (status = 200, description = "The created account", body = CreateImageRegistryAccountResponse), ), )] -pub fn create_docker_registry_account() {} +pub fn create_image_registry_account() {} -/// **Admin only.** Create a docker registry account. -/// Response: [DockerRegistryAccount]. +/// **Admin only.** Create an image registry account. +/// Response: [ImageRegistryAccount]. +/// +/// Pre v2.3.0, called `CreateDockerRegistryAccount` #[typeshare] #[derive(Serialize, Deserialize, Debug, Clone, Resolve)] #[cfg_attr(feature = "utoipa", derive(utoipa::ToSchema))] #[empty_traits(KomodoWriteRequest)] -#[response(CreateDockerRegistryAccountResponse)] +#[response(CreateImageRegistryAccountResponse)] #[error(mogh_error::Error)] -pub struct CreateDockerRegistryAccount { - pub account: _PartialDockerRegistryAccount, +pub struct CreateImageRegistryAccount { + pub account: _PartialImageRegistryAccount, } #[typeshare] -pub type CreateDockerRegistryAccountResponse = DockerRegistryAccount; +pub type CreateImageRegistryAccountResponse = ImageRegistryAccount; // #[cfg(feature = "utoipa")] #[utoipa::path( post, - path = "/UpdateDockerRegistryAccount", - description = "**Admin only.** Update a docker registry account.", - request_body(content = UpdateDockerRegistryAccount), + path = "/UpdateImageRegistryAccount", + description = "**Admin only.** Update an image registry account.", + request_body(content = UpdateImageRegistryAccount), responses( - (status = 200, description = "The updated account", body = UpdateDockerRegistryAccountResponse), + (status = 200, description = "The updated account", body = UpdateImageRegistryAccountResponse), ), )] -pub fn update_docker_registry_account() {} +pub fn update_image_registry_account() {} -/// **Admin only.** Update a docker registry account. -/// Response: [DockerRegistryAccount]. +/// **Admin only.** Update a image registry account. +/// Response: [ImageRegistryAccount]. +/// +/// Pre v2.3.0, called `UpdateDockerRegistryAccount` #[typeshare] #[derive(Serialize, Deserialize, Debug, Clone, Resolve)] #[cfg_attr(feature = "utoipa", derive(utoipa::ToSchema))] #[empty_traits(KomodoWriteRequest)] -#[response(UpdateDockerRegistryAccountResponse)] +#[response(UpdateImageRegistryAccountResponse)] #[error(mogh_error::Error)] -pub struct UpdateDockerRegistryAccount { - /// The id of the docker registry to update +pub struct UpdateImageRegistryAccount { + /// The id of the image registry to update pub id: String, - /// The partial docker registry account. - pub account: _PartialDockerRegistryAccount, + /// The partial image registry account. + pub account: _PartialImageRegistryAccount, } #[typeshare] -pub type UpdateDockerRegistryAccountResponse = DockerRegistryAccount; +pub type UpdateImageRegistryAccountResponse = ImageRegistryAccount; // #[cfg(feature = "utoipa")] #[utoipa::path( post, - path = "/DeleteDockerRegistryAccount", - description = "**Admin only.** Delete a docker registry account.", - request_body(content = DeleteDockerRegistryAccount), + path = "/DeleteImageRegistryAccount", + description = "**Admin only.** Delete an image registry account.", + request_body(content = DeleteImageRegistryAccount), responses( - (status = 200, description = "The deleted account", body = DeleteDockerRegistryAccountResponse), + (status = 200, description = "The deleted account", body = DeleteImageRegistryAccountResponse), ), )] -pub fn delete_docker_registry_account() {} +pub fn delete_image_registry_account() {} -/// **Admin only.** Delete a docker registry account. -/// Response: [DockerRegistryAccount]. +/// **Admin only.** Delete an image registry account. +/// Response: [ImageRegistryAccount]. +/// +/// Pre v2.3.0, called `DeleteDockerRegistryAccount` #[typeshare] #[derive(Serialize, Deserialize, Debug, Clone, Resolve)] #[cfg_attr(feature = "utoipa", derive(utoipa::ToSchema))] #[empty_traits(KomodoWriteRequest)] -#[response(DeleteDockerRegistryAccountResponse)] +#[response(DeleteImageRegistryAccountResponse)] #[error(mogh_error::Error)] -pub struct DeleteDockerRegistryAccount { - /// The id of the docker registry account to delete +pub struct DeleteImageRegistryAccount { + /// The id of the image registry account to delete pub id: String, } #[typeshare] -pub type DeleteDockerRegistryAccountResponse = DockerRegistryAccount; +pub type DeleteImageRegistryAccountResponse = ImageRegistryAccount; diff --git a/client/core/rs/src/api/write/stack.rs b/client/core/rs/src/api/write/stack.rs index 8dc44cb0c4..5d249d3827 100644 --- a/client/core/rs/src/api/write/stack.rs +++ b/client/core/rs/src/api/write/stack.rs @@ -310,6 +310,10 @@ pub struct BatchCheckStackForUpdate { /// extra-stack-1, extra-stack-2 /// ``` pub pattern: String, + /// Filter matches by tag. + /// If empty, skips tag filtering. + #[serde(default)] + pub tags: Vec, /// Normally resources with 'auto_update' will be /// redeployed immediately if updates are found. /// With this enabled, convert this into an UpdateAvailable alert. diff --git a/client/core/rs/src/busy.rs b/client/core/rs/src/busy.rs index f30b3295b8..5ebf5dd794 100644 --- a/client/core/rs/src/busy.rs +++ b/client/core/rs/src/busy.rs @@ -22,11 +22,12 @@ impl Busy for ServerActionState { || self.pruning_images || self.pruning_networks || self.pruning_volumes - || self.starting_containers - || self.restarting_containers - || self.pausing_containers - || self.unpausing_containers - || self.stopping_containers + || self.starting_containers > 0 + || self.restarting_containers > 0 + || self.pausing_containers > 0 + || self.unpausing_containers > 0 + || self.stopping_containers > 0 + || self.destroying_containers > 0 } } diff --git a/client/core/rs/src/entities/action.rs b/client/core/rs/src/entities/action.rs index 92ce10bc6f..b24e823908 100644 --- a/client/core/rs/src/entities/action.rs +++ b/client/core/rs/src/entities/action.rs @@ -53,15 +53,15 @@ pub struct ActionListItemInfo { )] #[cfg_attr(feature = "utoipa", derive(utoipa::ToSchema))] pub enum ActionState { - /// Unknown case - #[default] - Unknown, + /// Currently running + Running, /// Last clone / pull successful (or never cloned) Ok, /// Last clone / pull failed Failed, - /// Currently running - Running, + /// Unknown case + #[default] + Unknown, } #[cfg(feature = "utoipa")] @@ -256,13 +256,51 @@ pub struct ActionActionState { #[typeshare] pub type ActionQuery = ResourceQuery; +#[typeshare] +#[derive( + Debug, Clone, Copy, Default, PartialEq, Eq, Serialize, Deserialize, +)] +#[cfg_attr(feature = "utoipa", derive(utoipa::ToSchema))] +pub enum ActionSortBy { + /// Sort by name. Default. + #[default] + Name, + /// Sort by state. + State, + /// Sort by next scheduled run. + NextRun, +} + #[typeshare] #[derive( Serialize, Deserialize, Debug, Clone, Default, DefaultBuilder, )] #[cfg_attr(feature = "utoipa", derive(utoipa::ToSchema))] -pub struct ActionQuerySpecifics {} +pub struct ActionQuerySpecifics { + /// Query only for Actions matching these states. + /// If empty, does not filter by state. + #[serde(default)] + pub states: Vec, + /// Query only for Actions with (or without) + /// a schedule configured. + #[serde(default)] + pub scheduled: Option, +} impl super::resource::AddFilters for ActionQuerySpecifics { - fn add_filters(&self, _filters: &mut Document) {} + fn add_filters(&self, filters: &mut Document) { + if let Some(scheduled) = self.scheduled { + if scheduled { + filters.insert( + "config.schedule", + bson::doc! { "$nin": ["", null] }, + ); + } else { + filters.insert( + "config.schedule", + bson::doc! { "$in": ["", null] }, + ); + } + } + } } diff --git a/client/core/rs/src/entities/alerter.rs b/client/core/rs/src/entities/alerter.rs index 7a4d9c2d6f..2bb578a467 100644 --- a/client/core/rs/src/entities/alerter.rs +++ b/client/core/rs/src/entities/alerter.rs @@ -323,6 +323,21 @@ fn default_pushover_url() -> String { #[typeshare] pub type AlerterQuery = ResourceQuery; +#[typeshare] +#[derive( + Debug, Clone, Copy, Default, PartialEq, Eq, Serialize, Deserialize, +)] +#[cfg_attr(feature = "utoipa", derive(utoipa::ToSchema))] +pub enum AlerterSortBy { + /// Sort by name. Default. + #[default] + Name, + /// Sort by endpoint type. + Type, + /// Sort by enabled. + Enabled, +} + #[typeshare] #[derive( Serialize, Deserialize, Debug, Clone, Default, DefaultBuilder, diff --git a/client/core/rs/src/entities/build.rs b/client/core/rs/src/entities/build.rs index d2ae5ae794..4a048ef625 100644 --- a/client/core/rs/src/entities/build.rs +++ b/client/core/rs/src/entities/build.rs @@ -56,30 +56,7 @@ impl Build { } image_registry .iter() - .map( - |ImageRegistryConfig { - domain, - account, - organization, - }| { - match ( - !domain.is_empty(), - !organization.is_empty(), - !account.is_empty(), - ) { - // If organization and account provided, name under organization. - (true, true, true) => { - format!("{domain}/{organization}/{name}") - } - // Just domain / account provided - (true, false, true) => { - format!("{domain}/{account}/{name}") - } - // Otherwise, just use name (local only) - _ => name.to_string(), - } - }, - ) + .map(|registry| registry.full_image_name(name)) .collect() } @@ -93,7 +70,7 @@ impl Build { version, image_tag, include_latest_tag, - include_version_tags: include_version_tag, + include_version_tags, include_commit_tag, .. } = &self.config; @@ -118,7 +95,7 @@ impl Build { tags.push(format!("{image_name}:latest{image_tag_postfix}")); } // `:1.19.5` + `:1.19` etc. / `1.19.5-tag` - if *include_version_tag { + if *include_version_tags { tags .push(format!("{image_name}:{version}{image_tag_postfix}")); tags.push(format!( @@ -152,6 +129,70 @@ impl Build { } Ok(res) } + + /// Used in build -> deployment flow to choose + /// the associated image to deploy. + pub fn get_deployment_image_name(&self) -> String { + let Build { + name, + config: + BuildConfig { + image_name, + image_registry, + .. + }, + .. + } = self; + let name = if image_name.is_empty() { + name + } else { + image_name + }; + if let Some(registry) = image_registry.first() { + registry.full_image_name(name) + } else { + name.to_string() + } + } + + /// Used in build -> deployment flow to choose the + /// associated latest tag. + /// + /// Priority: + /// - Semver version + /// - Commit hash + /// - Latest tag + pub fn get_deployment_image_tag(&self, version: Version) -> String { + let Build { + config: + BuildConfig { + image_tag, + include_version_tags, + include_commit_tag, + repo, + linked_repo, + .. + }, + .. + } = self; + + let image_tag_postfix = if image_tag.is_empty() { + format_args!("") + } else { + format_args!("-{image_tag}") + }; + + if *include_version_tags { + format!("{version}{image_tag_postfix}") + } else if (!repo.is_empty() || !linked_repo.is_empty()) + && *include_commit_tag + && let Some(hash) = &self.info.built_hash + { + format!("{hash}{image_tag_postfix}") + } else { + String::from("latest") + } + } } #[typeshare] @@ -177,6 +218,9 @@ pub struct BuildListItemInfo { /// Linked repo, if one is attached. pub linked_repo: String, + /// The name of the linked repo, if one is attached. + #[serde(default)] + pub linked_repo_name: String, /// The git provider domain pub git_provider: String, /// The repo used as the source of the build @@ -631,6 +675,30 @@ impl ImageRegistryConfig { static DEFAULT: OnceLock = OnceLock::new(); DEFAULT.get_or_init(Default::default) } + + pub fn full_image_name(&self, short_name: &str) -> String { + let Self { + domain, + organization, + account, + } = self; + match ( + !domain.is_empty(), + !organization.is_empty(), + !account.is_empty(), + ) { + // If organization and account provided, name under organization. + (true, true, true) => { + format!("{domain}/{organization}/{short_name}") + } + // Just domain / account provided + (true, false, true) => { + format!("{domain}/{account}/{short_name}") + } + // Otherwise, just use name (local only) + _ => short_name.to_string(), + } + } } #[typeshare] @@ -643,6 +711,21 @@ pub struct BuildActionState { #[typeshare] pub type BuildQuery = ResourceQuery; +#[typeshare] +#[derive( + Debug, Clone, Copy, Default, PartialEq, Eq, Serialize, Deserialize, +)] +#[cfg_attr(feature = "utoipa", derive(utoipa::ToSchema))] +pub enum BuildSortBy { + /// Sort by name. Default. + #[default] + Name, + /// Sort by source repo. + Source, + /// Sort by state. + State, +} + #[typeshare] #[derive( Debug, Clone, Default, Serialize, Deserialize, DefaultBuilder, @@ -655,10 +738,20 @@ pub struct BuildQuerySpecifics { #[serde(default)] pub repos: Vec, + /// Query only for Builds with these linked repos. + /// Only accepts Repo id (not name). + #[serde(default)] + pub linked_repos: Vec, + /// query for builds last built more recently than this timestamp /// defaults to 0 which is a no op #[serde(default)] pub built_since: I64, + + /// Query only for Builds matching these states. + /// If empty, does not filter by state. + #[serde(default)] + pub states: Vec, } impl super::resource::AddFilters for BuildQuerySpecifics { @@ -672,6 +765,12 @@ impl super::resource::AddFilters for BuildQuerySpecifics { if !self.repos.is_empty() { filters.insert("config.repo", doc! { "$in": &self.repos }); } + if !self.linked_repos.is_empty() { + filters.insert( + "config.linked_repo", + doc! { "$in": &self.linked_repos }, + ); + } if self.built_since > 0 { filters.insert( "info.last_built_at", diff --git a/client/core/rs/src/entities/builder.rs b/client/core/rs/src/entities/builder.rs index 46e0f235c7..2897abeeba 100644 --- a/client/core/rs/src/entities/builder.rs +++ b/client/core/rs/src/entities/builder.rs @@ -10,7 +10,7 @@ use crate::deserializers::{ use super::{ MergePartial, - config::{DockerRegistry, GitProvider}, + config::{GitProvider, ImageRegistry}, resource::{AddFilters, Resource, ResourceListItem, ResourceQuery}, }; @@ -311,7 +311,9 @@ impl MergePartial for BuilderConfig { PartialBuilderConfig::Server(partial) => match self { BuilderConfig::Server(config) => { let config = ServerBuilderConfig { - server_id: partial.server_id.unwrap_or(config.server_id), + server_ids: partial + .server_ids + .unwrap_or(config.server_ids), }; BuilderConfig::Server(config) } @@ -351,9 +353,9 @@ impl MergePartial for BuilderConfig { git_providers: partial .git_providers .unwrap_or(config.git_providers), - docker_registries: partial - .docker_registries - .unwrap_or(config.docker_registries), + image_registries: partial + .image_registries + .unwrap_or(config.image_registries), secrets: partial.secrets.unwrap_or(config.secrets), }; BuilderConfig::Aws(config) @@ -456,14 +458,27 @@ pub type _PartialServerBuilderConfig = PartialServerBuilderConfig; #[diff_derive(Serialize, Deserialize, Debug, Clone, Default)] #[partial(skip_serializing_none, from, diff)] pub struct ServerBuilderConfig { - /// The server id of the builder - #[serde(default, alias = "server")] - #[partial_attr(serde(alias = "server"))] + /// The server ids of the builders. + /// If multiple are given, builds will overflow + /// to later specified servers as needed. + #[serde( + default, + alias = "server_id", + alias = "server", + alias = "servers", + deserialize_with = "string_list_deserializer" + )] + #[partial_attr(serde( + alias = "server_id", + alias = "server", + alias = "servers", + deserialize_with = "option_string_list_deserializer" + ))] #[cfg_attr( feature = "schemars", - partial_attr(schemars(rename = "server")) + partial_attr(schemars(rename = "servers")) )] - pub server_id: String, + pub server_ids: Vec, } impl ServerBuilderConfig { @@ -581,10 +596,12 @@ pub struct AwsBuilderConfig { #[serde(default)] #[builder(default)] pub git_providers: Vec, - /// Which docker registries are available on the AMI. + /// Which image registries are available on the AMI. + /// + /// Pre v2.3.0, called `docker_registries` #[serde(default)] #[builder(default)] - pub docker_registries: Vec, + pub image_registries: Vec, /// Which secrets are available on the AMI. #[serde(default, deserialize_with = "string_list_deserializer")] #[partial_attr(serde( @@ -613,7 +630,7 @@ impl Default for AwsBuilderConfig { periphery_public_key: Default::default(), insecure_tls: default_insecure_tls(), git_providers: Default::default(), - docker_registries: Default::default(), + image_registries: Default::default(), secrets: Default::default(), } } @@ -659,6 +676,21 @@ impl utoipa::ToSchema for PartialAwsBuilderConfig {} #[typeshare] pub type BuilderQuery = ResourceQuery; +#[typeshare] +#[derive( + Debug, Clone, Copy, Default, PartialEq, Eq, Serialize, Deserialize, +)] +#[cfg_attr(feature = "utoipa", derive(utoipa::ToSchema))] +pub enum BuilderSortBy { + /// Sort by name. Default. + #[default] + Name, + /// Sort by builder provider type. + Provider, + /// Sort by instance type. + InstanceType, +} + #[typeshare] #[derive(Serialize, Deserialize, Debug, Clone, Default)] #[cfg_attr(feature = "utoipa", derive(utoipa::ToSchema))] diff --git a/client/core/rs/src/entities/config/cli/args/container.rs b/client/core/rs/src/entities/config/cli/args/container.rs index 6b0ff02f62..88c122dca2 100644 --- a/client/core/rs/src/entities/config/cli/args/container.rs +++ b/client/core/rs/src/entities/config/cli/args/container.rs @@ -37,6 +37,9 @@ pub struct Container { /// Specify the format of the output. #[arg(long, short = 'f', default_value_t = super::CliFormat::Table)] pub format: super::CliFormat, + /// Specify page for container list, starting at 1. + #[arg(long, short = 'p', default_value_t = 1)] + pub page: u64, } #[derive(Debug, Clone, clap::Subcommand)] diff --git a/client/core/rs/src/entities/config/cli/args/list.rs b/client/core/rs/src/entities/config/cli/args/list.rs index bb463af5c9..9a28df6105 100644 --- a/client/core/rs/src/entities/config/cli/args/list.rs +++ b/client/core/rs/src/entities/config/cli/args/list.rs @@ -18,7 +18,7 @@ pub struct List { /// List only "in progress" / "pending" resources, like Actions / Procedures that are running (alias: `pending`) #[arg( long, - short = 'p', + short = 'i', alias = "pending", default_value_t = false )] @@ -45,6 +45,10 @@ pub struct List { /// Can be specified multiple times. (alias `s`) #[arg(name = "server", long, short = 's')] pub servers: Vec, + /// Filter by a particular swarm. Supports wildcard. + /// Can be specified multiple times. (alias `w`) + #[arg(name = "swarm", long, short = 'w')] + pub swarms: Vec, /// Filter by a particular builder. Supports wildcard. /// Can be specified multiple times. (alias `b`) #[arg(name = "builder", long, short = 'b')] @@ -52,6 +56,9 @@ pub struct List { /// Specify the format of the output. #[arg(long, short = 'f', default_value_t = super::CliFormat::Table)] pub format: super::CliFormat, + /// Specify page for resource list, starting at 1. + #[arg(long, short = 'p', default_value_t = 1)] + pub page: u64, } impl From for ResourceFilters { @@ -66,6 +73,7 @@ impl From for ResourceFilters { names: value.names, tags: value.tags, servers: value.servers, + swarms: value.swarms, builders: value.builders, format: value.format, } @@ -77,6 +85,9 @@ pub enum ListCommand { /// List Servers (aliases: `server`, `sv`) #[clap(alias = "server", alias = "sv")] Servers(ResourceFilters), + /// List Swarms (aliases: `swarm`, `sw`) + #[clap(alias = "swarm", alias = "sw")] + Swarms(ResourceFilters), /// List Stacks (aliases: `stack`, `st`) #[clap(alias = "stack", alias = "st")] Stacks(ResourceFilters), @@ -154,6 +165,10 @@ pub struct ResourceFilters { /// Can be specified multiple times. (alias `s`) #[arg(name = "server", long, short = 's')] pub servers: Vec, + /// Filter by a particular swarm. Supports wildcard. + /// Can be specified multiple times. (alias `w`) + #[arg(name = "swarm", long, short = 'w')] + pub swarms: Vec, /// Filter by a particular builder. Supports wildcard. /// Can be specified multiple times. (alias `b`) #[arg(name = "builder", long, short = 'b')] diff --git a/client/core/rs/src/entities/config/core.rs b/client/core/rs/src/entities/config/core.rs index 1a1692a35c..e8589b59c6 100644 --- a/client/core/rs/src/entities/config/core.rs +++ b/client/core/rs/src/entities/config/core.rs @@ -23,7 +23,7 @@ use crate::{ }, }; -use super::{DockerRegistry, GitProvider, empty_or_redacted}; +use super::{GitProvider, ImageRegistry, empty_or_redacted}; /// # Komodo Core Environment Variables /// @@ -118,6 +118,8 @@ pub struct Env { /// Override `transparent_mode` pub komodo_transparent_mode: Option, + /// Override `default_pagination_limit` + pub komodo_default_pagination_limit: Option, /// Override `ui_write_disabled` pub komodo_ui_write_disabled: Option, /// Override `enable_new_users` @@ -294,6 +296,11 @@ pub struct Env { /// Override `ssl_cert_file` pub komodo_ssl_cert_file: Option, + /// Override `reporting_enabled` + pub komodo_reporting_enabled: Option, + /// Override `reporting_private_key` + pub komodo_reporting_private_key: Option, + /// Override `ui_path` pub komodo_ui_path: Option, /// Override `ui_index_force_no_cache` @@ -397,6 +404,11 @@ pub struct CoreConfig { #[serde(default)] pub timezone: String, + /// Set the default pagination limit for the API and UI to use. + /// Default: 50 + #[serde(default = "default_default_pagination_limit")] + pub default_pagination_limit: u64, + /// Disable user ability to use the UI to update resource configuration. #[serde(default)] pub ui_write_disabled: bool, @@ -726,14 +738,17 @@ pub struct CoreConfig { // ====================== // = Registry Providers = // ====================== - /// Configure docker credentials used to push / pull images. - /// Supports any docker image repository. + /// Configure image registry credentials used to push / pull images. + /// + /// Pre v2.3.0, called `docker_registries` #[serde( default, + alias = "image_registry", alias = "docker_registry", + alias = "docker_registries", skip_serializing_if = "Vec::is_empty" )] - pub docker_registries: Vec, + pub image_registries: Vec, // =========== // = Secrets = @@ -761,6 +776,28 @@ pub struct CoreConfig { #[serde(default = "default_ssl_cert_file")] pub ssl_cert_file: String, + // ============= + // = Reporting = + // ============= + /// If passed, enables semi-anonymous reporting to https://mogh.tech/komodo/report. + /// Reports are only identified by the specific + /// private key ([CoreConfig::reporting_private_key]) used + /// to sign the report. + #[serde(default)] + pub reporting_enabled: bool, + + /// Private key to sign the core reports. + /// + /// Supports openssl generated pem file, `openssl genpkey -algorithm X25519 -out private.key`. + /// To load from file, use `private_key = "file:/path/to/private.key"`. + /// + /// If a file is specified and does not exist, will try to generate one at the path + /// and use it going forward. + /// + /// Default: file:/config/keys/core-reporting.key + #[serde(default = "default_reporting_private_key")] + pub reporting_private_key: String, + // ========= // = Other = // ========= @@ -811,6 +848,14 @@ fn default_private_key() -> String { String::from("file:/config/keys/core.key") } +fn default_reporting_private_key() -> String { + String::from("file:/config/keys/core-reporting.key") +} + +fn default_default_pagination_limit() -> u64 { + 50 +} + fn default_ui_path() -> String { "/app/ui".to_string() } @@ -891,10 +936,11 @@ impl Default for CoreConfig { port: default_core_port(), bind_ip: default_core_bind_ip(), internet_interface: Default::default(), - private_key: Default::default(), + private_key: default_private_key(), periphery_public_keys: Default::default(), passkey: Default::default(), timezone: Default::default(), + default_pagination_limit: default_default_pagination_limit(), ui_write_disabled: Default::default(), disable_confirm_dialog: Default::default(), disable_websocket_reconnect: Default::default(), @@ -950,11 +996,13 @@ impl Default for CoreConfig { monitoring_interval: default_monitoring_interval(), aws: Default::default(), git_providers: Default::default(), - docker_registries: Default::default(), + image_registries: Default::default(), secrets: Default::default(), ssl_enabled: Default::default(), ssl_key_file: default_ssl_key_file(), ssl_cert_file: default_ssl_cert_file(), + reporting_enabled: Default::default(), + reporting_private_key: default_reporting_private_key(), ui_path: default_ui_path(), ui_index_force_no_cache: Default::default(), sync_directory: default_sync_directory(), @@ -980,6 +1028,7 @@ impl CoreConfig { periphery_public_keys: config.periphery_public_keys, passkey: config.passkey.as_deref().map(empty_or_redacted), timezone: config.timezone, + default_pagination_limit: config.default_pagination_limit, first_server_address: config.first_server_address, first_server_name: config.first_server_name, jwt_secret: empty_or_redacted(&config.jwt_secret), @@ -1080,8 +1129,8 @@ impl CoreConfig { provider }) .collect(), - docker_registries: config - .docker_registries + image_registries: config + .image_registries .into_iter() .map(|mut provider| { provider.accounts.iter_mut().for_each(|account| { @@ -1090,10 +1139,18 @@ impl CoreConfig { provider }) .collect(), - ssl_enabled: config.ssl_enabled, ssl_key_file: config.ssl_key_file, ssl_cert_file: config.ssl_cert_file, + reporting_enabled: config.reporting_enabled, + reporting_private_key: if self + .reporting_private_key + .starts_with("file:") + { + self.reporting_private_key.clone() + } else { + empty_or_redacted(&self.reporting_private_key) + }, ui_path: config.ui_path, ui_index_force_no_cache: config.ui_index_force_no_cache, repo_directory: config.repo_directory, diff --git a/client/core/rs/src/entities/config/mod.rs b/client/core/rs/src/entities/config/mod.rs index 6ec7babed8..e74b3c482c 100644 --- a/client/core/rs/src/entities/config/mod.rs +++ b/client/core/rs/src/entities/config/mod.rs @@ -154,9 +154,9 @@ fn default_git_https() -> bool { )] #[cfg_attr(feature = "utoipa", derive(utoipa::ToSchema))] #[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))] -pub struct DockerRegistry { - /// The docker provider domain. Default: `docker.io`. - #[serde(default = "default_docker_provider")] +pub struct ImageRegistry { + /// The image provider domain. Default: `docker.io`. + #[serde(default = "default_image_provider")] pub domain: String, /// The accounts on the registry. Required. #[serde(alias = "account")] @@ -167,7 +167,7 @@ pub struct DockerRegistry { pub organizations: Vec, } -fn default_docker_provider() -> String { +fn default_image_provider() -> String { String::from("docker.io") } diff --git a/client/core/rs/src/entities/config/periphery.rs b/client/core/rs/src/entities/config/periphery.rs index 3ecca5ea23..76d022ea3b 100644 --- a/client/core/rs/src/entities/config/periphery.rs +++ b/client/core/rs/src/entities/config/periphery.rs @@ -29,7 +29,7 @@ use crate::{ }; use super::{ - DockerRegistry, GitProvider, ProviderAccount, empty_or_redacted, + GitProvider, ImageRegistry, ProviderAccount, empty_or_redacted, }; /// # Periphery Command Line Arguments. @@ -424,10 +424,16 @@ pub struct PeripheryConfig { #[serde(default, alias = "git_provider")] pub git_providers: ForgivingVec, - /// Configure docker credentials used to push / pull images. - /// Supports any docker image repository. - #[serde(default, alias = "docker_registry")] - pub docker_registries: ForgivingVec, + /// Configure image registry credentials used to push / pull images. + /// + /// Pre v2.3.0, called `docker_registries` + #[serde( + default, + alias = "image_registry", + alias = "docker_registry", + alias = "docker_registries" + )] + pub image_registries: ForgivingVec, } fn default_periphery_port() -> u16 { @@ -489,7 +495,7 @@ impl Default for PeripheryConfig { exclude_disk_mounts: Default::default(), secrets: Default::default(), git_providers: Default::default(), - docker_registries: Default::default(), + image_registries: Default::default(), ssl_enabled: default_ssl_enabled(), ssl_key_file: None, ssl_cert_file: None, @@ -560,10 +566,10 @@ impl PeripheryConfig { .collect(), }) .collect(), - docker_registries: self - .docker_registries + image_registries: self + .image_registries .iter() - .map(|provider| DockerRegistry { + .map(|provider| ImageRegistry { domain: provider.domain.clone(), organizations: provider.organizations.clone(), accounts: provider diff --git a/client/core/rs/src/entities/core_report.rs b/client/core/rs/src/entities/core_report.rs new file mode 100644 index 0000000000..b3db82fde7 --- /dev/null +++ b/client/core/rs/src/entities/core_report.rs @@ -0,0 +1,19 @@ +use std::collections::HashMap; + +use serde::{Deserialize, Serialize}; + +use crate::entities::ResourceTargetVariant; + +/// Semi-anonymous core data reporting. +/// Reports only include the reporting-specific core public key +/// (not the keys used for server connection). +#[derive(Debug, Clone, Default, Serialize, Deserialize)] +pub struct CoreReport { + /// Reporting specific public key. + /// Must match public key obtained through request signature. + pub report_public_key: String, + /// The total number of users + pub users: u64, + /// Resource counts by type + pub count: HashMap, +} diff --git a/client/core/rs/src/entities/deployment.rs b/client/core/rs/src/entities/deployment.rs index 8e3c2eecab..c81a17b450 100644 --- a/client/core/rs/src/entities/deployment.rs +++ b/client/core/rs/src/entities/deployment.rs @@ -8,13 +8,7 @@ use strum::{AsRefStr, Display, EnumDiscriminants, EnumString}; use typeshare::typeshare; use crate::{ - deserializers::{ - conversions_deserializer, env_vars_deserializer, - labels_deserializer, option_conversions_deserializer, - option_env_vars_deserializer, option_labels_deserializer, - option_string_list_deserializer, option_term_labels_deserializer, - string_list_deserializer, term_labels_deserializer, - }, + deserializers::*, entities::{ EnvironmentVar, ImageDigest, environment_vars_from_str, optional_str, @@ -39,10 +33,43 @@ pub struct DeploymentSchema( #[typeshare] pub type Deployment = Resource; +impl Deployment { + /// Configured custom container / service name, + /// falling back to deployment name. + /// This is the name the next deploy will use. + pub fn custom_name(&self) -> &str { + optional_str(self.config.custom_name.trim()) + .unwrap_or(self.name.trim()) + } + + /// The name of the currently deployed container / service, + /// falling back to [Deployment::custom_name]. + /// May be different than custom_name if the name configuration + /// changed since the last deploy. Use this to match the Deployment + /// against the existing container / service. + pub fn deployed_name(&self) -> &str { + let trimmed = self.info.deployed_name.trim(); + if !trimmed.is_empty() { + trimmed + } else { + self.custom_name() + } + } +} + #[typeshare] pub type DeploymentListItem = ResourceListItem; +impl DeploymentListItem { + /// Configured custom container / service name, + /// falling back to deployment name. + pub fn custom_name(&self) -> &str { + optional_str(self.info.custom_name.trim()) + .unwrap_or(self.name.trim()) + } +} + #[typeshare] #[derive(Serialize, Deserialize, Debug, Clone)] #[cfg_attr(feature = "utoipa", derive(utoipa::ToSchema))] @@ -51,14 +78,24 @@ pub struct DeploymentListItemInfo { pub state: DeploymentState, /// The status of the docker container (eg. up 12 hours, exited 5 minutes ago.) pub status: Option, + /// The container / service name, if different than + /// the deployment name. Uses the currently deployed name + /// if deployed, else the configured custom name. + pub custom_name: String, /// The image attached to the deployment. pub image: String, /// Whether there is a newer image available at the same tag. pub update_available: bool, /// The swarm that deployment is deployed on, when in Swarm mode. pub swarm_id: String, + /// The name of the swarm that deployment is deployed on, when in Swarm mode. + #[serde(default)] + pub swarm_name: String, /// The server that deployment is deployed on, when in Server mode. pub server_id: String, + /// The name of the server that deployment is deployed on, when in Server mode. + #[serde(default)] + pub server_name: String, /// An attached Komodo Build, if it exists. pub build_id: Option, } @@ -71,6 +108,11 @@ pub struct DeploymentInfo { /// This includes both the image name / tag, and the specific digest hash. #[serde(default)] pub latest_image_digest: ImageDigest, + /// The container / service name used at the time of the last deploy. + /// Kept to match the Deployment to its container / service + /// even if the name configuration changes before the next deploy. + #[serde(default)] + pub deployed_name: String, } #[typeshare(serialized_as = "Partial")] @@ -113,6 +155,12 @@ pub struct DeploymentConfig { #[builder(default)] pub server_id: String, + /// Specify a custom container / service name, + /// if different from Deployment name. + #[serde(default)] + #[builder(default)] + pub custom_name: String, + /// The image which the deployment deploys. /// Can either be a user inputted image, or a Komodo Build. #[serde(default)] @@ -299,6 +347,7 @@ impl Default for DeploymentConfig { Self { swarm_id: Default::default(), server_id: Default::default(), + custom_name: Default::default(), image: Default::default(), image_registry_account: Default::default(), skip_secret_interp: Default::default(), @@ -588,6 +637,23 @@ pub struct DeploymentActionState { #[typeshare] pub type DeploymentQuery = ResourceQuery; +#[typeshare] +#[derive( + Debug, Clone, Copy, Default, PartialEq, Eq, Serialize, Deserialize, +)] +#[cfg_attr(feature = "utoipa", derive(utoipa::ToSchema))] +pub enum DeploymentSortBy { + /// Sort by name. Default. + #[default] + Name, + /// Sort by image. + Image, + /// Sort by host Server / Swarm name. + Host, + /// Sort by state. + State, +} + #[typeshare] #[derive( Debug, Clone, Default, Serialize, Deserialize, DefaultBuilder, @@ -600,6 +666,12 @@ pub struct DeploymentQuerySpecifics { #[serde(default)] pub server_ids: Vec, + /// Query only for Deployments on these Swarms. + /// If empty, does not filter by Swarm. + /// Only accepts Swarm id (not name). + #[serde(default)] + pub swarm_ids: Vec, + /// Query only for Deployments with these Builds attached. /// If empty, does not filter by Build. /// Only accepts Build id (not name). @@ -609,6 +681,11 @@ pub struct DeploymentQuerySpecifics { /// Query only for Deployments with available image updates. #[serde(default)] pub update_available: bool, + + /// Query only for Deployments matching these states. + /// If empty, does not filter by state. + #[serde(default)] + pub states: Vec, } impl super::resource::AddFilters for DeploymentQuerySpecifics { @@ -617,6 +694,10 @@ impl super::resource::AddFilters for DeploymentQuerySpecifics { filters .insert("config.server_id", doc! { "$in": &self.server_ids }); } + if !self.swarm_ids.is_empty() { + filters + .insert("config.swarm_id", doc! { "$in": &self.swarm_ids }); + } if !self.build_ids.is_empty() { filters.insert("config.image.type", "Build"); filters.insert( diff --git a/client/core/rs/src/entities/docker/container.rs b/client/core/rs/src/entities/docker/container.rs index aa2c9a033f..81231e4efe 100644 --- a/client/core/rs/src/entities/docker/container.rs +++ b/client/core/rs/src/entities/docker/container.rs @@ -22,6 +22,9 @@ pub struct ContainerListItem { /// The Server which hosts the container. #[serde(skip_serializing_if = "Option::is_none")] pub server_id: Option, + /// The name of the Server which hosts the container. + #[serde(default, skip_serializing_if = "Option::is_none")] + pub server_name: Option, /// The first name in Names, not including the initial '/' pub name: String, /// The ID of this container @@ -271,6 +274,29 @@ pub struct ContainerState { pub health: Option, } +#[typeshare] +#[derive( + Debug, Clone, Copy, Default, PartialEq, Eq, Serialize, Deserialize, +)] +#[cfg_attr(feature = "utoipa", derive(utoipa::ToSchema))] +pub enum ContainerSortBy { + /// Sort by container name. Default. + #[default] + Name, + /// Sort by host Server name. + Server, + /// Sort by container state. + State, + /// Sort by image. + Image, + /// Sort by first network. + Networks, + /// Sort by first port. + Ports, + /// Sort by first volume. + Volumes, +} + #[typeshare] #[derive( Debug, @@ -291,11 +317,11 @@ pub struct ContainerState { pub enum ContainerStateStatusEnum { Running, Created, - Paused, Restarting, - Exited, Stopping, Removing, + Paused, + Exited, Dead, #[default] #[serde(rename = "")] diff --git a/client/core/rs/src/entities/mod.rs b/client/core/rs/src/entities/mod.rs index cd3a31b0e1..012ece8000 100644 --- a/client/core/rs/src/entities/mod.rs +++ b/client/core/rs/src/entities/mod.rs @@ -35,6 +35,8 @@ pub mod build; pub mod builder; /// [core config][config::core] and [periphery config][config::periphery] pub mod config; +/// Subtypes of [CoreReport][core_report::CoreReport] +pub mod core_report; /// Subtypes of [Deployment][deployment::Deployment]. pub mod deployment; /// Networks, Images, Containers. @@ -156,13 +158,34 @@ pub fn to_path_compatible_name(name: &str) -> String { name.trim().replace([' ', '\n'], "_").to_string() } -/// Enforce common container naming rules. -/// [a-zA-Z0-9_.-] +/// Enforce docker container / service naming rules: +/// [a-zA-Z0-9][a-zA-Z0-9_.-]* +/// Any other characters are replaced with '_', +/// and leading non-alphanumeric characters are stripped. +/// This also guards against shell injection through the name, +/// as it is passed to shell commands like 'docker stop {name}'. pub fn to_container_compatible_name(name: &str) -> String { - name.trim().replace([' ', ',', '\n', '&'], "_").to_string() + name + .trim() + .chars() + .map(|c| { + if c.is_ascii_alphanumeric() || matches!(c, '_' | '.' | '-') { + c + } else { + '_' + } + }) + .collect::() + .trim_start_matches(|c: char| !c.is_ascii_alphanumeric()) + .to_string() } -/// Enforce common docker naming rules, such as only lowercase, and no '.'. +/// Enforce common docker naming rules, such as only lowercase, and no '.': +/// [a-z0-9][a-z0-9_-]* +/// Any other characters are replaced with '_', +/// and leading non-alphanumeric characters are stripped. +/// This also guards against shell injection through the name, +/// as it is passed to shell commands like 'docker compose -p {name}'. /// These apply to: /// - Stacks (docker project name) /// - Builds (docker image name) @@ -170,9 +193,21 @@ pub fn to_container_compatible_name(name: &str) -> String { /// - Volumes pub fn to_docker_compatible_name(name: &str) -> String { name - .to_lowercase() - .replace([' ', '.', ',', '\n', '&'], "_") .trim() + .to_lowercase() + .chars() + .map(|c| { + if c.is_ascii_lowercase() + || c.is_ascii_digit() + || matches!(c, '_' | '-') + { + c + } else { + '_' + } + }) + .collect::() + .trim_start_matches(|c: char| !c.is_ascii_alphanumeric()) .to_string() } @@ -1281,6 +1316,7 @@ pub enum Operation { RenameProcedure, DeleteProcedure, RunProcedure, + CancelProcedure, // Action CreateAction, @@ -1288,6 +1324,7 @@ pub enum Operation { RenameAction, DeleteAction, RunAction, + CancelAction, // Sync CreateResourceSync, diff --git a/client/core/rs/src/entities/procedure.rs b/client/core/rs/src/entities/procedure.rs index a534da0da2..80e96a0afc 100644 --- a/client/core/rs/src/entities/procedure.rs +++ b/client/core/rs/src/entities/procedure.rs @@ -256,13 +256,51 @@ pub struct ProcedureActionState { #[typeshare] pub type ProcedureQuery = ResourceQuery; +#[typeshare] +#[derive( + Debug, Clone, Copy, Default, PartialEq, Eq, Serialize, Deserialize, +)] +#[cfg_attr(feature = "utoipa", derive(utoipa::ToSchema))] +pub enum ProcedureSortBy { + /// Sort by name. Default. + #[default] + Name, + /// Sort by state. + State, + /// Sort by next scheduled run. + NextRun, +} + #[typeshare] #[derive( Serialize, Deserialize, Debug, Clone, Default, DefaultBuilder, )] #[cfg_attr(feature = "utoipa", derive(utoipa::ToSchema))] -pub struct ProcedureQuerySpecifics {} +pub struct ProcedureQuerySpecifics { + /// Query only for Procedures matching these states. + /// If empty, does not filter by state. + #[serde(default)] + pub states: Vec, + /// Query only for Procedures with (or without) + /// a schedule configured. + #[serde(default)] + pub scheduled: Option, +} impl super::resource::AddFilters for ProcedureQuerySpecifics { - fn add_filters(&self, _: &mut Document) {} + fn add_filters(&self, filters: &mut Document) { + if let Some(scheduled) = self.scheduled { + if scheduled { + filters.insert( + "config.schedule", + bson::doc! { "$nin": ["", null] }, + ); + } else { + filters.insert( + "config.schedule", + bson::doc! { "$in": ["", null] }, + ); + } + } + } } diff --git a/client/core/rs/src/entities/provider.rs b/client/core/rs/src/entities/provider.rs index f685b396d2..a4d20e72ca 100644 --- a/client/core/rs/src/entities/provider.rs +++ b/client/core/rs/src/entities/provider.rs @@ -72,8 +72,8 @@ impl utoipa::PartialSchema for PartialGitProviderAccount { #[cfg(feature = "utoipa")] impl utoipa::ToSchema for PartialGitProviderAccount {} -#[typeshare(serialized_as = "Partial")] -pub type _PartialDockerRegistryAccount = PartialDockerRegistryAccount; +#[typeshare(serialized_as = "Partial")] +pub type _PartialImageRegistryAccount = PartialImageRegistryAccount; /// Configuration to access private image repositories on various registries. #[typeshare] @@ -87,10 +87,10 @@ pub type _PartialDockerRegistryAccount = PartialDockerRegistryAccount; derive(mongo_indexed::derive::MongoIndexed) )] #[cfg_attr(feature = "mongo", unique_doc_index({ "domain": 1, "username": 1 }))] -pub struct DockerRegistryAccount { +pub struct ImageRegistryAccount { /// The Mongo ID of the docker registry account. /// This field is de/serialized from/to JSON as - /// `{ "_id": { "$oid": "..." }, ...(rest of DockerRegistryAccount) }` + /// `{ "_id": { "$oid": "..." }, ...(rest of ImageRegistryAccount) }` #[serde( default, rename = "_id", @@ -122,7 +122,7 @@ fn default_registry_domain() -> String { } #[cfg(feature = "utoipa")] -impl utoipa::PartialSchema for PartialDockerRegistryAccount { +impl utoipa::PartialSchema for PartialImageRegistryAccount { fn schema() -> utoipa::openapi::RefOr { utoipa::schema!(#[inline] std::collections::HashMap).into() @@ -130,4 +130,4 @@ impl utoipa::PartialSchema for PartialDockerRegistryAccount { } #[cfg(feature = "utoipa")] -impl utoipa::ToSchema for PartialDockerRegistryAccount {} +impl utoipa::ToSchema for PartialImageRegistryAccount {} diff --git a/client/core/rs/src/entities/repo.rs b/client/core/rs/src/entities/repo.rs index 3c48af4478..25ca866cfe 100644 --- a/client/core/rs/src/entities/repo.rs +++ b/client/core/rs/src/entities/repo.rs @@ -29,6 +29,9 @@ pub type RepoListItem = ResourceListItem; pub struct RepoListItemInfo { /// The server that repo sits on. pub server_id: String, + /// The name of the server that repo sits on. + #[serde(default)] + pub server_name: String, /// The builder that builds the repo. pub builder_id: String, /// Repo last cloned / pulled timestamp in ms. @@ -57,23 +60,33 @@ pub struct RepoListItemInfo { #[typeshare] #[derive( - Debug, Clone, Copy, Default, Serialize, Deserialize, Display, + Debug, + Clone, + Copy, + Default, + PartialEq, + Eq, + PartialOrd, + Ord, + Serialize, + Deserialize, + Display, )] #[cfg_attr(feature = "utoipa", derive(utoipa::ToSchema))] pub enum RepoState { - /// Unknown case - #[default] - Unknown, - /// Last clone / pull successful (or never cloned) - Ok, - /// Last clone / pull failed - Failed, /// Currently cloning Cloning, /// Currently pulling Pulling, /// Currently building Building, + /// Last clone / pull successful (or never cloned) + Ok, + /// Last clone / pull failed + Failed, + /// Unknown case + #[default] + Unknown, } #[cfg(feature = "utoipa")] @@ -331,6 +344,23 @@ pub struct RepoActionState { #[typeshare] pub type RepoQuery = ResourceQuery; +#[typeshare] +#[derive( + Debug, Clone, Copy, Default, PartialEq, Eq, Serialize, Deserialize, +)] +#[cfg_attr(feature = "utoipa", derive(utoipa::ToSchema))] +pub enum RepoSortBy { + /// Sort by name. Default. + #[default] + Name, + /// Sort by the git repo. + Repo, + /// Sort by branch. + Branch, + /// Sort by state. + State, +} + #[typeshare] #[derive( Serialize, Deserialize, Debug, Clone, Default, DefaultBuilder, @@ -338,7 +368,19 @@ pub type RepoQuery = ResourceQuery; #[cfg_attr(feature = "utoipa", derive(utoipa::ToSchema))] pub struct RepoQuerySpecifics { /// Filter repos by their repo. + #[serde(default)] pub repos: Vec, + + /// Query only for Repos on these Servers. + /// If empty, does not filter by Server. + /// Only accepts Server id (not name). + #[serde(default)] + pub server_ids: Vec, + + /// Query only for Repos matching these states. + /// If empty, does not filter by state. + #[serde(default)] + pub states: Vec, } impl super::resource::AddFilters for RepoQuerySpecifics { @@ -346,5 +388,9 @@ impl super::resource::AddFilters for RepoQuerySpecifics { if !self.repos.is_empty() { filters.insert("config.repo", doc! { "$in": &self.repos }); } + if !self.server_ids.is_empty() { + filters + .insert("config.server_id", doc! { "$in": &self.server_ids }); + } } } diff --git a/client/core/rs/src/entities/resource.rs b/client/core/rs/src/entities/resource.rs index e5ef642c61..0d638e6367 100644 --- a/client/core/rs/src/entities/resource.rs +++ b/client/core/rs/src/entities/resource.rs @@ -120,10 +120,13 @@ pub struct ResourceListItem { )] #[cfg_attr(feature = "utoipa", derive(utoipa::ToSchema))] pub struct ResourceQuery { - #[serde(default)] + /// List of search terms. Names must contain + /// all terms to match. + #[serde(default, deserialize_with = "string_list_deserializer")] + pub terms: Vec, + /// List of exact names to return + #[serde(default, deserialize_with = "string_list_deserializer")] pub names: Vec, - #[serde(default)] - pub templates: TemplatesQueryBehavior, /// Pass Vec of tag ids or tag names #[serde(default, deserialize_with = "string_list_deserializer")] pub tags: Vec, @@ -131,6 +134,8 @@ pub struct ResourceQuery { #[serde(default)] pub tag_behavior: TagQueryBehavior, #[serde(default)] + pub templates: TemplatesQueryBehavior, + #[serde(default)] pub specific: T, } @@ -177,13 +182,36 @@ impl AddFilters for () {} impl AddFilters for ResourceQuery { fn add_filters(&self, filters: &mut Document) { + // Seperate _id query let (ids, names) = split_names(&self.names); if !ids.is_empty() { filters.insert("_id", doc! { "$in": ids }); } + + // Name queries using $and doc array. + let mut name_queries = Vec::::new(); + if !names.is_empty() { - filters.insert("name", doc! { "$in": names }); + name_queries.push(doc! { "name": { "$in": names } }); } + + for term in &self.terms { + if term.is_empty() { + continue; + } + name_queries.push(doc! { + "name": { + "$regex": regex::escape(term), + "$options": "i" + }, + }); + } + + if !name_queries.is_empty() { + filters.insert("$and", name_queries); + } + + // Template query match self.templates { TemplatesQueryBehavior::Exclude => { filters.insert("template", doc! { "$ne": true }); @@ -195,6 +223,8 @@ impl AddFilters for ResourceQuery { // No query on template field necessary } }; + + // Tag queries maybe using $or doc array if !self.tags.is_empty() { match self.tag_behavior { TagQueryBehavior::All => { @@ -210,6 +240,7 @@ impl AddFilters for ResourceQuery { } } } + self.specific.add_filters(filters); } } diff --git a/client/core/rs/src/entities/schedule.rs b/client/core/rs/src/entities/schedule.rs index 86a20185c0..93f2d7b56d 100644 --- a/client/core/rs/src/entities/schedule.rs +++ b/client/core/rs/src/entities/schedule.rs @@ -30,3 +30,20 @@ pub struct Schedule { /// Resource tags. pub tags: Vec, } + +#[typeshare] +#[derive( + Debug, Clone, Copy, Default, PartialEq, Eq, Serialize, Deserialize, +)] +#[cfg_attr(feature = "utoipa", derive(utoipa::ToSchema))] +pub enum ScheduleSortBy { + /// Sort by target name. Default. + #[default] + Name, + /// Sort by the schedule expression. + Schedule, + /// Sort by next scheduled run. + NextRun, + /// Sort by enabled. + Enabled, +} diff --git a/client/core/rs/src/entities/server.rs b/client/core/rs/src/entities/server.rs index 823f1846e0..c32907c48d 100644 --- a/client/core/rs/src/entities/server.rs +++ b/client/core/rs/src/entities/server.rs @@ -10,7 +10,9 @@ use crate::{ deserializers::{ option_string_list_deserializer, string_list_deserializer, }, - entities::{_Serror, MaintenanceWindow, Timelength}, + entities::{ + _Serror, MaintenanceWindow, Timelength, stats::MinimalSystemStats, + }, }; use super::{ @@ -40,6 +42,12 @@ pub struct ServerListItemInfo { /// If there is an error reaching /// the server, message will be given here. pub err: Option<_Serror>, + /// System stats, if available + pub stats: Option, + /// The server's number of physical cores. + pub core_count: Option, + /// The server's number of logical cores. + pub logical_core_count: Option, /// Region of the server. pub region: String, /// Address of the server, or null if empty. @@ -399,7 +407,7 @@ pub struct PeripheryInformation { /// Current pending actions on the server. #[typeshare] -#[derive(Serialize, Deserialize, Debug, Clone, Copy, Default)] +#[derive(Debug, Clone, Copy, Default, Serialize, Deserialize)] #[cfg_attr(feature = "utoipa", derive(utoipa::ToSchema))] pub struct ServerActionState { /// Server currently pruning networks @@ -417,15 +425,17 @@ pub struct ServerActionState { /// Server currently pruning system pub pruning_system: bool, /// Server currently starting containers. - pub starting_containers: bool, + pub starting_containers: u32, /// Server currently restarting containers. - pub restarting_containers: bool, + pub restarting_containers: u32, /// Server currently pausing containers. - pub pausing_containers: bool, + pub pausing_containers: u32, /// Server currently unpausing containers. - pub unpausing_containers: bool, + pub unpausing_containers: u32, /// Server currently stopping containers. - pub stopping_containers: bool, + pub stopping_containers: u32, + /// Server currently destroying containers. + pub destroying_containers: u32, } #[typeshare] @@ -459,9 +469,41 @@ pub enum ServerState { #[typeshare] pub type ServerQuery = ResourceQuery; +#[typeshare] +#[derive( + Debug, Clone, Copy, Default, PartialEq, Eq, Serialize, Deserialize, +)] +#[cfg_attr(feature = "utoipa", derive(utoipa::ToSchema))] +pub enum ServerSortBy { + /// Sort by name. Default. + #[default] + Name, + /// Sort by region. + Region, + /// Sort by periphery version. + Version, + /// Sort by state. + State, + /// Sort by current cpu usage percentage. + Cpu, + /// Sort by current memory usage percentage. + Memory, + /// Sort by current disk usage percentage. + Disk, + /// Sort by current 1m load average. + LoadAverage, + /// Sort by current network usage (ingress + egress). + Network, +} + #[typeshare] #[derive(Serialize, Deserialize, Debug, Clone, Default)] #[cfg_attr(feature = "utoipa", derive(utoipa::ToSchema))] -pub struct ServerQuerySpecifics {} +pub struct ServerQuerySpecifics { + /// Query only for Servers matching these states. + /// If empty, does not filter by state. + #[serde(default)] + pub states: Vec, +} impl AddFilters for ServerQuerySpecifics {} diff --git a/client/core/rs/src/entities/stack.rs b/client/core/rs/src/entities/stack.rs index 1b62304bd2..f77a07f475 100644 --- a/client/core/rs/src/entities/stack.rs +++ b/client/core/rs/src/entities/stack.rs @@ -22,7 +22,12 @@ use crate::{ }, entities::{ EnvironmentVar, ImageDigest, - docker::service::SwarmServiceListItem, environment_vars_from_str, + docker::{ + container::ContainerStateStatusEnum, + service::SwarmServiceListItem, + }, + environment_vars_from_str, + swarm::SwarmState, }, }; @@ -146,14 +151,23 @@ pub type StackListItem = ResourceListItem; pub struct StackListItemInfo { /// The swarm that stack is deployed on, when in Swarm mode. pub swarm_id: String, + /// The name of the swarm that stack is deployed on, when in Swarm mode. + #[serde(default)] + pub swarm_name: String, /// The server that stack is deployed on, when in Server mode. pub server_id: String, + /// The name of the server that stack is deployed on, when in Server mode. + #[serde(default)] + pub server_name: String, /// Whether stack is using files on host mode pub files_on_host: bool, /// Whether stack has file contents defined. pub file_contents: bool, /// Linked repo, if one is attached. pub linked_repo: String, + /// The name of the linked repo, if one is attached. + #[serde(default)] + pub linked_repo_name: String, /// The git provider domain pub git_provider: String, /// The configured repo @@ -183,6 +197,12 @@ pub struct StackListItemInfo { pub latest_hash: Option, } +impl StackListItemInfo { + pub fn update_available(&self) -> bool { + self.services.iter().any(|s| s.update_available) + } +} + #[typeshare] #[derive(Debug, Clone, Serialize, Deserialize)] #[cfg_attr(feature = "utoipa", derive(utoipa::ToSchema))] @@ -803,10 +823,16 @@ pub struct StackServiceNames { pub image_digest: Option, } +/// A stack service, whether server or swarm based. #[typeshare] #[derive(Debug, Clone, Default, Serialize, Deserialize)] #[cfg_attr(feature = "utoipa", derive(utoipa::ToSchema))] pub struct StackService { + /// The stack which the service is a part of. + pub stack_id: String, + /// The name of the stack which the service is a part of. + #[serde(default)] + pub stack_name: String, /// The service name pub service: String, /// The service image @@ -815,10 +841,90 @@ pub struct StackService { pub container: Option, /// The service (Swarm mode) pub swarm_service: Option, + /// The service state + pub state: StackServiceState, /// The service image digests pub image_digests: Option>, } +/// Combined state options for +/// both Server and Swarm based Stacks. +#[typeshare] +#[derive( + Debug, + Clone, + Copy, + PartialEq, + Eq, + PartialOrd, + Ord, + Default, + Serialize, + Deserialize, + Display, +)] +#[cfg_attr(feature = "utoipa", derive(utoipa::ToSchema))] +pub enum StackServiceState { + /// (Swarm) All tasks OK + Healthy, + /// (Swarm) Some tasks don't match desired state + Unhealthy, + /// (Swarm) All tasks down. + Down, + /// (Container) Container is running + Running, + /// (Container) Container is created + Created, + /// (Container) Container is paused + Paused, + /// (Container) Container is restarting + Restarting, + /// (Container) Container is exited + Exited, + /// (Container) Container is stopping + Stopping, + /// (Container) Container is removing + Removing, + /// (Container) Container is dead + Dead, + /// Unknown case + #[default] + Unknown, +} + +impl From for StackServiceState { + fn from(value: SwarmState) -> Self { + match value { + SwarmState::Healthy => StackServiceState::Healthy, + SwarmState::Unhealthy => StackServiceState::Unhealthy, + SwarmState::Down => StackServiceState::Down, + SwarmState::Unknown => StackServiceState::Unknown, + } + } +} + +impl From for StackServiceState { + fn from(value: ContainerStateStatusEnum) -> Self { + match value { + ContainerStateStatusEnum::Running => StackServiceState::Running, + ContainerStateStatusEnum::Created => StackServiceState::Created, + ContainerStateStatusEnum::Paused => StackServiceState::Paused, + ContainerStateStatusEnum::Restarting => { + StackServiceState::Restarting + } + ContainerStateStatusEnum::Exited => StackServiceState::Exited, + ContainerStateStatusEnum::Stopping => { + StackServiceState::Stopping + } + ContainerStateStatusEnum::Removing => { + StackServiceState::Removing + } + ContainerStateStatusEnum::Dead => StackServiceState::Dead, + ContainerStateStatusEnum::Empty => StackServiceState::Unknown, + } + } +} + #[typeshare] #[derive(Serialize, Deserialize, Debug, Clone, Copy, Default)] #[cfg_attr(feature = "utoipa", derive(utoipa::ToSchema))] @@ -836,6 +942,23 @@ pub struct StackActionState { #[typeshare] pub type StackQuery = ResourceQuery; +#[typeshare] +#[derive( + Debug, Clone, Copy, Default, PartialEq, Eq, Serialize, Deserialize, +)] +#[cfg_attr(feature = "utoipa", derive(utoipa::ToSchema))] +pub enum StackSortBy { + /// Sort by name. Default. + #[default] + Name, + /// Sort by source repo. + Source, + /// Sort by host Server / Swarm name. + Host, + /// Sort by state. + State, +} + #[typeshare] #[derive( Serialize, Deserialize, Debug, Clone, Default, DefaultBuilder, @@ -847,6 +970,11 @@ pub struct StackQuerySpecifics { /// Only accepts Server id (not name). #[serde(default)] pub server_ids: Vec, + /// Query only for Stacks on these Swarms. + /// If empty, does not filter by Swarm. + /// Only accepts Swarm id (not name). + #[serde(default)] + pub swarm_ids: Vec, /// Query only for Stacks with these linked repos. /// Only accepts Repo id (not name). #[serde(default)] @@ -857,6 +985,10 @@ pub struct StackQuerySpecifics { /// Query only for Stack with available image updates. #[serde(default)] pub update_available: bool, + /// Query only for Stacks matching these states. + /// If empty, does not filter by state. + #[serde(default)] + pub states: Vec, } impl super::resource::AddFilters for StackQuerySpecifics { @@ -865,6 +997,10 @@ impl super::resource::AddFilters for StackQuerySpecifics { filters .insert("config.server_id", doc! { "$in": &self.server_ids }); } + if !self.swarm_ids.is_empty() { + filters + .insert("config.swarm_id", doc! { "$in": &self.swarm_ids }); + } if !self.linked_repos.is_empty() { filters.insert( "config.linked_repo", diff --git a/client/core/rs/src/entities/stats.rs b/client/core/rs/src/entities/stats.rs index da22cb9c1f..51292dfc7d 100644 --- a/client/core/rs/src/entities/stats.rs +++ b/client/core/rs/src/entities/stats.rs @@ -18,10 +18,16 @@ pub struct SystemInformation { pub kernel: Option, /// Physical core count pub core_count: Option, + /// Logical core count. + pub logical_core_count: Option, /// System hostname based off DNS pub host_name: Option, /// The CPU's brand + #[serde(default)] pub cpu_brand: String, + /// CPU architecture (eg. x86_64, aarch64, arm64) + #[serde(default)] + pub cpu_arch: String, } /// System stats stored on the database. @@ -50,6 +56,22 @@ pub struct SystemStatsRecord { pub mem_used_gb: f64, /// Total memory in GB pub mem_total_gb: f64, + /// [2.3.0+] + /// Reclaimable page cache + buffers in GB. + #[serde(default)] + pub mem_buff_cache_gb: f64, + /// [2.3.0+] + /// ZFS ARC cache in GB. 0 when ZFS is not present. + #[serde(default)] + pub mem_zfs_arc_gb: f64, + /// [2.3.0+] + /// Total swap in GB. + #[serde(default)] + pub swap_total_gb: f64, + /// [2.3.0+] + /// Used swap in GB. + #[serde(default)] + pub swap_used_gb: f64, /// Disk used in GB pub disk_used_gb: f64, /// Total disk size in GB @@ -83,10 +105,27 @@ pub struct SystemStats { /// It may be different than mem_total_gb - mem_used_gb. #[serde(default)] pub mem_free_gb: f64, - /// Used memory in GB. 'Total' - 'Available' (not free) memory. + /// Used memory in GB. 'Total' - 'Available' (not free) memory, + /// with the (reclaimable) ZFS ARC cache subtracted out. pub mem_used_gb: f64, /// Total memory in GB pub mem_total_gb: f64, + /// [2.3.0+] + /// Reclaimable page cache + buffers in GB. + #[serde(default)] + pub mem_buff_cache_gb: f64, + /// [2.3.0+] + /// ZFS ARC cache in GB. 0 when ZFS is not present. + #[serde(default)] + pub mem_zfs_arc_gb: f64, + /// [2.3.0+] + /// Total swap in GB. + #[serde(default)] + pub swap_total_gb: f64, + /// [2.3.0+] + /// Used swap in GB. + #[serde(default)] + pub swap_used_gb: f64, /// Breakdown of individual disks, ie their usages, sizes, and mount points pub disks: Vec, /// Network ingress usage in MB @@ -107,6 +146,76 @@ pub struct SystemStats { pub refresh_list_ts: I64, } +/// Realtime minimal system stats data (zero allocation) +#[typeshare] +#[derive(Serialize, Deserialize, Debug, Default, Clone, Copy)] +#[cfg_attr(feature = "utoipa", derive(utoipa::ToSchema))] +pub struct MinimalSystemStats { + /// Cpu usage percentage + pub cpu_perc: f32, + /// Load average (1m, 5m, 15m) + pub load_average: SystemLoadAverage, + /// This is really the 'Free' memory, not the 'Available' memory. + /// It may be different than mem_total_gb - mem_used_gb. + pub mem_free_gb: f64, + /// Used memory in GB. 'Total' - 'Available' (not free) memory, + /// with the (reclaimable) ZFS ARC cache subtracted out. + pub mem_used_gb: f64, + /// Total memory in GB + pub mem_total_gb: f64, + /// Reclaimable page cache + buffers in GB. + pub mem_buff_cache_gb: f64, + /// ZFS ARC cache in GB. 0 when ZFS is not present. + pub mem_zfs_arc_gb: f64, + /// Total swap in GB. + pub swap_total_gb: f64, + /// Used swap in GB. + pub swap_used_gb: f64, + /// Total size of all disks combined in GB + pub disk_total_gb: f64, + /// Used portion of all disks combined in GB + pub disk_used_gb: f64, + /// Network ingress usage in MB + pub network_ingress_bytes: f64, + /// Network egress usage in MB + pub network_egress_bytes: f64, + /// The rate the system stats are being polled from the system + pub polling_rate: Timelength, + /// Unix timestamp in milliseconds when stats were last polled + pub refresh_ts: I64, + /// Unix timestamp in milliseconds when disk list was last refreshed + pub refresh_list_ts: I64, +} + +impl From<&SystemStats> for MinimalSystemStats { + fn from(value: &SystemStats) -> Self { + Self { + cpu_perc: value.cpu_perc, + load_average: value.load_average, + mem_free_gb: value.mem_free_gb, + mem_used_gb: value.mem_used_gb, + mem_total_gb: value.mem_total_gb, + mem_buff_cache_gb: value.mem_buff_cache_gb, + mem_zfs_arc_gb: value.mem_zfs_arc_gb, + swap_total_gb: value.swap_total_gb, + swap_used_gb: value.swap_used_gb, + disk_total_gb: value + .disks + .iter() + .fold(0.0, |total, disk| total + disk.total_gb), + disk_used_gb: value + .disks + .iter() + .fold(0.0, |used, disk| used + disk.used_gb), + network_ingress_bytes: value.network_ingress_bytes, + network_egress_bytes: value.network_egress_bytes, + polling_rate: value.polling_rate, + refresh_ts: value.refresh_ts, + refresh_list_ts: value.refresh_list_ts, + } + } +} + /// Info for a single disk mounted on the system. #[typeshare] #[derive(Serialize, Deserialize, Debug, Clone)] @@ -187,7 +296,7 @@ pub struct SystemProcess { } #[typeshare] -#[derive(Serialize, Deserialize, Debug, Default, Clone)] +#[derive(Serialize, Deserialize, Debug, Default, Clone, Copy)] #[cfg_attr(feature = "utoipa", derive(utoipa::ToSchema))] pub struct SystemLoadAverage { /// 1m load average diff --git a/client/core/rs/src/entities/swarm.rs b/client/core/rs/src/entities/swarm.rs index 966d64cc03..1a55e933bd 100644 --- a/client/core/rs/src/entities/swarm.rs +++ b/client/core/rs/src/entities/swarm.rs @@ -145,6 +145,19 @@ pub struct SwarmActionState {} #[typeshare] pub type SwarmQuery = ResourceQuery; +#[typeshare] +#[derive( + Debug, Clone, Copy, Default, PartialEq, Eq, Serialize, Deserialize, +)] +#[cfg_attr(feature = "utoipa", derive(utoipa::ToSchema))] +pub enum SwarmSortBy { + /// Sort by name. Default. + #[default] + Name, + /// Sort by state. + State, +} + #[typeshare] #[derive( Serialize, Deserialize, Debug, Clone, Default, DefaultBuilder, diff --git a/client/core/rs/src/entities/sync.rs b/client/core/rs/src/entities/sync.rs index 0c720e0477..c8c49bfc54 100644 --- a/client/core/rs/src/entities/sync.rs +++ b/client/core/rs/src/entities/sync.rs @@ -36,6 +36,9 @@ pub struct ResourceSyncListItemInfo { pub resource_path: Vec, /// Linked repo, if one is attached. pub linked_repo: String, + /// The name of the linked repo, if one is attached. + #[serde(default)] + pub linked_repo_name: String, /// The git provider domain. pub git_provider: String, /// The Github repo used as the source of the sync resources @@ -430,6 +433,23 @@ pub struct ResourceSyncActionState { pub type ResourceSyncQuery = ResourceQuery; +#[typeshare] +#[derive( + Debug, Clone, Copy, Default, PartialEq, Eq, Serialize, Deserialize, +)] +#[cfg_attr(feature = "utoipa", derive(utoipa::ToSchema))] +pub enum ResourceSyncSortBy { + /// Sort by name. Default. + #[default] + Name, + /// Sort by source repo. + Source, + /// Sort by branch. + Branch, + /// Sort by state. + State, +} + #[typeshare] #[derive( Serialize, Deserialize, Debug, Clone, Default, DefaultBuilder, @@ -437,7 +457,13 @@ pub type ResourceSyncQuery = #[cfg_attr(feature = "utoipa", derive(utoipa::ToSchema))] pub struct ResourceSyncQuerySpecifics { /// Filter syncs by their repo. + #[serde(default)] pub repos: Vec, + + /// Query only for Builds with these linked repos. + /// Only accepts Repo id (not name). + #[serde(default)] + pub linked_repos: Vec, } impl super::resource::AddFilters for ResourceSyncQuerySpecifics { @@ -445,5 +471,11 @@ impl super::resource::AddFilters for ResourceSyncQuerySpecifics { if !self.repos.is_empty() { filters.insert("config.repo", doc! { "$in": &self.repos }); } + if !self.linked_repos.is_empty() { + filters.insert( + "config.linked_repo", + doc! { "$in": &self.linked_repos }, + ); + } } } diff --git a/client/core/rs/src/entities/terminal.rs b/client/core/rs/src/entities/terminal.rs index 7f5caeaed6..3f789bbdcf 100644 --- a/client/core/rs/src/entities/terminal.rs +++ b/client/core/rs/src/entities/terminal.rs @@ -16,6 +16,10 @@ pub struct Terminal { pub name: String, /// The target resource of the Terminal. pub target: TerminalTarget, + /// The name of the target resource (Server / Stack / Deployment). + /// Resolved by Core when listing all terminals for a user. + #[serde(default, skip_serializing_if = "Option::is_none")] + pub target_name: Option, /// The command used to init the shell. pub command: String, /// The size of the terminal history in memory. @@ -25,6 +29,25 @@ pub struct Terminal { pub created_at: I64, } +#[typeshare] +#[derive( + Debug, Clone, Copy, Default, PartialEq, Eq, Serialize, Deserialize, +)] +#[cfg_attr(feature = "utoipa", derive(utoipa::ToSchema))] +pub enum TerminalSortBy { + /// Sort by name. Default. + #[default] + Name, + /// Sort by target. + Target, + /// Sort by init command. + Command, + /// Sort by stored size. + Size, + /// Sort by created timestamp. + Created, +} + #[typeshare] #[derive( Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Serialize, Deserialize, diff --git a/client/core/rs/src/entities/update.rs b/client/core/rs/src/entities/update.rs index 448f1234e1..071f05fdf3 100644 --- a/client/core/rs/src/entities/update.rs +++ b/client/core/rs/src/entities/update.rs @@ -1,6 +1,6 @@ use async_timing_util::unix_timestamp_ms; use serde::{Deserialize, Serialize}; -use strum::{Display, EnumString}; +use strum::{AsRefStr, Display, EnumString}; use typeshare::typeshare; use crate::entities::{ @@ -203,22 +203,30 @@ impl Log { (true, true) => String::from("No log"), } } + + #[cfg(feature = "svi")] + pub fn sanitize(&mut self, replacers: &Vec<(String, String)>) { + self.command = svi::replace_in_string(&self.command, replacers); + self.stdout = svi::replace_in_string(&self.stdout, replacers); + self.stderr = svi::replace_in_string(&self.stderr, replacers); + } } /// An update's status #[typeshare] #[derive( + Default, + Debug, + Clone, + Copy, + PartialEq, + Eq, + Hash, Serialize, Deserialize, - Debug, Display, EnumString, - PartialEq, - Hash, - Eq, - Clone, - Copy, - Default, + AsRefStr, )] #[cfg_attr(feature = "utoipa", derive(utoipa::ToSchema))] pub enum UpdateStatus { diff --git a/client/core/ts/package.json b/client/core/ts/package.json index 96e668a679..061356db56 100644 --- a/client/core/ts/package.json +++ b/client/core/ts/package.json @@ -1,6 +1,6 @@ { "name": "komodo_client", - "version": "2.2.0", + "version": "2.3.0", "description": "Komodo client package", "homepage": "https://komo.do", "main": "dist/lib.js", @@ -13,7 +13,7 @@ "build": "tsc" }, "dependencies": { - "mogh_auth_client": "^1.5.0" + "mogh_auth_client": "^1.7.0" }, "devDependencies": { "typescript": "^6.0.2" diff --git a/client/core/ts/src/responses.ts b/client/core/ts/src/responses.ts index 46bbe9aacb..808ac85d8e 100644 --- a/client/core/ts/src/responses.ts +++ b/client/core/ts/src/responses.ts @@ -5,7 +5,7 @@ export type ReadResponses = { GetCoreInfo: Types.GetCoreInfoResponse; ListSecrets: Types.ListSecretsResponse; ListGitProvidersFromConfig: Types.ListGitProvidersFromConfigResponse; - ListDockerRegistriesFromConfig: Types.ListDockerRegistriesFromConfigResponse; + ListImageRegistriesFromConfig: Types.ListImageRegistriesFromConfigResponse; // ==== SWARM ==== GetSwarmsSummary: Types.GetSwarmsSummaryResponse; @@ -43,21 +43,21 @@ export type ReadResponses = { ListTerminals: Types.ListTerminalsResponse; // ==== DOCKER ==== - GetDockerContainersSummary: Types.GetDockerContainersSummaryResponse; - ListAllDockerContainers: Types.ListAllDockerContainersResponse; - ListDockerContainers: Types.ListDockerContainersResponse; - InspectDockerContainer: Types.InspectDockerContainerResponse; + GetContainersSummary: Types.GetContainersSummaryResponse; + ListAllContainers: Types.ListAllContainersResponse; + ListContainers: Types.ListContainersResponse; + InspectContainer: Types.InspectContainerResponse; GetResourceMatchingContainer: Types.GetResourceMatchingContainerResponse; GetContainerLog: Types.GetContainerLogResponse; SearchContainerLog: Types.SearchContainerLogResponse; ListComposeProjects: Types.ListComposeProjectsResponse; - ListDockerNetworks: Types.ListDockerNetworksResponse; - InspectDockerNetwork: Types.InspectDockerNetworkResponse; - ListDockerImages: Types.ListDockerImagesResponse; - InspectDockerImage: Types.InspectDockerImageResponse; - ListDockerImageHistory: Types.ListDockerImageHistoryResponse; - ListDockerVolumes: Types.ListDockerVolumesResponse; - InspectDockerVolume: Types.InspectDockerVolumeResponse; + ListNetworks: Types.ListNetworksResponse; + InspectNetwork: Types.InspectNetworkResponse; + ListImages: Types.ListImagesResponse; + InspectImage: Types.InspectImageResponse; + ListImageHistory: Types.ListImageHistoryResponse; + ListVolumes: Types.ListVolumesResponse; + InspectVolume: Types.InspectVolumeResponse; // ==== SERVER STATS ==== GetSystemInformation: Types.GetSystemInformationResponse; @@ -76,6 +76,7 @@ export type ReadResponses = { ListStacks: Types.ListStacksResponse; ListFullStacks: Types.ListFullStacksResponse; ListStackServices: Types.ListStackServicesResponse; + ListAllStackServices: Types.ListAllStackServicesResponse; ListCommonStackExtraArgs: Types.ListCommonStackExtraArgsResponse; ListCommonStackBuildExtraArgs: Types.ListCommonStackBuildExtraArgsResponse; @@ -183,8 +184,8 @@ export type ReadResponses = { // ==== PROVIDER ==== GetGitProviderAccount: Types.GetGitProviderAccountResponse; ListGitProviderAccounts: Types.ListGitProviderAccountsResponse; - GetDockerRegistryAccount: Types.GetDockerRegistryAccountResponse; - ListDockerRegistryAccounts: Types.ListDockerRegistryAccountsResponse; + GetImageRegistryAccount: Types.GetImageRegistryAccountResponse; + ListImageRegistryAccounts: Types.ListImageRegistryAccountsResponse; // ==== ONBOARDING KEY ==== ListOnboardingKeys: Types.ListOnboardingKeysResponse; @@ -342,9 +343,9 @@ export type WriteResponses = { CreateGitProviderAccount: Types.CreateGitProviderAccountResponse; UpdateGitProviderAccount: Types.UpdateGitProviderAccountResponse; DeleteGitProviderAccount: Types.DeleteGitProviderAccountResponse; - CreateDockerRegistryAccount: Types.CreateDockerRegistryAccountResponse; - UpdateDockerRegistryAccount: Types.UpdateDockerRegistryAccountResponse; - DeleteDockerRegistryAccount: Types.DeleteDockerRegistryAccountResponse; + CreateImageRegistryAccount: Types.CreateImageRegistryAccountResponse; + UpdateImageRegistryAccount: Types.UpdateImageRegistryAccountResponse; + DeleteImageRegistryAccount: Types.DeleteImageRegistryAccountResponse; // ==== ALERT ==== CloseAlert: Types.NoData; @@ -396,10 +397,12 @@ export type ExecuteResponses = { // ==== PROCEDURE ==== RunProcedure: Types.Update; BatchRunProcedure: Types.BatchExecutionResponse; + CancelProcedure: Types.Update; // ==== ACTION ==== RunAction: Types.Update; BatchRunAction: Types.BatchExecutionResponse; + CancelAction: Types.Update; // ==== SYNC ==== RunSync: Types.Update; diff --git a/client/core/ts/src/types.ts b/client/core/ts/src/types.ts index cb7f0c110f..a3ccdb6229 100644 --- a/client/core/ts/src/types.ts +++ b/client/core/ts/src/types.ts @@ -156,14 +156,14 @@ export interface ResourceListItem { } export enum ActionState { - /** Unknown case */ - Unknown = "Unknown", + /** Currently running */ + Running = "Running", /** Last clone / pull successful (or never cloned) */ Ok = "Ok", /** Last clone / pull failed */ Failed = "Failed", - /** Currently running */ - Running = "Running", + /** Unknown case */ + Unknown = "Unknown", } export interface ActionListItemInfo { @@ -185,6 +185,13 @@ export interface ActionListItemInfo { export type ActionListItem = ResourceListItem; +export enum TagQueryBehavior { + /** Returns resources which have strictly all the tags */ + All = "All", + /** Returns resources which have one or more of the tags */ + Any = "Any", +} + export enum TemplatesQueryBehavior { /** Include templates in results. Default. */ Include = "Include", @@ -194,25 +201,34 @@ export enum TemplatesQueryBehavior { Only = "Only", } -export enum TagQueryBehavior { - /** Returns resources which have strictly all the tags */ - All = "All", - /** Returns resources which have one or more of the tags */ - Any = "Any", -} - /** Passing empty Vec is the same as not filtering by that field */ export interface ResourceQuery { + /** + * List of search terms. Names must contain + * all terms to match. + */ + terms?: string[]; + /** List of exact names to return */ names?: string[]; - templates?: TemplatesQueryBehavior; /** Pass Vec of tag ids or tag names */ tags?: string[]; /** 'All' or 'Any' */ tag_behavior?: TagQueryBehavior; + templates?: TemplatesQueryBehavior; specific?: T; } export interface ActionQuerySpecifics { + /** + * Query only for Actions matching these states. + * If empty, does not filter by state. + */ + states?: ActionState[]; + /** + * Query only for Actions with (or without) + * a schedule configured. + */ + scheduled?: boolean; } export type ActionQuery = ResourceQuery; @@ -471,11 +487,13 @@ export enum Operation { RenameProcedure = "RenameProcedure", DeleteProcedure = "DeleteProcedure", RunProcedure = "RunProcedure", + CancelProcedure = "CancelProcedure", CreateAction = "CreateAction", UpdateAction = "UpdateAction", RenameAction = "RenameAction", DeleteAction = "DeleteAction", RunAction = "RunAction", + CancelAction = "CancelAction", CreateResourceSync = "CreateResourceSync", UpdateResourceSync = "UpdateResourceSync", RenameResourceSync = "RenameResourceSync", @@ -795,6 +813,8 @@ export interface BuildListItemInfo { dockerfile_contents: boolean; /** Linked repo, if one is attached. */ linked_repo: string; + /** The name of the linked repo, if one is attached. */ + linked_repo_name?: string; /** The git provider domain */ git_provider: string; /** The repo used as the source of the build */ @@ -816,11 +836,21 @@ export type BuildListItem = ResourceListItem; export interface BuildQuerySpecifics { builder_ids?: string[]; repos?: string[]; + /** + * Query only for Builds with these linked repos. + * Only accepts Repo id (not name). + */ + linked_repos?: string[]; /** * query for builds last built more recently than this timestamp * defaults to 0 which is a no op */ built_since?: I64; + /** + * Query only for Builds matching these states. + * If empty, does not filter by state. + */ + states?: BuildState[]; } export type BuildQuery = ResourceQuery; @@ -898,9 +928,11 @@ export type Execution = /** Run the target procedure. (alias: `procedure`, `pr`) */ | { type: "RunProcedure", params: RunProcedure } | { type: "BatchRunProcedure", params: BatchRunProcedure } + | { type: "CancelProcedure", params: CancelProcedure } /** Run the target action. (alias: `action`, `ac`) */ | { type: "RunAction", params: RunAction } | { type: "BatchRunAction", params: BatchRunAction } + | { type: "CancelAction", params: CancelAction } /** Execute a Resource Sync. (alias: `sync`) */ | { type: "RunSync", params: RunSync } /** Commit a Resource Sync. (alias: `commit`) */ @@ -1031,22 +1063,26 @@ export interface CreateApiKeyResponse { export type CreateApiKeyForServiceUserResponse = CreateApiKeyResponse; -/** Configuration to access private image repositories on various registries. */ -export interface DockerRegistryAccount { +/** + * Configuration to access private git repos from various git providers. + * Note. Cannot create two accounts with the same domain and username. + */ +export interface GitProviderAccount { /** - * The Mongo ID of the docker registry account. + * The Mongo ID of the git provider account. * This field is de/serialized from/to JSON as - * `{ "_id": { "$oid": "..." }, ...(rest of DockerRegistryAccount) }` + * `{ "_id": { "$oid": "..." }, ...(rest of serialized User) }` */ _id?: MongoId; /** * The domain of the provider. * - * For docker registry, this can include 'http://...', - * however this is not recommended and won't work unless "insecure registries" are enabled - * on your hosts. See . + * For git, this cannot include the protocol eg 'http://', + * which is controlled with 'https' field. */ domain: string; + /** Whether git provider is accessed over http or https. */ + https: boolean; /** The account username */ username?: string; /** @@ -1056,28 +1092,24 @@ export interface DockerRegistryAccount { token?: string; } -export type CreateDockerRegistryAccountResponse = DockerRegistryAccount; +export type CreateGitProviderAccountResponse = GitProviderAccount; -/** - * Configuration to access private git repos from various git providers. - * Note. Cannot create two accounts with the same domain and username. - */ -export interface GitProviderAccount { +/** Configuration to access private image repositories on various registries. */ +export interface ImageRegistryAccount { /** - * The Mongo ID of the git provider account. + * The Mongo ID of the docker registry account. * This field is de/serialized from/to JSON as - * `{ "_id": { "$oid": "..." }, ...(rest of serialized User) }` + * `{ "_id": { "$oid": "..." }, ...(rest of ImageRegistryAccount) }` */ _id?: MongoId; /** * The domain of the provider. * - * For git, this cannot include the protocol eg 'http://', - * which is controlled with 'https' field. + * For docker registry, this can include 'http://...', + * however this is not recommended and won't work unless "insecure registries" are enabled + * on your hosts. See . */ domain: string; - /** Whether git provider is accessed over http or https. */ - https: boolean; /** The account username */ username?: string; /** @@ -1087,7 +1119,7 @@ export interface GitProviderAccount { token?: string; } -export type CreateGitProviderAccountResponse = GitProviderAccount; +export type CreateImageRegistryAccountResponse = ImageRegistryAccount; export type UserConfig = /** User that logs in with username / password */ @@ -1210,10 +1242,10 @@ export type CreateVariableResponse = Variable; export type DeleteApiKeyForServiceUserResponse = NoData; -export type DeleteDockerRegistryAccountResponse = DockerRegistryAccount; - export type DeleteGitProviderAccountResponse = GitProviderAccount; +export type DeleteImageRegistryAccountResponse = ImageRegistryAccount; + /** * An public key used to authenticate new Periphery -> Core connections * to join Komodo as a newly created Server. @@ -1308,6 +1340,11 @@ export interface DeploymentConfig { * swarm_id overrides server_id and the Deployment will be in Swarm mode. */ server_id?: string; + /** + * Specify a custom container / service name, + * if different from Deployment name. + */ + custom_name?: string; /** * The image which the deployment deploys. * Can either be a user inputted image, or a Komodo Build. @@ -1407,6 +1444,12 @@ export interface DeploymentInfo { * This includes both the image name / tag, and the specific digest hash. */ latest_image_digest?: ImageDigest; + /** + * The container / service name used at the time of the last deploy. + * Kept to match the Deployment to its container / service + * even if the name configuration changes before the next deploy. + */ + deployed_name?: string; } export type Deployment = Resource; @@ -1451,14 +1494,24 @@ export interface DeploymentListItemInfo { state: DeploymentState; /** The status of the docker container (eg. up 12 hours, exited 5 minutes ago.) */ status?: string; + /** + * The container / service name, if different than + * the deployment name. Uses the currently deployed name + * if deployed, else the configured custom name. + */ + custom_name: string; /** The image attached to the deployment. */ image: string; /** Whether there is a newer image available at the same tag. */ update_available: boolean; /** The swarm that deployment is deployed on, when in Swarm mode. */ swarm_id: string; + /** The name of the swarm that deployment is deployed on, when in Swarm mode. */ + swarm_name?: string; /** The server that deployment is deployed on, when in Server mode. */ server_id: string; + /** The name of the server that deployment is deployed on, when in Server mode. */ + server_name?: string; /** An attached Komodo Build, if it exists. */ build_id?: string; } @@ -1472,6 +1525,12 @@ export interface DeploymentQuerySpecifics { * Only accepts Server id (not name). */ server_ids?: string[]; + /** + * Query only for Deployments on these Swarms. + * If empty, does not filter by Swarm. + * Only accepts Swarm id (not name). + */ + swarm_ids?: string[]; /** * Query only for Deployments with these Builds attached. * If empty, does not filter by Build. @@ -1480,6 +1539,11 @@ export interface DeploymentQuerySpecifics { build_ids?: string[]; /** Query only for Deployments with available image updates. */ update_available?: boolean; + /** + * Query only for Deployments matching these states. + * If empty, does not filter by state. + */ + states?: DeploymentState[]; } export type DeploymentQuery = ResourceQuery; @@ -1875,10 +1939,10 @@ export interface ContainerStats { export type GetDeploymentStatsResponse = ContainerStats; -export type GetDockerRegistryAccountResponse = DockerRegistryAccount; - export type GetGitProviderAccountResponse = GitProviderAccount; +export type GetImageRegistryAccountResponse = ImageRegistryAccount; + export enum Timelength { /** `1-sec` */ OneSecond = "1-sec", @@ -2251,15 +2315,17 @@ export interface ServerActionState { /** Server currently pruning system */ pruning_system: boolean; /** Server currently starting containers. */ - starting_containers: boolean; + starting_containers: number; /** Server currently restarting containers. */ - restarting_containers: boolean; + restarting_containers: number; /** Server currently pausing containers. */ - pausing_containers: boolean; + pausing_containers: number; /** Server currently unpausing containers. */ - unpausing_containers: boolean; + unpausing_containers: number; /** Server currently stopping containers. */ - stopping_containers: boolean; + stopping_containers: number; + /** Server currently destroying containers. */ + destroying_containers: number; } export type GetServerActionStateResponse = ServerActionState; @@ -2792,10 +2858,17 @@ export interface SystemInformation { kernel?: string; /** Physical core count */ core_count?: number; + /** + * Logical core count. If available, + * used to interpret system load accurately. + */ + logical_core_count?: number; /** System hostname based off DNS */ host_name?: string; /** The CPU's brand */ - cpu_brand: string; + cpu_brand?: string; + /** CPU architecture (eg. x86_64, aarch64, arm64) */ + cpu_arch?: string; } export type GetSystemInformationResponse = SystemInformation; @@ -2834,10 +2907,33 @@ export interface SystemStats { * It may be different than mem_total_gb - mem_used_gb. */ mem_free_gb?: number; - /** Used memory in GB. 'Total' - 'Available' (not free) memory. */ + /** + * Used memory in GB. 'Total' - 'Available' (not free) memory, + * with the (reclaimable) ZFS ARC cache subtracted out. + */ mem_used_gb: number; /** Total memory in GB */ mem_total_gb: number; + /** + * [2.3.0+] + * Reclaimable page cache + buffers in GB. + */ + mem_buff_cache_gb?: number; + /** + * [2.3.0+] + * ZFS ARC cache in GB. 0 when ZFS is not present. + */ + mem_zfs_arc_gb?: number; + /** + * [2.3.0+] + * Total swap in GB. + */ + swap_total_gb?: number; + /** + * [2.3.0+] + * Used swap in GB. + */ + swap_used_gb?: number; /** Breakdown of individual disks, ie their usages, sizes, and mount points */ disks: SingleDiskUsage[]; /** Network ingress usage in MB */ @@ -2962,11 +3058,11 @@ export type GetVariableResponse = Variable; export enum ContainerStateStatusEnum { Running = "running", Created = "created", - Paused = "paused", Restarting = "restarting", - Exited = "exited", Stopping = "stopping", Removing = "removing", + Paused = "paused", + Exited = "exited", Dead = "dead", Empty = "", } @@ -3509,6 +3605,8 @@ export interface Container { NetworkSettings?: NetworkSettings; } +export type InspectContainerResponse = Container; + export type InspectDeploymentContainerResponse = Container; /** The service mode. */ @@ -4069,8 +4167,6 @@ export interface SwarmService { export type InspectDeploymentSwarmServiceResponse = SwarmService; -export type InspectDockerContainerResponse = Container; - /** Describes the platform which the image in the manifest runs on, as defined in the [OCI Image Index Specification](https://github.com/opencontainers/image-spec/blob/v1.0.1/image-index.md). */ export interface OciPlatform { /** The CPU architecture, for example `amd64` or `ppc64`. */ @@ -4227,7 +4323,7 @@ export interface Image { Metadata?: ImageInspectMetadata; } -export type InspectDockerImageResponse = Image; +export type InspectImageResponse = Image; export interface IpamConfig { Subnet?: string; @@ -4272,157 +4368,7 @@ export interface Network { Labels?: Record; } -export type InspectDockerNetworkResponse = Network; - -export enum VolumeScopeEnum { - Empty = "", - Local = "local", - Global = "global", -} - -export enum ClusterVolumeSpecAccessModeScopeEnum { - Empty = "", - Single = "single", - Multi = "multi", -} - -export enum ClusterVolumeSpecAccessModeSharingEnum { - Empty = "", - None = "none", - Readonly = "readonly", - Onewriter = "onewriter", - All = "all", -} - -/** One cluster volume secret entry. Defines a key-value pair that is passed to the plugin. */ -export interface ClusterVolumeSpecAccessModeSecrets { - /** Key is the name of the key of the key-value pair passed to the plugin. */ - Key?: string; - /** Secret is the swarm Secret object from which to read data. This can be a Secret name or ID. The Secret data is retrieved by swarm and used as the value of the key-value pair passed to the plugin. */ - Secret?: string; -} - -/** A map of topological domains to topological segments. For in depth details, see documentation for the Topology object in the CSI specification. */ -export interface Topology { - Segments?: Record; -} - -/** Requirements for the accessible topology of the volume. These fields are optional. For an in-depth description of what these fields mean, see the CSI specification. */ -export interface ClusterVolumeSpecAccessModeAccessibilityRequirements { - /** A list of required topologies, at least one of which the volume must be accessible from. */ - Requisite?: Topology[]; - /** A list of topologies that the volume should attempt to be provisioned in. */ - Preferred?: Topology[]; -} - -/** The desired capacity that the volume should be created with. If empty, the plugin will decide the capacity. */ -export interface ClusterVolumeSpecAccessModeCapacityRange { - /** The volume must be at least this big. The value of 0 indicates an unspecified minimum */ - RequiredBytes?: I64; - /** The volume must not be bigger than this. The value of 0 indicates an unspecified maximum. */ - LimitBytes?: I64; -} - -export enum ClusterVolumeSpecAccessModeAvailabilityEnum { - Empty = "", - Active = "active", - Pause = "pause", - Drain = "drain", -} - -/** Defines how the volume is used by tasks. */ -export interface ClusterVolumeSpecAccessMode { - /** The set of nodes this volume can be used on at one time. - `single` The volume may only be scheduled to one node at a time. - `multi` the volume may be scheduled to any supported number of nodes at a time. */ - Scope?: ClusterVolumeSpecAccessModeScopeEnum; - /** The number and way that different tasks can use this volume at one time. - `none` The volume may only be used by one task at a time. - `readonly` The volume may be used by any number of tasks, but they all must mount the volume as readonly - `onewriter` The volume may be used by any number of tasks, but only one may mount it as read/write. - `all` The volume may have any number of readers and writers. */ - Sharing?: ClusterVolumeSpecAccessModeSharingEnum; - /** Swarm Secrets that are passed to the CSI storage plugin when operating on this volume. */ - Secrets?: ClusterVolumeSpecAccessModeSecrets[]; - AccessibilityRequirements?: ClusterVolumeSpecAccessModeAccessibilityRequirements; - CapacityRange?: ClusterVolumeSpecAccessModeCapacityRange; - /** The availability of the volume for use in tasks. - `active` The volume is fully available for scheduling on the cluster - `pause` No new workloads should use the volume, but existing workloads are not stopped. - `drain` All workloads using this volume should be stopped and rescheduled, and no new ones should be started. */ - Availability?: ClusterVolumeSpecAccessModeAvailabilityEnum; -} - -/** Cluster-specific options used to create the volume. */ -export interface ClusterVolumeSpec { - /** Group defines the volume group of this volume. Volumes belonging to the same group can be referred to by group name when creating Services. Referring to a volume by group instructs Swarm to treat volumes in that group interchangeably for the purpose of scheduling. Volumes with an empty string for a group technically all belong to the same, emptystring group. */ - Group?: string; - AccessMode?: ClusterVolumeSpecAccessMode; -} - -/** Information about the global status of the volume. */ -export interface ClusterVolumeInfo { - /** The capacity of the volume in bytes. A value of 0 indicates that the capacity is unknown. */ - CapacityBytes?: I64; - /** A map of strings to strings returned from the storage plugin when the volume is created. */ - VolumeContext?: Record; - /** The ID of the volume as returned by the CSI storage plugin. This is distinct from the volume's ID as provided by Docker. This ID is never used by the user when communicating with Docker to refer to this volume. If the ID is blank, then the Volume has not been successfully created in the plugin yet. */ - VolumeID?: string; - /** The topology this volume is actually accessible from. */ - AccessibleTopology?: Topology[]; -} - -export enum ClusterVolumePublishStatusStateEnum { - Empty = "", - PendingPublish = "pending-publish", - Published = "published", - PendingNodeUnpublish = "pending-node-unpublish", - PendingControllerUnpublish = "pending-controller-unpublish", -} - -export interface ClusterVolumePublishStatus { - /** The ID of the Swarm node the volume is published on. */ - NodeID?: string; - /** The published state of the volume. * `pending-publish` The volume should be published to this node, but the call to the controller plugin to do so has not yet been successfully completed. * `published` The volume is published successfully to the node. * `pending-node-unpublish` The volume should be unpublished from the node, and the manager is awaiting confirmation from the worker that it has done so. * `pending-controller-unpublish` The volume is successfully unpublished from the node, but has not yet been successfully unpublished on the controller. */ - State?: ClusterVolumePublishStatusStateEnum; - /** A map of strings to strings returned by the CSI controller plugin when a volume is published. */ - PublishContext?: Record; -} - -/** Options and information specific to, and only present on, Swarm CSI cluster volumes. */ -export interface ClusterVolume { - /** The Swarm ID of this volume. Because cluster volumes are Swarm objects, they have an ID, unlike non-cluster volumes. This ID can be used to refer to the Volume instead of the name. */ - ID?: string; - Version?: ObjectVersion; - CreatedAt?: string; - UpdatedAt?: string; - Spec?: ClusterVolumeSpec; - Info?: ClusterVolumeInfo; - /** The status of the volume as it pertains to its publishing and use on specific nodes */ - PublishStatus?: ClusterVolumePublishStatus[]; -} - -/** Usage details about the volume. This information is used by the `GET /system/df` endpoint, and omitted in other endpoints. */ -export interface VolumeUsageData { - /** Amount of disk space used by the volume (in bytes). This information is only available for volumes created with the `\"local\"` volume driver. For volumes created with other volume drivers, this field is set to `-1` (\"not available\") */ - Size: I64; - /** The number of containers referencing this volume. This field is set to `-1` if the reference-count is not available. */ - RefCount: I64; -} - -export interface Volume { - /** Name of the volume. */ - Name: string; - /** Name of the volume driver used by the volume. */ - Driver: string; - /** Mount path of the volume on the host. */ - Mountpoint: string; - /** Date/Time the volume was created. */ - CreatedAt?: string; - /** Low-level details about the volume, provided by the volume driver. Details are returned as a map with key/value pairs: `{\"key\":\"value\",\"key2\":\"value2\"}`. The `Status` field is optional, and is omitted if the volume driver does not support this feature. */ - Status?: string[]; - /** User-defined key/value metadata. */ - Labels?: Record; - /** The level at which the volume exists. Either `global` for cluster-wide, or `local` for machine level. */ - Scope?: VolumeScopeEnum; - ClusterVolume?: ClusterVolume; - /** The driver specific options used when creating the volume. */ - Options?: Record; - UsageData?: VolumeUsageData; -} - -export type InspectDockerVolumeResponse = Volume; +export type InspectNetworkResponse = Network; export type InspectStackContainerResponse = Container; @@ -4897,9 +4843,159 @@ export interface SwarmTask { export type InspectSwarmTaskResponse = SwarmTask; -export type JsonObject = any; - -export type ListActionsResponse = ActionListItem[]; +export enum VolumeScopeEnum { + Empty = "", + Local = "local", + Global = "global", +} + +export enum ClusterVolumeSpecAccessModeScopeEnum { + Empty = "", + Single = "single", + Multi = "multi", +} + +export enum ClusterVolumeSpecAccessModeSharingEnum { + Empty = "", + None = "none", + Readonly = "readonly", + Onewriter = "onewriter", + All = "all", +} + +/** One cluster volume secret entry. Defines a key-value pair that is passed to the plugin. */ +export interface ClusterVolumeSpecAccessModeSecrets { + /** Key is the name of the key of the key-value pair passed to the plugin. */ + Key?: string; + /** Secret is the swarm Secret object from which to read data. This can be a Secret name or ID. The Secret data is retrieved by swarm and used as the value of the key-value pair passed to the plugin. */ + Secret?: string; +} + +/** A map of topological domains to topological segments. For in depth details, see documentation for the Topology object in the CSI specification. */ +export interface Topology { + Segments?: Record; +} + +/** Requirements for the accessible topology of the volume. These fields are optional. For an in-depth description of what these fields mean, see the CSI specification. */ +export interface ClusterVolumeSpecAccessModeAccessibilityRequirements { + /** A list of required topologies, at least one of which the volume must be accessible from. */ + Requisite?: Topology[]; + /** A list of topologies that the volume should attempt to be provisioned in. */ + Preferred?: Topology[]; +} + +/** The desired capacity that the volume should be created with. If empty, the plugin will decide the capacity. */ +export interface ClusterVolumeSpecAccessModeCapacityRange { + /** The volume must be at least this big. The value of 0 indicates an unspecified minimum */ + RequiredBytes?: I64; + /** The volume must not be bigger than this. The value of 0 indicates an unspecified maximum. */ + LimitBytes?: I64; +} + +export enum ClusterVolumeSpecAccessModeAvailabilityEnum { + Empty = "", + Active = "active", + Pause = "pause", + Drain = "drain", +} + +/** Defines how the volume is used by tasks. */ +export interface ClusterVolumeSpecAccessMode { + /** The set of nodes this volume can be used on at one time. - `single` The volume may only be scheduled to one node at a time. - `multi` the volume may be scheduled to any supported number of nodes at a time. */ + Scope?: ClusterVolumeSpecAccessModeScopeEnum; + /** The number and way that different tasks can use this volume at one time. - `none` The volume may only be used by one task at a time. - `readonly` The volume may be used by any number of tasks, but they all must mount the volume as readonly - `onewriter` The volume may be used by any number of tasks, but only one may mount it as read/write. - `all` The volume may have any number of readers and writers. */ + Sharing?: ClusterVolumeSpecAccessModeSharingEnum; + /** Swarm Secrets that are passed to the CSI storage plugin when operating on this volume. */ + Secrets?: ClusterVolumeSpecAccessModeSecrets[]; + AccessibilityRequirements?: ClusterVolumeSpecAccessModeAccessibilityRequirements; + CapacityRange?: ClusterVolumeSpecAccessModeCapacityRange; + /** The availability of the volume for use in tasks. - `active` The volume is fully available for scheduling on the cluster - `pause` No new workloads should use the volume, but existing workloads are not stopped. - `drain` All workloads using this volume should be stopped and rescheduled, and no new ones should be started. */ + Availability?: ClusterVolumeSpecAccessModeAvailabilityEnum; +} + +/** Cluster-specific options used to create the volume. */ +export interface ClusterVolumeSpec { + /** Group defines the volume group of this volume. Volumes belonging to the same group can be referred to by group name when creating Services. Referring to a volume by group instructs Swarm to treat volumes in that group interchangeably for the purpose of scheduling. Volumes with an empty string for a group technically all belong to the same, emptystring group. */ + Group?: string; + AccessMode?: ClusterVolumeSpecAccessMode; +} + +/** Information about the global status of the volume. */ +export interface ClusterVolumeInfo { + /** The capacity of the volume in bytes. A value of 0 indicates that the capacity is unknown. */ + CapacityBytes?: I64; + /** A map of strings to strings returned from the storage plugin when the volume is created. */ + VolumeContext?: Record; + /** The ID of the volume as returned by the CSI storage plugin. This is distinct from the volume's ID as provided by Docker. This ID is never used by the user when communicating with Docker to refer to this volume. If the ID is blank, then the Volume has not been successfully created in the plugin yet. */ + VolumeID?: string; + /** The topology this volume is actually accessible from. */ + AccessibleTopology?: Topology[]; +} + +export enum ClusterVolumePublishStatusStateEnum { + Empty = "", + PendingPublish = "pending-publish", + Published = "published", + PendingNodeUnpublish = "pending-node-unpublish", + PendingControllerUnpublish = "pending-controller-unpublish", +} + +export interface ClusterVolumePublishStatus { + /** The ID of the Swarm node the volume is published on. */ + NodeID?: string; + /** The published state of the volume. * `pending-publish` The volume should be published to this node, but the call to the controller plugin to do so has not yet been successfully completed. * `published` The volume is published successfully to the node. * `pending-node-unpublish` The volume should be unpublished from the node, and the manager is awaiting confirmation from the worker that it has done so. * `pending-controller-unpublish` The volume is successfully unpublished from the node, but has not yet been successfully unpublished on the controller. */ + State?: ClusterVolumePublishStatusStateEnum; + /** A map of strings to strings returned by the CSI controller plugin when a volume is published. */ + PublishContext?: Record; +} + +/** Options and information specific to, and only present on, Swarm CSI cluster volumes. */ +export interface ClusterVolume { + /** The Swarm ID of this volume. Because cluster volumes are Swarm objects, they have an ID, unlike non-cluster volumes. This ID can be used to refer to the Volume instead of the name. */ + ID?: string; + Version?: ObjectVersion; + CreatedAt?: string; + UpdatedAt?: string; + Spec?: ClusterVolumeSpec; + Info?: ClusterVolumeInfo; + /** The status of the volume as it pertains to its publishing and use on specific nodes */ + PublishStatus?: ClusterVolumePublishStatus[]; +} + +/** Usage details about the volume. This information is used by the `GET /system/df` endpoint, and omitted in other endpoints. */ +export interface VolumeUsageData { + /** Amount of disk space used by the volume (in bytes). This information is only available for volumes created with the `\"local\"` volume driver. For volumes created with other volume drivers, this field is set to `-1` (\"not available\") */ + Size: I64; + /** The number of containers referencing this volume. This field is set to `-1` if the reference-count is not available. */ + RefCount: I64; +} + +export interface Volume { + /** Name of the volume. */ + Name: string; + /** Name of the volume driver used by the volume. */ + Driver: string; + /** Mount path of the volume on the host. */ + Mountpoint: string; + /** Date/Time the volume was created. */ + CreatedAt?: string; + /** Low-level details about the volume, provided by the volume driver. Details are returned as a map with key/value pairs: `{\"key\":\"value\",\"key2\":\"value2\"}`. The `Status` field is optional, and is omitted if the volume driver does not support this feature. */ + Status?: string[]; + /** User-defined key/value metadata. */ + Labels?: Record; + /** The level at which the volume exists. Either `global` for cluster-wide, or `local` for machine level. */ + Scope?: VolumeScopeEnum; + ClusterVolume?: ClusterVolume; + /** The driver specific options used when creating the volume. */ + Options?: Record; + UsageData?: VolumeUsageData; +} + +export type InspectVolumeResponse = Volume; + +export type JsonObject = any; + +export type ListActionsResponse = ActionListItem[]; export type ListAlertersResponse = AlerterListItem[]; @@ -4925,6 +5021,8 @@ export interface Port { export interface ContainerListItem { /** The Server which hosts the container. */ server_id?: string; + /** The name of the Server which hosts the container. */ + server_name?: string; /** The first name in Names, not including the initial '/' */ name: string; /** The ID of this container */ @@ -4961,7 +5059,60 @@ export interface ContainerListItem { labels?: Record; } -export type ListAllDockerContainersResponse = ContainerListItem[]; +export type ListAllContainersResponse = ContainerListItem[]; + +/** + * Combined state options for + * both Server and Swarm based Stacks. + */ +export enum StackServiceState { + /** (Swarm) All tasks OK */ + Healthy = "Healthy", + /** (Swarm) Some tasks don't match desired state */ + Unhealthy = "Unhealthy", + /** (Swarm) All tasks down. */ + Down = "Down", + /** (Container) Container is running */ + Running = "Running", + /** (Container) Container is created */ + Created = "Created", + /** (Container) Container is paused */ + Paused = "Paused", + /** (Container) Container is restarting */ + Restarting = "Restarting", + /** (Container) Container is exited */ + Exited = "Exited", + /** (Container) Container is stopping */ + Stopping = "Stopping", + /** (Container) Container is removing */ + Removing = "Removing", + /** (Container) Container is dead */ + Dead = "Dead", + /** Unknown case */ + Unknown = "Unknown", +} + +/** A stack service, whether server or swarm based. */ +export interface StackService { + /** The stack which the service is a part of. */ + stack_id: string; + /** The name of the stack which the service is a part of. */ + stack_name?: string; + /** The service name */ + service: string; + /** The service image */ + image: string; + /** The container (Server mode) */ + container?: ContainerListItem; + /** The service (Swarm mode) */ + swarm_service?: SwarmServiceListItem; + /** The service state */ + state: StackServiceState; + /** The service image digests */ + image_digests?: ImageDigest[]; +} + +export type ListAllStackServicesResponse = StackService[]; /** An api key used to authenticate requests via request headers. */ export interface ApiKey { @@ -5013,9 +5164,51 @@ export interface ComposeProject { export type ListComposeProjectsResponse = ComposeProject[]; +export type ListContainersResponse = ContainerListItem[]; + export type ListDeploymentsResponse = DeploymentListItem[]; -export type ListDockerContainersResponse = ContainerListItem[]; +export type ListFullActionsResponse = Action[]; + +export type ListFullAlertersResponse = Alerter[]; + +export type ListFullBuildersResponse = Builder[]; + +export type ListFullBuildsResponse = Build[]; + +export type ListFullDeploymentsResponse = Deployment[]; + +export type ListFullProceduresResponse = Procedure[]; + +export type ListFullReposResponse = Repo[]; + +export type ListFullResourceSyncsResponse = ResourceSync[]; + +export type ListFullServersResponse = Server[]; + +export type ListFullStacksResponse = Stack[]; + +export type ListFullSwarmsResponse = Swarm[]; + +export type ListGitProviderAccountsResponse = GitProviderAccount[]; + +export interface ProviderAccount { + /** The account username. Required. */ + username: string; + /** The account access token. Required. */ + token?: string; +} + +export interface GitProvider { + /** The git provider domain. Default: `github.com`. */ + domain: string; + /** Whether to use https. Default: true. */ + https: boolean; + /** The accounts on the git provider. Required. */ + accounts: ProviderAccount[]; +} + +export type ListGitProvidersFromConfigResponse = GitProvider[]; /** individual image layer information in response to ImageHistory operation */ export interface ImageHistoryResponseItem { @@ -5027,7 +5220,23 @@ export interface ImageHistoryResponseItem { Comment: string; } -export type ListDockerImageHistoryResponse = ImageHistoryResponseItem[]; +export type ListImageHistoryResponse = ImageHistoryResponseItem[]; + +export interface ImageRegistry { + /** The image provider domain. Default: `docker.io`. */ + domain: string; + /** The accounts on the registry. Required. */ + accounts: ProviderAccount[]; + /** + * Available organizations on the registry provider. + * Used to push an image under an organization's repo rather than an account's repo. + */ + organizations?: string[]; +} + +export type ListImageRegistriesFromConfigResponse = ImageRegistry[]; + +export type ListImageRegistryAccountsResponse = ImageRegistryAccount[]; export interface ImageListItem { /** @@ -5056,7 +5265,7 @@ export interface ImageListItem { in_use: boolean; } -export type ListDockerImagesResponse = ImageListItem[]; +export type ListImagesResponse = ImageListItem[]; export interface NetworkListItem { name?: string; @@ -5075,80 +5284,7 @@ export interface NetworkListItem { in_use: boolean; } -export type ListDockerNetworksResponse = NetworkListItem[]; - -export interface ProviderAccount { - /** The account username. Required. */ - username: string; - /** The account access token. Required. */ - token?: string; -} - -export interface DockerRegistry { - /** The docker provider domain. Default: `docker.io`. */ - domain: string; - /** The accounts on the registry. Required. */ - accounts: ProviderAccount[]; - /** - * Available organizations on the registry provider. - * Used to push an image under an organization's repo rather than an account's repo. - */ - organizations?: string[]; -} - -export type ListDockerRegistriesFromConfigResponse = DockerRegistry[]; - -export type ListDockerRegistryAccountsResponse = DockerRegistryAccount[]; - -export interface VolumeListItem { - /** The name of the volume */ - name: string; - driver: string; - mountpoint: string; - created?: string; - scope: VolumeScopeEnum; - /** Amount of disk space used by the volume (in bytes). This information is only available for volumes created with the `\"local\"` volume driver. For volumes created with other volume drivers, this field is set to `-1` (\"not available\") */ - size?: I64; - /** Whether the volume is currently attached to any container */ - in_use: boolean; -} - -export type ListDockerVolumesResponse = VolumeListItem[]; - -export type ListFullActionsResponse = Action[]; - -export type ListFullAlertersResponse = Alerter[]; - -export type ListFullBuildersResponse = Builder[]; - -export type ListFullBuildsResponse = Build[]; - -export type ListFullDeploymentsResponse = Deployment[]; - -export type ListFullProceduresResponse = Procedure[]; - -export type ListFullReposResponse = Repo[]; - -export type ListFullResourceSyncsResponse = ResourceSync[]; - -export type ListFullServersResponse = Server[]; - -export type ListFullStacksResponse = Stack[]; - -export type ListFullSwarmsResponse = Swarm[]; - -export type ListGitProviderAccountsResponse = GitProviderAccount[]; - -export interface GitProvider { - /** The git provider domain. Default: `github.com`. */ - domain: string; - /** Whether to use https. Default: true. */ - https: boolean; - /** The accounts on the git provider. Required. */ - accounts: ProviderAccount[]; -} - -export type ListGitProvidersFromConfigResponse = GitProvider[]; +export type ListNetworksResponse = NetworkListItem[]; export type ListOnboardingKeysResponse = OnboardingKey[]; @@ -5209,23 +5345,25 @@ export type ProcedureListItem = ResourceListItem; export type ListProceduresResponse = ProcedureListItem[]; export enum RepoState { - /** Unknown case */ - Unknown = "Unknown", - /** Last clone / pull successful (or never cloned) */ - Ok = "Ok", - /** Last clone / pull failed */ - Failed = "Failed", /** Currently cloning */ Cloning = "Cloning", /** Currently pulling */ Pulling = "Pulling", /** Currently building */ Building = "Building", + /** Last clone / pull successful (or never cloned) */ + Ok = "Ok", + /** Last clone / pull failed */ + Failed = "Failed", + /** Unknown case */ + Unknown = "Unknown", } export interface RepoListItemInfo { /** The server that repo sits on. */ server_id: string; + /** The name of the server that repo sits on. */ + server_name?: string; /** The builder that builds the repo. */ builder_id: string; /** Repo last cloned / pulled timestamp in ms. */ @@ -5282,6 +5420,8 @@ export interface ResourceSyncListItemInfo { resource_path: string[]; /** Linked repo, if one is attached. */ linked_repo: string; + /** The name of the linked repo, if one is attached. */ + linked_repo_name?: string; /** The git provider domain. */ git_provider: string; /** The Github repo used as the source of the sync resources */ @@ -5349,6 +5489,48 @@ export interface __Serror { export type _Serror = __Serror; +/** Realtime minimal system stats data (zero allocation) */ +export interface MinimalSystemStats { + /** Cpu usage percentage */ + cpu_perc: number; + /** Load average (1m, 5m, 15m) */ + load_average: SystemLoadAverage; + /** + * This is really the 'Free' memory, not the 'Available' memory. + * It may be different than mem_total_gb - mem_used_gb. + */ + mem_free_gb: number; + /** + * Used memory in GB. 'Total' - 'Available' (not free) memory, + * with the (reclaimable) ZFS ARC cache subtracted out. + */ + mem_used_gb: number; + /** Total memory in GB */ + mem_total_gb: number; + /** Reclaimable page cache + buffers in GB. */ + mem_buff_cache_gb: number; + /** ZFS ARC cache in GB. 0 when ZFS is not present. */ + mem_zfs_arc_gb: number; + /** Total swap in GB. */ + swap_total_gb: number; + /** Used swap in GB. */ + swap_used_gb: number; + /** Total size of all disks combined in GB */ + disk_total_gb: number; + /** Used portion of all disks combined in GB */ + disk_used_gb: number; + /** Network ingress usage in MB */ + network_ingress_bytes: number; + /** Network egress usage in MB */ + network_egress_bytes: number; + /** The rate the system stats are being polled from the system */ + polling_rate: Timelength; + /** Unix timestamp in milliseconds when stats were last polled */ + refresh_ts: I64; + /** Unix timestamp in milliseconds when disk list was last refreshed */ + refresh_list_ts: I64; +} + export interface ServerListItemInfo { /** The server's state. */ state: ServerState; @@ -5357,6 +5539,12 @@ export interface ServerListItemInfo { * the server, message will be given here. */ err?: _Serror; + /** System stats, if available */ + stats?: MinimalSystemStats; + /** The server's number of physical cores. */ + core_count?: number; + /** The server's number of logical cores. */ + logical_core_count?: number; /** Region of the server. */ region: string; /** Address of the server, or null if empty. */ @@ -5400,19 +5588,6 @@ export type ServerListItem = ResourceListItem; export type ListServersResponse = ServerListItem[]; -export interface StackService { - /** The service name */ - service: string; - /** The service image */ - image: string; - /** The container (Server mode) */ - container?: ContainerListItem; - /** The service (Swarm mode) */ - swarm_service?: SwarmServiceListItem; - /** The service image digests */ - image_digests?: ImageDigest[]; -} - export type ListStackServicesResponse = StackService[]; export enum StackState { @@ -5443,14 +5618,20 @@ export enum StackState { export interface StackListItemInfo { /** The swarm that stack is deployed on, when in Swarm mode. */ swarm_id: string; + /** The name of the swarm that stack is deployed on, when in Swarm mode. */ + swarm_name?: string; /** The server that stack is deployed on, when in Server mode. */ server_id: string; + /** The name of the server that stack is deployed on, when in Server mode. */ + server_name?: string; /** Whether stack is using files on host mode */ files_on_host: boolean; /** Whether stack has file contents defined. */ file_contents: boolean; /** Linked repo, if one is attached. */ linked_repo: string; + /** The name of the linked repo, if one is attached. */ + linked_repo_name?: string; /** The git provider domain */ git_provider: string; /** The configured repo */ @@ -5660,6 +5841,11 @@ export interface Terminal { name: string; /** The target resource of the Terminal. */ target: TerminalTarget; + /** + * The name of the target resource (Server / Stack / Deployment). + * Resolved by Core when listing all terminals for a user. + */ + target_name?: string; /** The command used to init the shell. */ command: string; /** The size of the terminal history in memory. */ @@ -5681,9 +5867,34 @@ export type ListUsersResponse = User[]; export type ListVariablesResponse = Variable[]; +export interface VolumeListItem { + /** The name of the volume */ + name: string; + driver: string; + mountpoint: string; + created?: string; + scope: VolumeScopeEnum; + /** Amount of disk space used by the volume (in bytes). This information is only available for volumes created with the `\"local\"` volume driver. For volumes created with other volume drivers, this field is set to `-1` (\"not available\") */ + size?: I64; + /** Whether the volume is currently attached to any container */ + in_use: boolean; +} + +export type ListVolumesResponse = VolumeListItem[]; + export type MongoDocument = any; export interface ProcedureQuerySpecifics { + /** + * Query only for Procedures matching these states. + * If empty, does not filter by state. + */ + states?: ProcedureState[]; + /** + * Query only for Procedures with (or without) + * a schedule configured. + */ + scheduled?: boolean; } export type ProcedureQuery = ResourceQuery; @@ -5692,14 +5903,30 @@ export type PushRecentlyViewedResponse = NoData; export interface RepoQuerySpecifics { /** Filter repos by their repo. */ - repos: string[]; + repos?: string[]; + /** + * Query only for Repos on these Servers. + * If empty, does not filter by Server. + * Only accepts Server id (not name). + */ + server_ids?: string[]; + /** + * Query only for Repos matching these states. + * If empty, does not filter by state. + */ + states?: RepoState[]; } export type RepoQuery = ResourceQuery; export interface ResourceSyncQuerySpecifics { /** Filter syncs by their repo. */ - repos: string[]; + repos?: string[]; + /** + * Query only for Builds with these linked repos. + * Only accepts Repo id (not name). + */ + linked_repos?: string[]; } export type ResourceSyncQuery = ResourceQuery; @@ -5713,6 +5940,11 @@ export type SearchStackLogResponse = Log; export type SearchSwarmServiceLogResponse = Log; export interface ServerQuerySpecifics { + /** + * Query only for Servers matching these states. + * If empty, does not filter by state. + */ + states?: ServerState[]; } /** Server-specific query */ @@ -5727,6 +5959,12 @@ export interface StackQuerySpecifics { * Only accepts Server id (not name). */ server_ids?: string[]; + /** + * Query only for Stacks on these Swarms. + * If empty, does not filter by Swarm. + * Only accepts Swarm id (not name). + */ + swarm_ids?: string[]; /** * Query only for Stacks with these linked repos. * Only accepts Repo id (not name). @@ -5736,6 +5974,11 @@ export interface StackQuerySpecifics { repos?: string[]; /** Query only for Stack with available image updates. */ update_available?: boolean; + /** + * Query only for Stacks matching these states. + * If empty, does not filter by state. + */ + states?: StackState[]; } export type StackQuery = ResourceQuery; @@ -5747,10 +5990,10 @@ export interface SwarmQuerySpecifics { export type SwarmQuery = ResourceQuery; -export type UpdateDockerRegistryAccountResponse = DockerRegistryAccount; - export type UpdateGitProviderAccountResponse = GitProviderAccount; +export type UpdateImageRegistryAccountResponse = ImageRegistryAccount; + export type UpdateOnboardingKeyResponse = OnboardingKey; export type UpdatePermissionOnResourceTypeResponse = NoData; @@ -5785,10 +6028,10 @@ export type _PartialBuilderConfig = Partial; export type _PartialDeploymentConfig = Partial; -export type _PartialDockerRegistryAccount = Partial; - export type _PartialGitProviderAccount = Partial; +export type _PartialImageRegistryAccount = Partial; + export type _PartialProcedureConfig = Partial; export type _PartialRepoConfig = Partial; @@ -5865,8 +6108,12 @@ export interface AwsBuilderConfig { insecure_tls: boolean; /** Which git providers are available on the AMI */ git_providers?: GitProvider[]; - /** Which docker registries are available on the AMI. */ - docker_registries?: DockerRegistry[]; + /** + * Which image registries are available on the AMI. + * + * Pre v2.3.0, called `docker_registries` + */ + image_registries?: ImageRegistry[]; /** Which secrets are available on the AMI. */ secrets?: string[]; } @@ -5899,6 +6146,11 @@ export interface BatchBuildRepo { * ``` */ pattern: string; + /** + * Filter matches by tag. + * If empty, skips tag filtering. + */ + tags?: string[]; } /** Checks for newer image than what is deployed. Response: [BatchCheckDeploymentForUpdateResponse] */ @@ -5916,6 +6168,11 @@ export interface BatchCheckDeploymentForUpdate { * ``` */ pattern: string; + /** + * Filter matches by tag. + * If empty, skips tag filtering. + */ + tags?: string[]; /** * Normally resources with 'auto_update' will be * redeployed immediately if updates are found. @@ -5945,6 +6202,11 @@ export interface BatchCheckStackForUpdate { * ``` */ pattern: string; + /** + * Filter matches by tag. + * If empty, skips tag filtering. + */ + tags?: string[]; /** * Normally resources with 'auto_update' will be * redeployed immediately if updates are found. @@ -5979,6 +6241,11 @@ export interface BatchCloneRepo { * ``` */ pattern: string; + /** + * Filter matches by tag. + * If empty, skips tag filtering. + */ + tags?: string[]; } /** @@ -6005,6 +6272,11 @@ export interface BatchDeploy { * ``` */ pattern: string; + /** + * Filter matches by tag. + * If empty, skips tag filtering. + */ + tags?: string[]; } /** Deploys multiple Stacks in parallel that match pattern. Response: [BatchExecutionResponse]. */ @@ -6022,6 +6294,11 @@ export interface BatchDeployStack { * ``` */ pattern: string; + /** + * Filter matches by tag. + * If empty, skips tag filtering. + */ + tags?: string[]; } /** Deploys multiple Stacks if changed in parallel that match pattern. Response: [BatchExecutionResponse]. */ @@ -6039,6 +6316,11 @@ export interface BatchDeployStackIfChanged { * ``` */ pattern: string; + /** + * Filter matches by tag. + * If empty, skips tag filtering. + */ + tags?: string[]; } /** Destroys multiple Deployments in parallel that match pattern. Response: [BatchExecutionResponse]. */ @@ -6056,6 +6338,11 @@ export interface BatchDestroyDeployment { * ``` */ pattern: string; + /** + * Filter matches by tag. + * If empty, skips tag filtering. + */ + tags?: string[]; } /** Destroys multiple Stacks in parallel that match pattern. Response: [BatchExecutionResponse]. */ @@ -6073,6 +6360,11 @@ export interface BatchDestroyStack { * ``` */ pattern: string; + /** + * Filter matches by tag. + * If empty, skips tag filtering. + */ + tags?: string[]; } export interface BatchExecutionResponseItemErr { @@ -6095,6 +6387,11 @@ export interface BatchPullRepo { * ``` */ pattern: string; + /** + * Filter matches by tag. + * If empty, skips tag filtering. + */ + tags?: string[]; } /** Pulls multiple Stacks in parallel that match pattern. Response: [BatchExecutionResponse]. */ @@ -6112,6 +6409,11 @@ export interface BatchPullStack { * ``` */ pattern: string; + /** + * Filter matches by tag. + * If empty, skips tag filtering. + */ + tags?: string[]; } /** Runs multiple Actions in parallel that match pattern. Response: [BatchExecutionResponse] */ @@ -6129,6 +6431,11 @@ export interface BatchRunAction { * ``` */ pattern: string; + /** + * Filter matches by tag. + * If empty, skips tag filtering. + */ + tags?: string[]; } /** Runs multiple builds in parallel that match pattern. Response: [BatchExecutionResponse]. */ @@ -6146,6 +6453,11 @@ export interface BatchRunBuild { * ``` */ pattern: string; + /** + * Filter matches by tag. + * If empty, skips tag filtering. + */ + tags?: string[]; } /** Runs multiple Procedures in parallel that match pattern. Response: [BatchExecutionResponse]. */ @@ -6163,6 +6475,11 @@ export interface BatchRunProcedure { * ``` */ pattern: string; + /** + * Filter matches by tag. + * If empty, skips tag filtering. + */ + tags?: string[]; } /** @@ -6189,6 +6506,19 @@ export interface BuildStatsDay { ts: number; } +/** Cancels the target action run. Response: [Update] */ +export interface CancelAction { + /** Id or name */ + action: string; + /** + * The update id associated with the specific + * run to cancel + * Must provide either `action` + * or `update_id` + */ + update_id?: string; +} + /** * Cancels the target build. * Only does anything if the build is `building` when called. @@ -6199,6 +6529,19 @@ export interface CancelBuild { build: string; } +/** Cancels the target procedure run. Response: [Update] */ +export interface CancelProcedure { + /** Id or name */ + procedure: string; + /** + * The update id associated with the specific + * run to cancel + * Must provide either `action` + * or `update_id` + */ + update_id?: string; +} + /** * Cancels the target repo build. * Only does anything if the repo build is `building` when called. @@ -6762,14 +7105,6 @@ export interface CreateDeploymentFromContainer { server: string; } -/** - * **Admin only.** Create a docker registry account. - * Response: [DockerRegistryAccount]. - */ -export interface CreateDockerRegistryAccount { - account: _PartialDockerRegistryAccount; -} - /** * **Admin only.** Create a git provider account. * Response: [GitProviderAccount]. @@ -6782,6 +7117,16 @@ export interface CreateGitProviderAccount { account: _PartialGitProviderAccount; } +/** + * **Admin only.** Create an image registry account. + * Response: [ImageRegistryAccount]. + * + * Pre v2.3.0, called `CreateDockerRegistryAccount` + */ +export interface CreateImageRegistryAccount { + account: _PartialImageRegistryAccount; +} + /** * **Admin only.** Create a local user. * Response: [User]. @@ -7086,15 +7431,6 @@ export interface DeleteDeployment { id: string; } -/** - * **Admin only.** Delete a docker registry account. - * Response: [DockerRegistryAccount]. - */ -export interface DeleteDockerRegistryAccount { - /** The id of the docker registry account to delete */ - id: string; -} - /** * **Admin only.** Delete a git provider account. * Response: [DeleteGitProviderAccountResponse]. @@ -7115,6 +7451,17 @@ export interface DeleteImage { name: string; } +/** + * **Admin only.** Delete an image registry account. + * Response: [ImageRegistryAccount]. + * + * Pre v2.3.0, called `DeleteDockerRegistryAccount` + */ +export interface DeleteImageRegistryAccount { + /** The id of the image registry account to delete */ + id: string; +} + /** * Delete a docker network. * Response: [Update] @@ -7735,6 +8082,29 @@ export interface GetContainerLog { timestamps?: boolean; } +/** + * Gets a summary of data relating to all containers. + * Response: [GetContainersSummaryResponse]. + * + * Pre v2.3.0, called `GetDockerContainersSummary` + */ +export interface GetContainersSummary { +} + +/** Response for [GetContainersSummary] */ +export interface GetContainersSummaryResponse { + /** The total number of Containers */ + total: number; + /** The number of Containers with Running state */ + running: number; + /** The number of Containers with Stopped or Paused or Created state */ + stopped: number; + /** The number of Containers with Restarting or Dead state */ + unhealthy: number; + /** The number of Containers with Unknown state */ + unknown: number; +} + /** * Get information about the Komodo Core API configuration. * Response: [GetCoreInfoResponse]. @@ -7766,6 +8136,8 @@ export interface GetCoreInfoResponse { timezone: string; /** Public key for Core / Periphery authentication. */ public_key: string; + /** Default pagination limit for the UI to use. */ + default_pagination_limit: U64; } /** Get a specific deployment by name or id. Response: [Deployment]. */ @@ -7855,35 +8227,6 @@ export interface GetDeploymentsSummaryResponse { unknown: I64; } -/** - * Gets a summary of data relating to all containers. - * Response: [GetDockerContainersSummaryResponse]. - */ -export interface GetDockerContainersSummary { -} - -/** Response for [GetDockerContainersSummary] */ -export interface GetDockerContainersSummaryResponse { - /** The total number of Containers */ - total: number; - /** The number of Containers with Running state */ - running: number; - /** The number of Containers with Stopped or Paused or Created state */ - stopped: number; - /** The number of Containers with Restarting or Dead state */ - unhealthy: number; - /** The number of Containers with Unknown state */ - unknown: number; -} - -/** - * Get a specific docker registry account. - * Response: [GetDockerRegistryAccountResponse]. - */ -export interface GetDockerRegistryAccount { - id: string; -} - /** * Get a specific git provider account. * Response: [GetGitProviderAccountResponse]. @@ -7922,6 +8265,26 @@ export interface SystemStatsRecord { mem_used_gb: number; /** Total memory in GB */ mem_total_gb: number; + /** + * [2.3.0+] + * Reclaimable page cache + buffers in GB. + */ + mem_buff_cache_gb?: number; + /** + * [2.3.0+] + * ZFS ARC cache in GB. 0 when ZFS is not present. + */ + mem_zfs_arc_gb?: number; + /** + * [2.3.0+] + * Total swap in GB. + */ + swap_total_gb?: number; + /** + * [2.3.0+] + * Used swap in GB. + */ + swap_used_gb?: number; /** Disk used in GB */ disk_used_gb: number; /** Total disk size in GB */ @@ -7942,6 +8305,16 @@ export interface GetHistoricalServerStatsResponse { next_page?: number; } +/** + * Get a specific image registry account. + * Response: [GetImageRegistryAccountResponse]. + * + * Pre v2.3.0, called `GetDockerRegistryAccount` + */ +export interface GetImageRegistryAccount { + id: string; +} + /** * Get the Periphery information of the target server, * including the Periphery version and public key. @@ -8348,6 +8721,18 @@ export interface GlobalAutoUpdate { skip_auto_update?: boolean; } +/** + * Inspect a container on the server. Response: [Container]. + * + * Pre v2.3.0, called `InspectDockerContainer` + */ +export interface InspectContainer { + /** Id or name */ + server: string; + /** The container name */ + container: string; +} + /** * Inspect the docker container associated with the Deployment. * Response: [Container]. @@ -8366,38 +8751,30 @@ export interface InspectDeploymentSwarmService { deployment: string; } -/** Inspect a docker container on the server. Response: [Container]. */ -export interface InspectDockerContainer { - /** Id or name */ - server: string; - /** The container name */ - container: string; -} - -/** Inspect a docker image on the server. Response: [Image]. */ -export interface InspectDockerImage { +/** + * Inspect a container image on the server. Response: [Image]. + * + * Pre v2.3.0, called `InspectDockerImage` + */ +export interface InspectImage { /** Id or name */ server: string; /** The image name */ image: string; } -/** Inspect a docker network on the server. Response: [InspectDockerNetworkResponse]. */ -export interface InspectDockerNetwork { +/** + * Inspect a container network on the server. Response: [InspectNetworkResponse]. + * + * Pre v2.3.0, called `InspectDockerNetwork` + */ +export interface InspectNetwork { /** Id or name */ server: string; /** The network name */ network: string; } -/** Inspect a docker volume on the server. Response: [Volume]. */ -export interface InspectDockerVolume { - /** Id or name */ - server: string; - /** The volume name */ - volume: string; -} - /** * Inspect a docker container associated with a Stack. * Response: [Container]. @@ -8504,21 +8881,99 @@ export interface InspectSwarmTask { task: string; } +/** + * Inspect a container volume on the server. Response: [Volume]. + * + * Pre v2.3.0, called `InspectDockerVolume` + */ +export interface InspectVolume { + /** Id or name */ + server: string; + /** The volume name */ + volume: string; +} + export interface LatestCommit { hash: string; message: string; } +export enum ActionSortBy { + /** Sort by name. Default. */ + Name = "Name", + /** Sort by state. */ + State = "State", + /** Sort by next scheduled run. */ + NextRun = "NextRun", +} + /** List actions matching optional query. Response: [ListActionsResponse]. */ export interface ListActions { /** optional structured query to filter actions. */ query?: ActionQuery; + /** + * Retrieve more results by incrementing the page. + * `page: 0` is default. + */ + page?: U64; + /** + * Set the limit for number of resources per-page. + * If not provided, uses the Core config + * `default_pagination_limit` (default: 30). + * + * Passing `limit: 0` returns all results (unlimited). + * + * Note: the page logic relies on this being consistent + * across queries for more pages. + */ + limit?: U64; + /** + * Sort the results by this field. + * Defaults to Name. Non-Name sorts are applied in memory + * after querying all matching resources. + */ + sort_by?: ActionSortBy; + /** Reverse the sort direction. */ + sort_desc?: boolean; +} + +export enum AlerterSortBy { + /** Sort by name. Default. */ + Name = "Name", + /** Sort by endpoint type. */ + Type = "Type", + /** Sort by enabled. */ + Enabled = "Enabled", } /** List alerters matching optional query. Response: [ListAlertersResponse]. */ export interface ListAlerters { /** Structured query to filter alerters. */ query?: AlerterQuery; + /** + * Retrieve more results by incrementing the page. + * `page: 0` is default. + */ + page?: U64; + /** + * Set the limit for number of resources per-page. + * If not provided, uses the Core config + * `default_pagination_limit` (default: 30). + * + * Passing `limit: 0` returns all results (unlimited). + * + * Note: the page logic relies on this being consistent + * across queries for more pages. + */ + limit?: U64; + /** + * Sort the results by this field. + * Defaults to Name. Non-Name sorts are applied in memory + * after querying all matching resources. + */ + sort_by?: AlerterSortBy; + /** Reverse the sort direction. */ + sort_desc?: boolean; } /** @@ -8570,15 +9025,98 @@ export interface ListAlertsResponse { next_page?: I64; } +export enum ContainerSortBy { + /** Sort by container name. Default. */ + Name = "Name", + /** Sort by host Server name. */ + Server = "Server", + /** Sort by container state. */ + State = "State", + /** Sort by image. */ + Image = "Image", + /** Sort by first network. */ + Networks = "Networks", + /** Sort by first port. */ + Ports = "Ports", + /** Sort by first volume. */ + Volumes = "Volumes", +} + /** - * List all docker containers on the target servers. - * Response: [ListDockerContainersResponse]. + * List all containers on the target servers. + * Response: [ListAllContainersResponse]. + * + * Pre v2.3.0, called `ListAllDockerContainers` */ -export interface ListAllDockerContainers { +export interface ListAllContainers { /** Filter by server id or name. */ servers?: string[]; - /** Filter by container name. */ - containers?: string[]; + /** Filter servers by tag. */ + tags?: string[]; + /** + * Filter by container name. + * Returned containers have names which contain all terms. + */ + terms?: string[]; + /** Filter by container state. */ + state?: ContainerStateStatusEnum[]; + /** + * Retrieve more results by incrementing the page. + * `page: 0` is default. + */ + page?: U64; + /** + * Set the limit for number of containers per-page. + * If not provided, uses the Core config + * `default_pagination_limit` (default: 30). + * + * Passing `limit: 0` returns all results (unlimited). + * + * Note: the page logic relies on this being consistent + * across queries for more pages. + */ + limit?: U64; + /** + * Sort the results by this field. + * Defaults to Name. + */ + sort_by?: ContainerSortBy; + /** Reverse the sort direction. */ + sort_desc?: boolean; +} + +/** + * List all stack services part of the target stacks. + * Response: [ListStackServicesResponse]. + */ +export interface ListAllStackServices { + /** Filter by stack name. */ + stacks?: string[]; + /** Filter stacks by tag. */ + tags?: string[]; + /** + * Filter by service name. + * Returned services have names which contain all terms. + */ + terms?: string[]; + /** Filter by service state. */ + state?: StackServiceState[]; + /** + * Retrieve more results by incrementing the page. + * `page: 0` is default. + */ + page?: U64; + /** + * Set the limit for number of services per-page. + * If not provided, uses the Core config + * `default_pagination_limit` (default: 30). + * + * Passing `limit: 0` returns all results (unlimited). + * + * Note: the page logic relies on this being consistent + * across queries for more pages. + */ + limit?: U64; } /** @@ -8617,15 +9155,81 @@ export interface ListBuildVersions { limit?: I64; } +export enum BuilderSortBy { + /** Sort by name. Default. */ + Name = "Name", + /** Sort by builder provider type. */ + Provider = "Provider", + /** Sort by instance type. */ + InstanceType = "InstanceType", +} + /** List builders matching structured query. Response: [ListBuildersResponse]. */ export interface ListBuilders { query?: BuilderQuery; + /** + * Retrieve more results by incrementing the page. + * `page: 0` is default. + */ + page?: U64; + /** + * Set the limit for number of resources per-page. + * If not provided, uses the Core config + * `default_pagination_limit` (default: 30). + * + * Passing `limit: 0` returns all results (unlimited). + * + * Note: the page logic relies on this being consistent + * across queries for more pages. + */ + limit?: U64; + /** + * Sort the results by this field. + * Defaults to Name. Non-Name sorts are applied in memory + * after querying all matching resources. + */ + sort_by?: BuilderSortBy; + /** Reverse the sort direction. */ + sort_desc?: boolean; +} + +export enum BuildSortBy { + /** Sort by name. Default. */ + Name = "Name", + /** Sort by source repo. */ + Source = "Source", + /** Sort by state. */ + State = "State", } /** List builds matching optional query. Response: [ListBuildsResponse]. */ export interface ListBuilds { /** optional structured query to filter builds. */ query?: BuildQuery; + /** + * Retrieve more results by incrementing the page. + * `page: 0` is default. + */ + page?: U64; + /** + * Set the limit for number of resources per-page. + * If not provided, uses the Core config + * `default_pagination_limit` (default: 30). + * + * Passing `limit: 0` returns all results (unlimited). + * + * Note: the page logic relies on this being consistent + * across queries for more pages. + */ + limit?: U64; + /** + * Sort the results by this field. + * Defaults to Name. Non-Name sorts are applied in memory + * after querying all matching resources. + */ + sort_by?: BuildSortBy; + /** Reverse the sort direction. */ + sort_desc?: boolean; } /** @@ -8665,7 +9269,7 @@ export interface ListCommonStackExtraArgs { } /** - * List all docker compose projects on the target server. + * List all compose projects on the target server. * Response: [ListComposeProjectsResponse]. */ export interface ListComposeProjects { @@ -8674,104 +9278,145 @@ export interface ListComposeProjects { } /** - * List deployments matching optional query. - * Response: [ListDeploymentsResponse]. - */ -export interface ListDeployments { - /** optional structured query to filter deployments. */ - query?: DeploymentQuery; -} - -/** - * List all docker containers on the target server. - * Response: [ListDockerContainersResponse]. - */ -export interface ListDockerContainers { - /** Id or name */ - server: string; -} - -/** Get image history from the server. Response: [ListDockerImageHistoryResponse]. */ -export interface ListDockerImageHistory { - /** Id or name */ - server: string; - /** The image name */ - image: string; -} - -/** - * List the docker images locally cached on the target server. - * Response: [ListDockerImagesResponse]. + * List all containers on the target server. + * Response: [ListContainersResponse]. + * + * Pre v2.3.0, called `ListDockerContainers` */ -export interface ListDockerImages { +export interface ListContainers { /** Id or name */ server: string; } -/** List the docker networks on the server. Response: [ListDockerNetworksResponse]. */ -export interface ListDockerNetworks { - /** Id or name */ - server: string; +export enum DeploymentSortBy { + /** Sort by name. Default. */ + Name = "Name", + /** Sort by image. */ + Image = "Image", + /** Sort by host Server / Swarm name. */ + Host = "Host", + /** Sort by state. */ + State = "State", } /** - * List the docker registry providers available in Core / Periphery config files. - * Response: [ListDockerRegistriesFromConfigResponse]. - * - * Includes: - * - registries in core config - * - registries configured on builds, deployments - * - registries on the optional Server or Builder + * List deployments matching optional query. + * Response: [ListDeploymentsResponse]. */ -export interface ListDockerRegistriesFromConfig { +export interface ListDeployments { + /** optional structured query to filter deployments. */ + query?: DeploymentQuery; /** - * Accepts an optional Server or Builder target to expand the core list with - * providers available on that specific resource. + * Retrieve more results by incrementing the page. + * `page: 0` is default. */ - target?: ResourceTarget; -} - -/** - * List docker registry accounts matching optional query. - * Response: [ListDockerRegistryAccountsResponse]. - */ -export interface ListDockerRegistryAccounts { - /** Optionally filter by accounts with a specific domain. */ - domain?: string; - /** Optionally filter by accounts with a specific username. */ - username?: string; -} - -/** - * List all docker volumes on the target server. - * Response: [ListDockerVolumesResponse]. - */ -export interface ListDockerVolumes { - /** Id or name */ - server: string; + page?: U64; + /** + * Set the limit for number of resources per-page. + * If not provided, uses the Core config + * `default_pagination_limit` (default: 30). + * + * Passing `limit: 0` returns all results (unlimited). + * + * Note: the page logic relies on this being consistent + * across queries for more pages. + */ + limit?: U64; + /** + * Sort the results by this field. + * Defaults to Name. Non-Name sorts are applied in memory + * after querying all matching resources. + */ + sort_by?: DeploymentSortBy; + /** Reverse the sort direction. */ + sort_desc?: boolean; } /** List actions matching optional query. Response: [ListFullActionsResponse]. */ export interface ListFullActions { /** optional structured query to filter actions. */ query?: ActionQuery; + /** + * Retrieve more results by incrementing the page. + * `page: 0` is default. + */ + page?: U64; + /** + * Set the limit for number of resources per-page. + * If not provided, uses the Core config + * `default_pagination_limit` (default: 30). + * + * Passing `limit: 0` returns all results (unlimited). + * + * Note: the page logic relies on this being consistent + * across queries for more pages. + */ + limit?: U64; } /** List full alerters matching optional query. Response: [ListFullAlertersResponse]. */ export interface ListFullAlerters { /** Structured query to filter alerters. */ query?: AlerterQuery; + /** + * Retrieve more results by incrementing the page. + * `page: 0` is default. + */ + page?: U64; + /** + * Set the limit for number of resources per-page. + * If not provided, uses the Core config + * `default_pagination_limit` (default: 30). + * + * Passing `limit: 0` returns all results (unlimited). + * + * Note: the page logic relies on this being consistent + * across queries for more pages. + */ + limit?: U64; } /** List builders matching structured query. Response: [ListFullBuildersResponse]. */ export interface ListFullBuilders { query?: BuilderQuery; + /** + * Retrieve more results by incrementing the page. + * `page: 0` is default. + */ + page?: U64; + /** + * Set the limit for number of resources per-page. + * If not provided, uses the Core config + * `default_pagination_limit` (default: 30). + * + * Passing `limit: 0` returns all results (unlimited). + * + * Note: the page logic relies on this being consistent + * across queries for more pages. + */ + limit?: U64; } /** List builds matching optional query. Response: [ListFullBuildsResponse]. */ export interface ListFullBuilds { /** optional structured query to filter builds. */ query?: BuildQuery; + /** + * Retrieve more results by incrementing the page. + * `page: 0` is default. + */ + page?: U64; + /** + * Set the limit for number of resources per-page. + * If not provided, uses the Core config + * `default_pagination_limit` (default: 30). + * + * Passing `limit: 0` returns all results (unlimited). + * + * Note: the page logic relies on this being consistent + * across queries for more pages. + */ + limit?: U64; } /** @@ -8781,42 +9426,154 @@ export interface ListFullBuilds { export interface ListFullDeployments { /** optional structured query to filter deployments. */ query?: DeploymentQuery; + /** + * Retrieve more results by incrementing the page. + * `page: 0` is default. + */ + page?: U64; + /** + * Set the limit for number of resources per-page. + * If not provided, uses the Core config + * `default_pagination_limit` (default: 30). + * + * Passing `limit: 0` returns all results (unlimited). + * + * Note: the page logic relies on this being consistent + * across queries for more pages. + */ + limit?: U64; } /** List procedures matching optional query. Response: [ListFullProceduresResponse]. */ export interface ListFullProcedures { /** optional structured query to filter procedures. */ query?: ProcedureQuery; + /** + * Retrieve more results by incrementing the page. + * `page: 0` is default. + */ + page?: U64; + /** + * Set the limit for number of resources per-page. + * If not provided, uses the Core config + * `default_pagination_limit` (default: 30). + * + * Passing `limit: 0` returns all results (unlimited). + * + * Note: the page logic relies on this being consistent + * across queries for more pages. + */ + limit?: U64; } /** List repos matching optional query. Response: [ListFullReposResponse]. */ export interface ListFullRepos { /** optional structured query to filter repos. */ query?: RepoQuery; + /** + * Retrieve more results by incrementing the page. + * `page: 0` is default. + */ + page?: U64; + /** + * Set the limit for number of resources per-page. + * If not provided, uses the Core config + * `default_pagination_limit` (default: 30). + * + * Passing `limit: 0` returns all results (unlimited). + * + * Note: the page logic relies on this being consistent + * across queries for more pages. + */ + limit?: U64; } /** List syncs matching optional query. Response: [ListFullResourceSyncsResponse]. */ export interface ListFullResourceSyncs { /** optional structured query to filter syncs. */ query?: ResourceSyncQuery; + /** + * Retrieve more results by incrementing the page. + * `page: 0` is default. + */ + page?: U64; + /** + * Set the limit for number of resources per-page. + * If not provided, uses the Core config + * `default_pagination_limit` (default: 30). + * + * Passing `limit: 0` returns all results (unlimited). + * + * Note: the page logic relies on this being consistent + * across queries for more pages. + */ + limit?: U64; } /** List servers matching optional query. Response: [ListFullServersResponse]. */ export interface ListFullServers { /** optional structured query to filter servers. */ query?: ServerQuery; + /** + * Retrieve more results by incrementing the page. + * `page: 0` is default. + */ + page?: U64; + /** + * Set the limit for number of resources per-page. + * If not provided, uses the Core config + * `default_pagination_limit` (default: 30). + * + * Passing `limit: 0` returns all results (unlimited). + * + * Note: the page logic relies on this being consistent + * across queries for more pages. + */ + limit?: U64; } /** List stacks matching optional query. Response: [ListFullStacksResponse]. */ export interface ListFullStacks { /** optional structured query to filter stacks. */ query?: StackQuery; + /** + * Retrieve more results by incrementing the page. + * `page: 0` is default. + */ + page?: U64; + /** + * Set the limit for number of resources per-page. + * If not provided, uses the Core config + * `default_pagination_limit` (default: 30). + * + * Passing `limit: 0` returns all results (unlimited). + * + * Note: the page logic relies on this being consistent + * across queries for more pages. + */ + limit?: U64; } /** List Swarms matching optional query. Response: [ListFullSwarmsResponse]. */ export interface ListFullSwarms { /** optional structured query to filter swarms. */ query?: SwarmQuery; + /** + * Retrieve more results by incrementing the page. + * `page: 0` is default. + */ + page?: U64; + /** + * Set the limit for number of resources per-page. + * If not provided, uses the Core config + * `default_pagination_limit` (default: 30). + * + * Passing `limit: 0` returns all results (unlimited). + * + * Note: the page logic relies on this being consistent + * across queries for more pages. + */ + limit?: U64; } /** @@ -8847,6 +9604,71 @@ export interface ListGitProvidersFromConfig { target?: ResourceTarget; } +/** + * Get image history from the server. Response: [ListImageHistoryResponse]. + * + * Pre v2.3.0, called `ListDockerImageHistory` + */ +export interface ListImageHistory { + /** Id or name */ + server: string; + /** The image name */ + image: string; +} + +/** + * List the image registry providers available in Core / Periphery config files. + * Response: [ListImageRegistriesFromConfigResponse]. + * + * Includes: + * - registries in core config + * - registries configured on builds, deployments + * - registries on the optional Server or Builder + * + * Pre v2.3.0, called `ListDockerRegistriesFromConfig` + */ +export interface ListImageRegistriesFromConfig { + /** + * Accepts an optional Server or Builder target to expand the core list with + * providers available on that specific resource. + */ + target?: ResourceTarget; +} + +/** + * List image registry accounts matching optional query. + * Response: [ListImageRegistryAccountsResponse]. + * + * Pre v2.3.0, called `ListDockerRegistryAccounts` + */ +export interface ListImageRegistryAccounts { + /** Optionally filter by accounts with a specific domain. */ + domain?: string; + /** Optionally filter by accounts with a specific username. */ + username?: string; +} + +/** + * List the container images locally cached on the target server. + * Response: [ListImagesResponse]. + * + * Pre v2.3.0, called `ListDockerImages` + */ +export interface ListImages { + /** Id or name */ + server: string; +} + +/** + * List the container networks on the server. Response: [ListNetworksResponse]. + * + * Pre v2.3.0, called `ListDockerNetworks` + */ +export interface ListNetworks { + /** Id or name */ + server: string; +} + /** * **Admin only.** Gets list of onboarding keys. * Response: [ListOnboardingKeysResponse] @@ -8862,22 +9684,136 @@ export interface ListOnboardingKeys { export interface ListPermissions { } +export enum ProcedureSortBy { + /** Sort by name. Default. */ + Name = "Name", + /** Sort by state. */ + State = "State", + /** Sort by next scheduled run. */ + NextRun = "NextRun", +} + /** List procedures matching optional query. Response: [ListProceduresResponse]. */ export interface ListProcedures { /** optional structured query to filter procedures. */ query?: ProcedureQuery; + /** + * Retrieve more results by incrementing the page. + * `page: 0` is default. + */ + page?: U64; + /** + * Set the limit for number of resources per-page. + * If not provided, uses the Core config + * `default_pagination_limit` (default: 30). + * + * Passing `limit: 0` returns all results (unlimited). + * + * Note: the page logic relies on this being consistent + * across queries for more pages. + */ + limit?: U64; + /** + * Sort the results by this field. + * Defaults to Name. Non-Name sorts are applied in memory + * after querying all matching resources. + */ + sort_by?: ProcedureSortBy; + /** Reverse the sort direction. */ + sort_desc?: boolean; +} + +export enum RepoSortBy { + /** Sort by name. Default. */ + Name = "Name", + /** Sort by the git repo. */ + Repo = "Repo", + /** Sort by branch. */ + Branch = "Branch", + /** Sort by state. */ + State = "State", } /** List repos matching optional query. Response: [ListReposResponse]. */ export interface ListRepos { /** optional structured query to filter repos. */ query?: RepoQuery; + /** + * Retrieve more results by incrementing the page. + * `page: 0` is default. + */ + page?: U64; + /** + * Set the limit for number of resources per-page. + * If not provided, uses the Core config + * `default_pagination_limit` (default: 30). + * + * Passing `limit: 0` returns all results (unlimited). + * + * Note: the page logic relies on this being consistent + * across queries for more pages. + */ + limit?: U64; + /** + * Sort the results by this field. + * Defaults to Name. Non-Name sorts are applied in memory + * after querying all matching resources. + */ + sort_by?: RepoSortBy; + /** Reverse the sort direction. */ + sort_desc?: boolean; +} + +export enum ResourceSyncSortBy { + /** Sort by name. Default. */ + Name = "Name", + /** Sort by source repo. */ + Source = "Source", + /** Sort by branch. */ + Branch = "Branch", + /** Sort by state. */ + State = "State", } /** List syncs matching optional query. Response: [ListResourceSyncsResponse]. */ export interface ListResourceSyncs { /** optional structured query to filter syncs. */ query?: ResourceSyncQuery; + /** + * Retrieve more results by incrementing the page. + * `page: 0` is default. + */ + page?: U64; + /** + * Set the limit for number of resources per-page. + * If not provided, uses the Core config + * `default_pagination_limit` (default: 30). + * + * Passing `limit: 0` returns all results (unlimited). + * + * Note: the page logic relies on this being consistent + * across queries for more pages. + */ + limit?: U64; + /** + * Sort the results by this field. + * Defaults to Name. Non-Name sorts are applied in memory + * after querying all matching resources. + */ + sort_by?: ResourceSyncSortBy; + /** Reverse the sort direction. */ + sort_desc?: boolean; +} + +export enum ScheduleSortBy { + /** Sort by target name. Default. */ + Name = "Name", + /** Sort by the schedule expression. */ + Schedule = "Schedule", + /** Sort by next scheduled run. */ + NextRun = "NextRun", + /** Sort by enabled. */ + Enabled = "Enabled", } /** @@ -8889,6 +9825,34 @@ export interface ListSchedules { tags?: string[]; /** 'All' or 'Any' */ tag_behavior?: TagQueryBehavior; + /** + * Filter by target name. + * Returned schedules have names which contain all terms. + */ + terms?: string[]; + /** + * Retrieve more results by incrementing the page. + * `page: 0` is default. + */ + page?: U64; + /** + * Set the limit for number of schedules per-page. + * If not provided, uses the Core config + * `default_pagination_limit` (default: 30). + * + * Passing `limit: 0` returns all results (unlimited). + * + * Note: the page logic relies on this being consistent + * across queries for more pages. + */ + limit?: U64; + /** + * Sort the results by this field. + * Defaults to Name. + */ + sort_by?: ScheduleSortBy; + /** Reverse the sort direction. */ + sort_desc?: boolean; } /** @@ -8903,10 +9867,55 @@ export interface ListSecrets { target?: ResourceTarget; } +export enum ServerSortBy { + /** Sort by name. Default. */ + Name = "Name", + /** Sort by region. */ + Region = "Region", + /** Sort by periphery version. */ + Version = "Version", + /** Sort by state. */ + State = "State", + /** Sort by current cpu usage percentage. */ + Cpu = "Cpu", + /** Sort by current memory usage percentage. */ + Memory = "Memory", + /** Sort by current disk usage percentage. */ + Disk = "Disk", + /** Sort by current 1m load average. */ + LoadAverage = "LoadAverage", + /** Sort by current network usage (ingress + egress). */ + Network = "Network", +} + /** List servers matching optional query. Response: [ListServersResponse]. */ export interface ListServers { /** optional structured query to filter servers. */ query?: ServerQuery; + /** + * Retrieve more results by incrementing the page. + * `page: 0` is default. + */ + page?: U64; + /** + * Set the limit for number of resources per-page. + * If not provided, uses the Core config + * `default_pagination_limit` (default: 30). + * + * Passing `limit: 0` returns all results (unlimited). + * + * Note: the page logic relies on this being consistent + * across queries for more pages. + */ + limit?: U64; + /** + * Sort the results by this field. + * Defaults to Name. Non-Name sorts are applied in memory + * after querying all matching resources. + */ + sort_by?: ServerSortBy; + /** Reverse the sort direction. */ + sort_desc?: boolean; } /** Lists a specific stacks services (the containers). Response: [ListStackServicesResponse]. */ @@ -8915,10 +9924,45 @@ export interface ListStackServices { stack: string; } +export enum StackSortBy { + /** Sort by name. Default. */ + Name = "Name", + /** Sort by source repo. */ + Source = "Source", + /** Sort by host Server / Swarm name. */ + Host = "Host", + /** Sort by state. */ + State = "State", +} + /** List stacks matching optional query. Response: [ListStacksResponse]. */ export interface ListStacks { /** optional structured query to filter stacks. */ query?: StackQuery; + /** + * Retrieve more results by incrementing the page. + * `page: 0` is default. + */ + page?: U64; + /** + * Set the limit for number of resources per-page. + * If not provided, uses the Core config + * `default_pagination_limit` (default: 30). + * + * Passing `limit: 0` returns all results (unlimited). + * + * Note: the page logic relies on this being consistent + * across queries for more pages. + */ + limit?: U64; + /** + * Sort the results by this field. + * Defaults to Name. Non-Name sorts are applied in memory + * after querying all matching resources. + */ + sort_by?: StackSortBy; + /** Reverse the sort direction. */ + sort_desc?: boolean; } /** @@ -8986,10 +10030,41 @@ export interface ListSwarmTasks { swarm: string; } +export enum SwarmSortBy { + /** Sort by name. Default. */ + Name = "Name", + /** Sort by state. */ + State = "State", +} + /** List Swarms matching optional query. Response: [ListSwarmsResponse]. */ export interface ListSwarms { /** Optional structured query to filter Swarms. */ query?: SwarmQuery; + /** + * Retrieve more results by incrementing the page. + * `page: 0` is default. + */ + page?: U64; + /** + * Set the limit for number of resources per-page. + * If not provided, uses the Core config + * `default_pagination_limit` (default: 30). + * + * Passing `limit: 0` returns all results (unlimited). + * + * Note: the page logic relies on this being consistent + * across queries for more pages. + */ + limit?: U64; + /** + * Sort the results by this field. + * Defaults to Name. Non-Name sorts are applied in memory + * after querying all matching resources. + */ + sort_by?: SwarmSortBy; + /** Reverse the sort direction. */ + sort_desc?: boolean; } /** @@ -9013,6 +10088,19 @@ export interface ListTags { query?: MongoDocument; } +export enum TerminalSortBy { + /** Sort by name. Default. */ + Name = "Name", + /** Sort by target. */ + Target = "Target", + /** Sort by init command. */ + Command = "Command", + /** Sort by stored size. */ + Size = "Size", + /** Sort by created timestamp. */ + Created = "Created", +} + /** * List Terminals. * Response: [ListTerminalsResponse]. @@ -9022,6 +10110,34 @@ export interface ListTerminals { target?: TerminalTarget; /** Return results with resource names instead of ids. */ use_names?: boolean; + /** + * Filter by terminal name. + * Returned terminals have names which contain all terms. + */ + terms?: string[]; + /** + * Retrieve more results by incrementing the page. + * `page: 0` is default. + */ + page?: U64; + /** + * Set the limit for number of terminals per-page. + * If not provided, uses the Core config + * `default_pagination_limit` (default: 30). + * + * Passing `limit: 0` returns all results (unlimited). + * + * Note: the page logic relies on this being consistent + * across queries for more pages. + */ + limit?: U64; + /** + * Sort the results by this field. + * Defaults to Name. + */ + sort_by?: TerminalSortBy; + /** Reverse the sort direction. */ + sort_desc?: boolean; } /** @@ -9134,6 +10250,17 @@ export interface ListUsers { export interface ListVariables { } +/** + * List all container volumes on the target server. + * Response: [ListVolumesResponse]. + * + * Pre v2.3.0, called `ListDockerVolumes` + */ +export interface ListVolumes { + /** Id or name */ + server: string; +} + export interface NameAndId { name: string; id: string; @@ -9935,8 +11062,12 @@ export interface SendAlert { /** Configuration for a Komodo Server Builder. */ export interface ServerBuilderConfig { - /** The server id of the builder */ - server_id?: string; + /** + * The server ids of the builders. + * If multiple are given, builds will overflow + * to later specified servers as needed. + */ + server_ids?: string[]; } /** The health of a part of the server. */ @@ -10276,17 +11407,6 @@ export interface UpdateDeployment { config: _PartialDeploymentConfig; } -/** - * **Admin only.** Update a docker registry account. - * Response: [DockerRegistryAccount]. - */ -export interface UpdateDockerRegistryAccount { - /** The id of the docker registry to update */ - id: string; - /** The partial docker registry account. */ - account: _PartialDockerRegistryAccount; -} - /** * **Admin only.** Update a git provider account. * Response: [GitProviderAccount]. @@ -10298,6 +11418,19 @@ export interface UpdateGitProviderAccount { account: _PartialGitProviderAccount; } +/** + * **Admin only.** Update a image registry account. + * Response: [ImageRegistryAccount]. + * + * Pre v2.3.0, called `UpdateDockerRegistryAccount` + */ +export interface UpdateImageRegistryAccount { + /** The id of the image registry to update */ + id: string; + /** The partial image registry account. */ + account: _PartialImageRegistryAccount; +} + /** * **Admin only.** Update an onboarding key. * Response: The updated [OnboardingKey]. @@ -10695,8 +11828,10 @@ export type ExecuteRequest = | { type: "CancelRepoBuild", params: CancelRepoBuild } | { type: "RunProcedure", params: RunProcedure } | { type: "BatchRunProcedure", params: BatchRunProcedure } + | { type: "CancelProcedure", params: CancelProcedure } | { type: "RunAction", params: RunAction } | { type: "BatchRunAction", params: BatchRunAction } + | { type: "CancelAction", params: CancelAction } | { type: "RunSync", params: RunSync } | { type: "TestAlerter", params: TestAlerter } | { type: "SendAlert", params: SendAlert } @@ -10828,7 +11963,7 @@ export type ReadRequest = | { type: "GetCoreInfo", params: GetCoreInfo } | { type: "ListSecrets", params: ListSecrets } | { type: "ListGitProvidersFromConfig", params: ListGitProvidersFromConfig } - | { type: "ListDockerRegistriesFromConfig", params: ListDockerRegistriesFromConfig } + | { type: "ListImageRegistriesFromConfig", params: ListImageRegistriesFromConfig } | { type: "GetSwarmsSummary", params: GetSwarmsSummary } | { type: "GetSwarm", params: GetSwarm } | { type: "GetSwarmActionState", params: GetSwarmActionState } @@ -10858,21 +11993,21 @@ export type ReadRequest = | { type: "ListServers", params: ListServers } | { type: "ListFullServers", params: ListFullServers } | { type: "ListTerminals", params: ListTerminals } - | { type: "GetDockerContainersSummary", params: GetDockerContainersSummary } - | { type: "ListAllDockerContainers", params: ListAllDockerContainers } - | { type: "ListDockerContainers", params: ListDockerContainers } - | { type: "InspectDockerContainer", params: InspectDockerContainer } + | { type: "GetContainersSummary", params: GetContainersSummary } + | { type: "ListAllContainers", params: ListAllContainers } + | { type: "ListContainers", params: ListContainers } + | { type: "InspectContainer", params: InspectContainer } | { type: "GetResourceMatchingContainer", params: GetResourceMatchingContainer } | { type: "GetContainerLog", params: GetContainerLog } | { type: "SearchContainerLog", params: SearchContainerLog } | { type: "ListComposeProjects", params: ListComposeProjects } - | { type: "ListDockerNetworks", params: ListDockerNetworks } - | { type: "InspectDockerNetwork", params: InspectDockerNetwork } - | { type: "ListDockerImages", params: ListDockerImages } - | { type: "InspectDockerImage", params: InspectDockerImage } - | { type: "ListDockerImageHistory", params: ListDockerImageHistory } - | { type: "ListDockerVolumes", params: ListDockerVolumes } - | { type: "InspectDockerVolume", params: InspectDockerVolume } + | { type: "ListNetworks", params: ListNetworks } + | { type: "InspectNetwork", params: InspectNetwork } + | { type: "ListImages", params: ListImages } + | { type: "InspectImage", params: InspectImage } + | { type: "ListImageHistory", params: ListImageHistory } + | { type: "ListVolumes", params: ListVolumes } + | { type: "InspectVolume", params: InspectVolume } | { type: "GetSystemInformation", params: GetSystemInformation } | { type: "GetSystemStats", params: GetSystemStats } | { type: "GetHistoricalServerStats", params: GetHistoricalServerStats } @@ -10887,6 +12022,7 @@ export type ReadRequest = | { type: "ListStacks", params: ListStacks } | { type: "ListFullStacks", params: ListFullStacks } | { type: "ListStackServices", params: ListStackServices } + | { type: "ListAllStackServices", params: ListAllStackServices } | { type: "ListCommonStackExtraArgs", params: ListCommonStackExtraArgs } | { type: "ListCommonStackBuildExtraArgs", params: ListCommonStackBuildExtraArgs } | { type: "GetDeploymentsSummary", params: GetDeploymentsSummary } @@ -10960,8 +12096,8 @@ export type ReadRequest = | { type: "ListVariables", params: ListVariables } | { type: "GetGitProviderAccount", params: GetGitProviderAccount } | { type: "ListGitProviderAccounts", params: ListGitProviderAccounts } - | { type: "GetDockerRegistryAccount", params: GetDockerRegistryAccount } - | { type: "ListDockerRegistryAccounts", params: ListDockerRegistryAccounts } + | { type: "GetImageRegistryAccount", params: GetImageRegistryAccount } + | { type: "ListImageRegistryAccounts", params: ListImageRegistryAccounts } | { type: "ListOnboardingKeys", params: ListOnboardingKeys }; export enum RepoWebhookAction { @@ -11130,9 +12266,9 @@ export type WriteRequest = | { type: "CreateGitProviderAccount", params: CreateGitProviderAccount } | { type: "UpdateGitProviderAccount", params: UpdateGitProviderAccount } | { type: "DeleteGitProviderAccount", params: DeleteGitProviderAccount } - | { type: "CreateDockerRegistryAccount", params: CreateDockerRegistryAccount } - | { type: "UpdateDockerRegistryAccount", params: UpdateDockerRegistryAccount } - | { type: "DeleteDockerRegistryAccount", params: DeleteDockerRegistryAccount } + | { type: "CreateImageRegistryAccount", params: CreateImageRegistryAccount } + | { type: "UpdateImageRegistryAccount", params: UpdateImageRegistryAccount } + | { type: "DeleteImageRegistryAccount", params: DeleteImageRegistryAccount } | { type: "CloseAlert", params: CloseAlert }; export type WsLoginMessage = diff --git a/client/core/ts/yarn.lock b/client/core/ts/yarn.lock index 45934a5002..64671311dc 100644 --- a/client/core/ts/yarn.lock +++ b/client/core/ts/yarn.lock @@ -7,10 +7,10 @@ jwt-decode@^4.0.0: resolved "https://registry.yarnpkg.com/jwt-decode/-/jwt-decode-4.0.0.tgz#2270352425fd413785b2faf11f6e755c5151bd4b" integrity sha512-+KJGIyHgkGuIq3IEBNftfhW/LfWhXUIY6OmyVWjliu5KH1y0fw7VQ8YndE2O4qZdMSd9SqbnC8GOcZEy0Om7sA== -mogh_auth_client@^1.5.0: - version "1.5.0" - resolved "https://registry.yarnpkg.com/mogh_auth_client/-/mogh_auth_client-1.5.0.tgz#f54b8ae35648d5bf1e60d7b67fcdcef45b0ccd7a" - integrity sha512-eUQmx682AzJRaY4QJSALtIGxXzg2r2Tr8exnDeFMrNe0Ibn4Oqm3h9SkOzYiqqZjKQ13wvHm0RoxT2QKo+908g== +mogh_auth_client@^1.7.0: + version "1.7.0" + resolved "https://registry.yarnpkg.com/mogh_auth_client/-/mogh_auth_client-1.7.0.tgz#a016c8dafd0f73633098d41aa95889c444e481d9" + integrity sha512-v4TJVNIiFYBLd70yGw/eS37E15he4I5w3poj5X7W75D+m6ry9gbhtlePp5elRbqjDZ/WXkOZ/XYCHqGYb/PQ2A== dependencies: jwt-decode "^4.0.0" diff --git a/client/periphery/rs/Cargo.toml b/client/periphery/rs/Cargo.toml index 4fa2768591..b7e7a1691e 100644 --- a/client/periphery/rs/Cargo.toml +++ b/client/periphery/rs/Cargo.toml @@ -2,7 +2,6 @@ name = "periphery_client" version.workspace = true edition.workspace = true -authors.workspace = true license.workspace = true homepage.workspace = true repository.workspace = true diff --git a/client/periphery/rs/src/api/build.rs b/client/periphery/rs/src/api/build.rs index d4e8fb55e1..f42257d8e0 100644 --- a/client/periphery/rs/src/api/build.rs +++ b/client/periphery/rs/src/api/build.rs @@ -1,9 +1,11 @@ use komodo_client::entities::{ - FileContents, repo::Repo, update::Log, + FileContents, NoData, repo::Repo, update::Log, }; use mogh_resolver::Resolve; use serde::{Deserialize, Serialize}; +// + #[derive(Serialize, Deserialize, Debug, Clone, Resolve)] #[response(BuildResponse)] #[error(anyhow::Error)] @@ -29,6 +31,15 @@ pub type BuildResponse = Vec; // +#[derive(Serialize, Deserialize, Debug, Clone, Resolve)] +#[response(NoData)] +#[error(anyhow::Error)] +pub struct CancelBuild { + pub id: String, +} + +// + /// Get the dockerfile contents on the host, for builds using /// `files_on_host`. #[derive(Debug, Clone, Serialize, Deserialize, Resolve)] diff --git a/client/periphery/rs/src/api/mod.rs b/client/periphery/rs/src/api/mod.rs index 76f4d13bd0..ef5c1d0219 100644 --- a/client/periphery/rs/src/api/mod.rs +++ b/client/periphery/rs/src/api/mod.rs @@ -1,6 +1,6 @@ use komodo_client::entities::{ FileContents, - config::{DockerRegistry, GitProvider}, + config::{GitProvider, ImageRegistry}, stack::{StackRemoteFileContents, StackServiceNames}, update::Log, }; @@ -66,11 +66,11 @@ pub type ListGitProvidersResponse = Vec; // #[derive(Serialize, Deserialize, Debug, Clone, Resolve)] -#[response(ListDockerRegistriesResponse)] +#[response(ListImageRegistriesResponse)] #[error(anyhow::Error)] -pub struct ListDockerRegistries {} +pub struct ListImageRegistries {} -pub type ListDockerRegistriesResponse = Vec; +pub type ListImageRegistriesResponse = Vec; // diff --git a/compose/compose.env b/compose/compose.env index a3fe6553f8..428556d232 100644 --- a/compose/compose.env +++ b/compose/compose.env @@ -57,6 +57,9 @@ KOMODO_DISABLE_CONFIRM_DIALOG=false ## Disable creating the default Procedures on first startup. KOMODO_DISABLE_INIT_RESOURCES=false +## Change the default pagination limit for the API and UI to use. +KOMODO_DEFAULT_PAGINATION_LIMIT=50 + ## Used to auth incoming webhooks. Alt: KOMODO_WEBHOOK_SECRET_FILE KOMODO_WEBHOOK_SECRET=a_random_secret ## Used to generate jwt. Alt: KOMODO_JWT_SECRET_FILE diff --git a/config/core.config.toml b/config/core.config.toml index a662275249..f8c949efa3 100644 --- a/config/core.config.toml +++ b/config/core.config.toml @@ -79,6 +79,11 @@ private_key = "file:/config/keys/core.key" ## Optional, no default. # first_server_address = "" +## Set the default pagination limit for the API and UI to use. +## Env: KOMODO_DEFAULT_PAGINATION_LIMIT +## Default: 50 +default_pagination_limit = 50 + ## Disables write support on resources in the UI. ## This protects users that that would normally have write priviledges during their UI usage, ## when they intend to fully rely on ResourceSyncs to manage config. @@ -623,15 +628,15 @@ aws.secret_access_key = "" ## They allow these Resources to pull private images. ## They cannot be configured on the environment. -## configure docker registries -# [[docker_registry]] +## configure image registries +# [[image_registry]] # domain = "docker.io" # accounts = [ # { username = "mbecker2020", token = "access_token_for_account" } # ] # organizations = ["DockerhubOrganization"] -# [[docker_registry]] +# [[image_registry]] # domain = "git.mogh.tech" # use a custom provider, like self-hosted gitea # accounts = [ # { username = "mbecker20", token = "access_token_for_account" }, diff --git a/config/periphery.config.toml b/config/periphery.config.toml index 0da92f59bc..838e4ba948 100644 --- a/config/periphery.config.toml +++ b/config/periphery.config.toml @@ -270,15 +270,15 @@ pretty_startup_config = false # REGISTRY PROVIDERS # ###################### -## Configure Periphery based docker registries -# [[docker_registry]] +## Configure Periphery based image registries +# [[image_registry]] # domain = "docker.io" # accounts = [ # { username = "mbecker2020", token = "access_token_for_account" } # ] # organizations = ["DockerhubOrganization"] -# [[docker_registry]] +# [[image_registry]] # domain = "git.mogh.tech" # use a custom provider, like self-hosted gitea # accounts = [ # { username = "mbecker20", token = "access_token_for_account" }, diff --git a/docsite/docs/configuration/providers.md b/docsite/docs/configuration/providers.md index c32fec6197..41c78a32a3 100644 --- a/docsite/docs/configuration/providers.md +++ b/docsite/docs/configuration/providers.md @@ -91,20 +91,20 @@ Komodo supports pushing and pulling images from any Docker-compatible registry, ```toml # in core.config.toml or periphery.config.toml -[[docker_registry]] +[[image_registry]] domain = "docker.io" accounts = [ { username = "my-user", token = "dckr_pat_xxxxxxxxxxxx" }, ] organizations = ["MyOrg"] -[[docker_registry]] +[[image_registry]] domain = "ghcr.io" accounts = [ { username = "my-user", token = "ghp_xxxxxxxxxxxx" }, ] -[[docker_registry]] +[[image_registry]] domain = "registry.example.com" # self-hosted registry accounts = [ { username = "my-user", token = "access_token" }, diff --git a/docsite/docs/releases/v2.0.0.md b/docsite/docs/releases/v2.0.0.md index 0cf433f874..765492dfbd 100644 --- a/docsite/docs/releases/v2.0.0.md +++ b/docsite/docs/releases/v2.0.0.md @@ -1,4 +1,4 @@ -# v2.0.0 +# v2.0.0 – March 2026 Komodo v2 is a major release with significant architectural changes and new features. diff --git a/example/alerter/Cargo.toml b/example/alerter/Cargo.toml index 58a1dbc818..d2c9d71c27 100644 --- a/example/alerter/Cargo.toml +++ b/example/alerter/Cargo.toml @@ -2,7 +2,6 @@ name = "alerter" version.workspace = true edition.workspace = true -authors.workspace = true license.workspace = true homepage.workspace = true repository.workspace = true diff --git a/example/alerter/Dockerfile b/example/alerter/Dockerfile index 9ffd8a07f3..20e51f0e0a 100644 --- a/example/alerter/Dockerfile +++ b/example/alerter/Dockerfile @@ -1,4 +1,4 @@ -FROM rust:1.95.0-trixie as builder +FROM rust:1.97.1-trixie as builder WORKDIR /builder COPY . . diff --git a/example/update_logger/Cargo.toml b/example/update_logger/Cargo.toml index fa51460570..afe4397c24 100644 --- a/example/update_logger/Cargo.toml +++ b/example/update_logger/Cargo.toml @@ -2,7 +2,6 @@ name = "update_logger" version.workspace = true edition.workspace = true -authors.workspace = true license.workspace = true homepage.workspace = true repository.workspace = true diff --git a/example/update_logger/Dockerfile b/example/update_logger/Dockerfile index a4bc44fa1e..26533c8bb9 100644 --- a/example/update_logger/Dockerfile +++ b/example/update_logger/Dockerfile @@ -1,4 +1,4 @@ -FROM rust:1.95.0-trixie as builder +FROM rust:1.97.1-trixie as builder WORKDIR /builder COPY . . diff --git a/lib/command/Cargo.toml b/lib/command/Cargo.toml index 00912dddcc..802eede407 100644 --- a/lib/command/Cargo.toml +++ b/lib/command/Cargo.toml @@ -2,7 +2,6 @@ name = "command" version.workspace = true edition.workspace = true -authors.workspace = true license.workspace = true repository.workspace = true homepage.workspace = true @@ -13,4 +12,6 @@ komodo_client.workspace = true svi.workspace = true # tokio.workspace = true +tokio-util.workspace = true shlex.workspace = true +libc.workspace = true diff --git a/lib/command/src/lib.rs b/lib/command/src/lib.rs index 53c5baed02..7cdf921088 100644 --- a/lib/command/src/lib.rs +++ b/lib/command/src/lib.rs @@ -1,40 +1,38 @@ -use std::{ - path::{Path, PathBuf}, - process::Stdio, - sync::OnceLock, -}; +use std::{path::PathBuf, process::Stdio, sync::OnceLock}; use komodo_client::{ entities::{komodo_timestamp, update::Log}, parsers::parse_multiline_command, }; +use tokio::process::Command; +mod options; mod output; +pub use options::*; pub use output::*; -use tokio::process::Command; /// Commands are run directly, and cannot include '&&' pub async fn run_komodo_standard_command( stage: &str, - path: impl Into>, command: impl Into, + options: CommandOptions<'_>, ) -> Log { let command = command.into(); let start_ts = komodo_timestamp(); - let output = run_standard_command(&command, path).await; + let output = run_standard_command(&command, options).await; output_into_log(stage, command, start_ts, output) } /// Commands are wrapped in 'sh -c', and can include '&&' pub async fn run_komodo_shell_command( stage: &str, - path: impl Into>, command: impl Into, + options: CommandOptions<'_>, ) -> Log { let command = command.into(); let start_ts = komodo_timestamp(); - let output = run_shell_command(&command, path).await; + let output = run_shell_command(&command, options).await; output_into_log(stage, command, start_ts, output) } @@ -47,14 +45,14 @@ pub async fn run_komodo_shell_command( /// ie if all the lines are commented out. pub async fn run_komodo_multiline_command( stage: &str, - path: impl Into>, command: impl AsRef, + options: CommandOptions<'_>, ) -> Option { let command = parse_multiline_command(command); if command.is_empty() { return None; } - Some(run_komodo_shell_command(stage, path, command).await) + Some(run_komodo_shell_command(stage, command, options).await) } pub enum KomodoCommandMode { @@ -73,28 +71,24 @@ pub enum KomodoCommandMode { /// See [parse_multiline_command]. pub async fn run_komodo_command_with_sanitization( stage: &str, - path: impl Into>, command: impl AsRef, + options: CommandOptions<'_>, mode: KomodoCommandMode, replacers: &[(String, String)], ) -> Option { let mut log = match mode { - KomodoCommandMode::Standard => run_komodo_standard_command( - stage, - path, - command.as_ref().to_string(), - ) - .await - .into(), - KomodoCommandMode::Shell => run_komodo_shell_command( - stage, - path, - command.as_ref().to_string(), - ) - .await - .into(), + KomodoCommandMode::Standard => { + run_komodo_standard_command(stage, command.as_ref(), options) + .await + .into() + } + KomodoCommandMode::Shell => { + run_komodo_shell_command(stage, command.as_ref(), options) + .await + .into() + } KomodoCommandMode::Multiline => { - run_komodo_multiline_command(stage, path, command).await + run_komodo_multiline_command(stage, command, options).await } }?; @@ -124,10 +118,13 @@ pub fn output_into_log( } } -/// Commands are run directly, and cannot include '&&' +/// Commands are run directly, and cannot include '&&'. +/// +/// See [CommandOptions] for the available `timeout` / `cancel` / `stdin` +/// controls. pub async fn run_standard_command( command: &str, - path: impl Into>, + options: CommandOptions<'_>, ) -> CommandOutput { let lexed = if let Some(lexed) = shlex::split(command) && !lexed.is_empty() @@ -141,23 +138,9 @@ pub async fn run_standard_command( let mut cmd = Command::new(&lexed[0]); - cmd - .args(&lexed[1..]) - .kill_on_drop(true) - .stdin(Stdio::null()) - .stdout(Stdio::piped()) - .stderr(Stdio::piped()); - - if let Some(path) = path.into() { - match path.canonicalize() { - Ok(path) => { - cmd.current_dir(path); - } - Err(e) => return CommandOutput::from_err(e), - } - } + cmd.args(&lexed[1..]).kill_on_drop(true); - CommandOutput::from(cmd.output().await) + run_command(cmd, options).await } fn shell() -> &'static str { @@ -178,19 +161,45 @@ fn shell() -> &'static str { }) } -/// Commands are wrapped in 'sh -c', and can include '&&' +/// Commands are wrapped in 'sh -c', and can include '&&'. +/// +/// See [CommandOptions] for the available `timeout` / `cancel` / `stdin` +/// controls. pub async fn run_shell_command( command: &str, - path: impl Into>, + options: CommandOptions<'_>, ) -> CommandOutput { let mut cmd = Command::new(shell()); - cmd - .args(["-c", command]) - .kill_on_drop(true) - .stdin(Stdio::null()); + cmd.args(["-c", command]).kill_on_drop(true); - if let Some(path) = path.into() { + run_command(cmd, options).await +} + +/// Runs the command to completion, returning its output. +/// +/// With an empty `options`, this is just `cmd.output()`. +/// +/// With a `timeout` and/or `cancel` token, the child is spawned in its own +/// process group (via `process_group(0)`, so the child's pid is also its +/// process group id). If the timeout elapses or the token is cancelled +/// before the command finishes, the entire process group is killed with +/// `SIGKILL` — not just the direct child — so any descendants the command +/// spawned (e.g. processes started by a `sh -c` wrapper) are torn down too. +/// `kill_on_drop(true)` remains set as a backstop to reap the direct child. +async fn run_command( + mut cmd: Command, + options: CommandOptions<'_>, +) -> CommandOutput { + let CommandOptions { + path, + timeout, + cancel, + stdin, + } = options; + + // Attach the path to cmd as current dir + if let Some(path) = path { match path.canonicalize() { Ok(path) => { cmd.current_dir(path); @@ -199,5 +208,263 @@ pub async fn run_shell_command( } } - CommandOutput::from(cmd.output().await) + // Anything to write needs a pipe, otherwise the command gets no input. + cmd.stdin(if stdin.is_some() { + Stdio::piped() + } else { + Stdio::null() + }); + + // Fast path: nothing to write, and nothing to wait + // on besides the command itself. + if timeout.is_none() && cancel.is_none() && stdin.is_none() { + return CommandOutput::from(cmd.output().await); + } + + // `output()` configures stdout/stderr automatically, but since + // we spawn manually we set them here too. + cmd.stdout(Stdio::piped()).stderr(Stdio::piped()); + + // Place the child in a new process group so the whole group can be + // signalled together. Only needed when there is something to signal on. + if timeout.is_some() || cancel.is_some() { + cmd.process_group(0); + } + + let mut child = match cmd.spawn() { + Ok(child) => child, + Err(e) => return CommandOutput::from_err(e), + }; + + // Written from a task, so that a payload larger than the pipe buffer + // cannot deadlock against a child which is filling up its stdout. + if let Some(stdin) = stdin + && let Some(mut pipe) = child.stdin.take() + { + let stdin = stdin.to_string(); + tokio::spawn(async move { + use tokio::io::AsyncWriteExt; + let _ = pipe.write_all(stdin.as_bytes()).await; + // Dropping the pipe closes the child's stdin, so it stops reading. + let _ = pipe.shutdown().await; + }); + } + + // Because of `process_group(0)`, the child's pid equals its pgid. + let pid = child.id(); + + // Nothing to signal on, so just wait for it to finish. + if timeout.is_none() && cancel.is_none() { + return CommandOutput::from(child.wait_with_output().await); + } + + // Resolve to `()` only when the relevant control fires; otherwise stay + // pending forever so it never wins the `select!`. + let on_timeout = async { + match timeout { + Some(timeout) => tokio::time::sleep(timeout).await, + None => std::future::pending().await, + } + }; + let on_cancel = async { + match &cancel { + Some(cancel) => cancel.cancelled().await, + None => std::future::pending().await, + } + }; + + let killed_reason = tokio::select! { + output = child.wait_with_output() => { + return CommandOutput::from(output); + } + _ = on_timeout => format!( + "Command timed out after {:.1}s (process group killed)", + // `timeout` is `Some` here, since `on_timeout` only fires when set. + timeout.map(|t| t.as_secs_f64()).unwrap_or_default(), + ), + _ = on_cancel => { + "Command cancelled (process group killed)".to_string() + } + }; + + kill_process_group(pid); + CommandOutput::from_err_message(killed_reason) +} + +/// Sends `SIGKILL` to the entire process group led by `pid`. +/// +/// A negative pid targets the whole group, so a child spawned with +/// `process_group(0)` (group leader) is killed along with all of its +/// descendants. +fn kill_process_group(pid: Option) { + let Some(pid) = pid else { + return; + }; + // SAFETY: `kill` is a simple syscall with no memory safety concerns; + // we only signal our own child's process group. + unsafe { + libc::kill(-(pid as libc::pid_t), libc::SIGKILL); + } +} + +#[cfg(test)] +mod tests { + use std::time::Duration; + + use tokio_util::sync::CancellationToken; + + use super::*; + + /// On timeout, a backgrounded grandchild (here `sleep 31337`, started + /// with `&` so it is not the direct child of our spawned shell) must be + /// killed along with the rest of the process group. + #[tokio::test] + async fn timeout_kills_process_group() { + // Unique sleep duration so we can pgrep for exactly this process. + let marker = "sleep 31337"; + let out = run_shell_command( + &format!("{marker} & sleep 31336"), + CommandOptions::default().timeout(Duration::from_millis(300)), + ) + .await; + + // The command should have reported a timeout failure. + assert!(!out.success(), "expected timeout failure, got: {out:?}"); + assert!( + out.stderr.contains("timed out"), + "expected timeout error, got: {out:?}" + ); + + // Give the kernel a moment to reap the killed group. + tokio::time::sleep(Duration::from_millis(200)).await; + + let still_running = std::process::Command::new("pgrep") + .args(["-f", marker]) + .output() + .expect("pgrep should run"); + let pids = String::from_utf8_lossy(&still_running.stdout); + let pids = pids.trim(); + assert!( + pids.is_empty(), + "backgrounded grandchild survived timeout: pids={pids:?}" + ); + } + + /// Neither runner configures stdout/stderr itself — the fast path relies + /// on `output()` doing it, and the spawn path on [run_command]. Both must + /// still capture, rather than leaking output to the parent. + #[tokio::test] + async fn output_is_captured_on_both_spawn_paths() { + // Fast path: no stdin, timeout or cancel, so `cmd.output()`. + let fast = + run_standard_command("echo out", CommandOptions::default()) + .await; + assert!(fast.success(), "{fast:?}"); + assert_eq!(fast.stdout.trim(), "out"); + + // Spawn path, reached here by setting a timeout. + let spawned = run_shell_command( + "echo out; echo err >&2", + CommandOptions::default().timeout(Duration::from_secs(10)), + ) + .await; + assert!(spawned.success(), "{spawned:?}"); + assert_eq!(spawned.stdout.trim(), "out"); + assert_eq!(spawned.stderr.trim(), "err"); + } + + /// Data set as [CommandOptions::stdin] must reach the child on its + /// standard input. + #[tokio::test] + async fn stdin_is_delivered_to_child() { + let payload = "some-secret-token\nwith two lines"; + let out = run_standard_command( + "cat", + CommandOptions::default().stdin(payload), + ) + .await; + + assert!(out.success(), "cat failed: {out:?}"); + assert_eq!(out.stdout, payload); + } + + /// The whole point of [CommandOptions::stdin]: a secret passed this way + /// must not be visible in the process arguments, where any user on the + /// host could read it with `ps`. The shell equivalent, which interpolates + /// the secret into the command, is checked alongside it to confirm the + /// test would actually catch a regression. + #[tokio::test] + async fn stdin_secret_is_not_in_process_args() { + let secret = "komodo-stdin-secret-marker"; + + let visible_in_ps = |marker: &'static str| async move { + // Give the child a moment to actually be running. + tokio::time::sleep(Duration::from_millis(300)).await; + let out = std::process::Command::new("pgrep") + .args(["-f", marker]) + .output() + .expect("pgrep should run"); + !String::from_utf8_lossy(&out.stdout).trim().is_empty() + }; + + let (_, via_stdin) = tokio::join!( + run_standard_command( + "sleep 1", + CommandOptions::default().stdin(secret) + ), + visible_in_ps(secret) + ); + assert!(!via_stdin, "secret passed via stdin showed up in ps"); + + // Control: interpolating it into the command does expose it. + let interpolated = format!("echo {secret} | sleep 1"); + let (_, via_command) = tokio::join!( + run_shell_command(&interpolated, CommandOptions::default()), + visible_in_ps(secret) + ); + assert!( + via_command, + "control failed: interpolated secret should be visible in ps" + ); + } + + /// Cancelling the token mid-run must kill the whole process group, just + /// like a timeout does, and return promptly with a cancellation error. + #[tokio::test] + async fn cancel_kills_process_group() { + let marker = "sleep 41337"; + let cancel = CancellationToken::new(); + + // Cancel shortly after the command starts. + let cancel_clone = cancel.clone(); + tokio::spawn(async move { + tokio::time::sleep(Duration::from_millis(300)).await; + cancel_clone.cancel(); + }); + + let out = run_shell_command( + &format!("{marker} & sleep 41336"), + CommandOptions::default().cancel(cancel), + ) + .await; + + assert!(!out.success(), "expected cancel failure, got: {out:?}"); + assert!( + out.stderr.contains("cancelled"), + "expected cancellation error, got: {out:?}" + ); + + tokio::time::sleep(Duration::from_millis(200)).await; + + let still_running = std::process::Command::new("pgrep") + .args(["-f", marker]) + .output() + .expect("pgrep should run"); + let pids = String::from_utf8_lossy(&still_running.stdout); + let pids = pids.trim(); + assert!( + pids.is_empty(), + "backgrounded grandchild survived cancellation: pids={pids:?}" + ); + } } diff --git a/lib/command/src/options.rs b/lib/command/src/options.rs new file mode 100644 index 0000000000..84a10a857d --- /dev/null +++ b/lib/command/src/options.rs @@ -0,0 +1,58 @@ +use std::{path::Path, time::Duration}; + +use tokio_util::sync::CancellationToken; + +/// Controls for how a command is executed. +/// +/// When either timeout or cancel is set, the child is spawned in its own process group so +/// that, on timeout or cancellation, the entire group (the command and any +/// descendants it spawned) is killed together — not just the direct child. +#[derive(Default, Clone)] +pub struct CommandOptions<'a> { + /// Run the command at a particular path + pub path: Option<&'a Path>, + /// Kill the command (and its process group) if this duration elapses + /// before it finishes. + pub timeout: Option, + /// Kill the command (and its process group) when this token is + /// cancelled, allowing cancellation from elsewhere. + pub cancel: Option, + /// Write this to the command's standard input. Without it, + /// the command gets no input at all. + /// + /// Use this to hand a secret to a command which reads it from stdin, + /// such as `docker login --password-stdin`. Unlike interpolating the + /// secret into the command, it never appears in the process arguments, + /// where any user on the host could read it out of `ps`. + pub stdin: Option<&'a str>, +} + +impl<'a> CommandOptions<'a> { + pub fn path(mut self, path: impl Into>) -> Self { + self.path = path.into(); + self + } + + pub fn stdin(mut self, stdin: impl Into>) -> Self { + self.stdin = stdin.into(); + self + } +} + +impl CommandOptions<'_> { + pub fn timeout( + mut self, + timeout: impl Into>, + ) -> Self { + self.timeout = timeout.into(); + self + } + + pub fn cancel( + mut self, + cancel: impl Into>, + ) -> Self { + self.cancel = cancel.into(); + self + } +} diff --git a/lib/command/src/output.rs b/lib/command/src/output.rs index b2016e8bac..7403fac0c9 100644 --- a/lib/command/src/output.rs +++ b/lib/command/src/output.rs @@ -29,7 +29,15 @@ impl CommandOutput { Self { status: ExitStatus::from_raw(1), stdout: "".to_string(), - stderr: format!("{e:#?}"), + stderr: format!("{e:?}"), + } + } + + pub fn from_err_message(e: String) -> Self { + Self { + status: ExitStatus::from_raw(1), + stdout: "".to_string(), + stderr: e, } } diff --git a/lib/database/Cargo.toml b/lib/database/Cargo.toml index 26d38492ef..4421fe006f 100644 --- a/lib/database/Cargo.toml +++ b/lib/database/Cargo.toml @@ -2,7 +2,6 @@ name = "database" version.workspace = true edition.workspace = true -authors.workspace = true license.workspace = true repository.workspace = true homepage.workspace = true diff --git a/lib/database/src/lib.rs b/lib/database/src/lib.rs index bf46f611ea..d0e50e1d2c 100644 --- a/lib/database/src/lib.rs +++ b/lib/database/src/lib.rs @@ -13,7 +13,7 @@ use komodo_client::entities::{ onboarding_key::OnboardingKey, permission::Permission, procedure::Procedure, - provider::{DockerRegistryAccount, GitProviderAccount}, + provider::{GitProviderAccount, ImageRegistryAccount}, repo::Repo, server::Server, stack::Stack, @@ -52,7 +52,7 @@ pub struct Client { pub tags: Collection, pub variables: Collection, pub git_accounts: Collection, - pub registry_accounts: Collection, + pub registry_accounts: Collection, pub updates: Collection, pub alerts: Collection, pub stats: Collection, diff --git a/lib/encoding/Cargo.toml b/lib/encoding/Cargo.toml index c2178cadd5..63abb3dc7f 100644 --- a/lib/encoding/Cargo.toml +++ b/lib/encoding/Cargo.toml @@ -2,7 +2,6 @@ name = "encoding" version.workspace = true edition.workspace = true -authors.workspace = true license.workspace = true repository.workspace = true homepage.workspace = true diff --git a/lib/environment/Cargo.toml b/lib/environment/Cargo.toml index a9d821fc72..ce7e91d5ce 100644 --- a/lib/environment/Cargo.toml +++ b/lib/environment/Cargo.toml @@ -2,7 +2,6 @@ name = "environment" version.workspace = true edition.workspace = true -authors.workspace = true license.workspace = true repository.workspace = true homepage.workspace = true diff --git a/lib/formatting/Cargo.toml b/lib/formatting/Cargo.toml index abc2fe23d1..b6d17731dd 100644 --- a/lib/formatting/Cargo.toml +++ b/lib/formatting/Cargo.toml @@ -2,7 +2,6 @@ name = "formatting" version.workspace = true edition.workspace = true -authors.workspace = true license.workspace = true repository.workspace = true homepage.workspace = true diff --git a/lib/git/Cargo.toml b/lib/git/Cargo.toml index df618a85ab..8de2fa564b 100644 --- a/lib/git/Cargo.toml +++ b/lib/git/Cargo.toml @@ -2,7 +2,6 @@ name = "git" version.workspace = true edition.workspace = true -authors.workspace = true license.workspace = true repository.workspace = true homepage.workspace = true diff --git a/lib/git/src/clone.rs b/lib/git/src/clone.rs index 0f3371f34c..1c3bb08057 100644 --- a/lib/git/src/clone.rs +++ b/lib/git/src/clone.rs @@ -1,14 +1,14 @@ use std::{io::ErrorKind, path::Path}; use anyhow::Context; -use command::run_komodo_standard_command; +use command::{CommandOptions, run_komodo_standard_command}; use formatting::format_serror; use komodo_client::entities::{ RepoExecutionArgs, RepoExecutionResponse, all_logs_success, update::Log, }; -use crate::get_commit_hash_log; +use crate::{check_installed, get_commit_hash_log}; /// Will delete the existing repo folder, /// clone the repo, get the latest hash / message, @@ -24,6 +24,8 @@ pub async fn clone( where T: Into + std::fmt::Debug, { + check_installed().await?; + let args: RepoExecutionArgs = clone_args.into(); let repo_url = args.remote_url(access_token.as_deref())?; @@ -70,8 +72,12 @@ where args.branch ); - let mut log = - run_komodo_standard_command("Clone Repo", None, command).await; + let mut log = run_komodo_standard_command( + "Clone Repo", + command, + CommandOptions::default(), + ) + .await; if let Some(token) = access_token { log.command = log.command.replace(&token, ""); @@ -88,8 +94,8 @@ where if let Some(commit) = args.commit { let reset_log = run_komodo_standard_command( "set commit", - res.path.as_path(), format!("git reset --hard {commit}",), + CommandOptions::default().path(res.path.as_path()), ) .await; res.logs.push(reset_log); diff --git a/lib/git/src/commit.rs b/lib/git/src/commit.rs index 42a6ca0743..060e56b365 100644 --- a/lib/git/src/commit.rs +++ b/lib/git/src/commit.rs @@ -1,13 +1,15 @@ use std::path::{Path, PathBuf}; use anyhow::Context; -use command::{run_komodo_standard_command, run_standard_command}; +use command::{ + CommandOptions, run_komodo_standard_command, run_standard_command, +}; use formatting::format_serror; use komodo_client::entities::{ RepoExecutionResponse, all_logs_success, update::Log, }; -use crate::get_commit_hash_log; +use crate::{check_installed, get_commit_hash_log}; /// Write file, add, commit, force push. /// Repo must be cloned. @@ -91,12 +93,19 @@ pub async fn commit_file_inner( file: &Path, branch: &str, ) { + if let Err(e) = check_installed().await { + res + .logs + .push(Log::error("Commit", format_serror(&e.into()))); + return; + }; + ensure_global_git_config_set().await; let add_log = run_komodo_standard_command( "Add Files", - repo_dir, format!("git add {}", file.display()), + CommandOptions::default().path(repo_dir), ) .await; res.logs.push(add_log); @@ -106,10 +115,10 @@ pub async fn commit_file_inner( let commit_log = run_komodo_standard_command( "Commit", - repo_dir, format!( r#"git commit -m "[Komodo] {commit_msg}: update {file:?}""#, ), + CommandOptions::default().path(repo_dir), ) .await; @@ -140,8 +149,8 @@ pub async fn commit_file_inner( let push_log = run_komodo_standard_command( "Push", - repo_dir, format!("git push --set-upstream origin {branch}"), + CommandOptions::default().path(repo_dir), ) .await; @@ -155,8 +164,6 @@ pub async fn commit_all( message: &str, branch: &str, ) -> RepoExecutionResponse { - ensure_global_git_config_set().await; - let mut res = RepoExecutionResponse { path: repo_dir.to_path_buf(), logs: Vec::new(), @@ -164,9 +171,21 @@ pub async fn commit_all( commit_message: None, }; - let add_log = - run_komodo_standard_command("Add Files", repo_dir, "git add -A") - .await; + if let Err(e) = check_installed().await { + res + .logs + .push(Log::error("Commit", format_serror(&e.into()))); + return res; + }; + + ensure_global_git_config_set().await; + + let add_log = run_komodo_standard_command( + "Add Files", + "git add -A", + CommandOptions::default().path(repo_dir), + ) + .await; res.logs.push(add_log); if !all_logs_success(&res.logs) { return res; @@ -174,8 +193,8 @@ pub async fn commit_all( let commit_log = run_komodo_standard_command( "Commit", - repo_dir, format!(r#"git commit -m "[Komodo] {message}""#), + CommandOptions::default().path(repo_dir), ) .await; res.logs.push(commit_log); @@ -200,8 +219,8 @@ pub async fn commit_all( let push_log = run_komodo_standard_command( "Push", - repo_dir, format!("git push --set-upstream origin {branch}"), + CommandOptions::default().path(repo_dir), ) .await; res.logs.push(push_log); @@ -212,23 +231,25 @@ pub async fn commit_all( async fn ensure_global_git_config_set() { let res = run_standard_command( "git config --global --get user.email", - None, + CommandOptions::default(), ) .await; if !res.success() { let _ = run_standard_command( "git config --global user.email komodo@komo.do", - None, + CommandOptions::default(), ) .await; } - let res = - run_standard_command("git config --global --get user.name", None) - .await; + let res = run_standard_command( + "git config --global --get user.name", + CommandOptions::default(), + ) + .await; if !res.success() { let _ = run_standard_command( "git config --global user.name komodo", - None, + CommandOptions::default(), ) .await; } diff --git a/lib/git/src/init.rs b/lib/git/src/init.rs index 33b159fa9c..f8a4d6273c 100644 --- a/lib/git/src/init.rs +++ b/lib/git/src/init.rs @@ -1,21 +1,31 @@ use std::path::Path; -use command::run_komodo_standard_command; +use command::{CommandOptions, run_komodo_standard_command}; use formatting::format_serror; use komodo_client::entities::{ RepoExecutionArgs, all_logs_success, update::Log, }; +use crate::check_installed; + pub async fn init_folder_as_repo( folder_path: &Path, args: &RepoExecutionArgs, access_token: Option<&str>, logs: &mut Vec, ) { + if let Err(e) = check_installed().await { + logs.push(Log::error("Git Init", format_serror(&e.into()))); + return; + }; + // Initialize the folder as a git repo - let init_repo = - run_komodo_standard_command("Git Init", folder_path, "git init") - .await; + let init_repo = run_komodo_standard_command( + "Git Init", + "git init", + CommandOptions::default().path(folder_path), + ) + .await; logs.push(init_repo); if !all_logs_success(logs) { return; @@ -33,8 +43,8 @@ pub async fn init_folder_as_repo( // Set remote url let mut set_remote = run_komodo_standard_command( "Add git remote", - folder_path, format!("git remote add origin {repo_url}"), + CommandOptions::default().path(folder_path), ) .await; // Sanitize the output @@ -51,8 +61,8 @@ pub async fn init_folder_as_repo( // Set branch. let init_repo = run_komodo_standard_command( "Set Branch", - folder_path, format!("git switch -c {}", args.branch), + CommandOptions::default().path(folder_path), ) .await; if !init_repo.success { diff --git a/lib/git/src/installed.rs b/lib/git/src/installed.rs new file mode 100644 index 0000000000..6b3af67003 --- /dev/null +++ b/lib/git/src/installed.rs @@ -0,0 +1,21 @@ +use std::time::Duration; + +use anyhow::anyhow; +use command::{CommandOptions, run_standard_command}; + +/// Returns error if git not installed +pub async fn check_installed() -> anyhow::Result<()> { + if run_standard_command( + "which git", + CommandOptions::default().timeout(Duration::from_secs(1)), + ) + .await + .success() + { + Ok(()) + } else { + Err(anyhow!( + "Failed: 'git' is not installed or available on $PATH" + )) + } +} diff --git a/lib/git/src/lib.rs b/lib/git/src/lib.rs index 6a209ad0b6..fbf19398cb 100644 --- a/lib/git/src/lib.rs +++ b/lib/git/src/lib.rs @@ -1,7 +1,7 @@ -use std::path::Path; +use std::{path::Path, time::Duration}; use anyhow::anyhow; -use command::run_standard_command; +use command::{CommandOptions, run_standard_command}; use formatting::{bold, muted}; use komodo_client::entities::{ LatestCommit, komodo_timestamp, update::Log, @@ -10,6 +10,7 @@ use komodo_client::entities::{ mod clone; mod commit; mod init; +mod installed; mod pull; mod pull_or_clone; @@ -17,6 +18,7 @@ pub use crate::{ clone::clone, commit::{commit_all, commit_file, write_commit_file}, init::init_folder_as_repo, + installed::check_installed, pull::pull, pull_or_clone::pull_or_clone, }; @@ -24,9 +26,14 @@ pub use crate::{ pub async fn get_commit_hash_info( repo_dir: &Path, ) -> anyhow::Result { - let hash = - run_standard_command("git rev-parse --short HEAD", repo_dir) - .await; + check_installed().await?; + let hash = run_standard_command( + "git rev-parse --short HEAD", + CommandOptions::default() + .path(repo_dir) + .timeout(Duration::from_secs(1)), + ) + .await; let hash = if hash.status.success() { hash.stdout.trim().to_string() } else { @@ -35,8 +42,13 @@ pub async fn get_commit_hash_info( hash.stderr )); }; - let message = - run_standard_command("git log -1 --pretty=%B", repo_dir).await; + let message = run_standard_command( + "git log -1 --pretty=%B", + CommandOptions::default() + .path(repo_dir) + .timeout(Duration::from_secs(1)), + ) + .await; let message = if message.status.success() { message.stdout.trim().to_string() } else { @@ -76,8 +88,14 @@ pub async fn get_commit_hash_log( /// Gets the remote url, with `.git` stripped from the end. pub async fn get_remote_url(path: &Path) -> anyhow::Result { - let output = - run_standard_command("git remote show origin", path).await; + check_installed().await?; + let output = run_standard_command( + "git remote show origin", + CommandOptions::default() + .path(path) + .timeout(Duration::from_secs(1)), + ) + .await; if output.success() { Ok( output diff --git a/lib/git/src/pull.rs b/lib/git/src/pull.rs index c9e5e76955..18ac693393 100644 --- a/lib/git/src/pull.rs +++ b/lib/git/src/pull.rs @@ -3,7 +3,7 @@ use std::{ sync::OnceLock, }; -use command::run_komodo_standard_command; +use command::{CommandOptions, run_komodo_standard_command}; use formatting::format_serror; use komodo_client::entities::{ RepoExecutionArgs, RepoExecutionResponse, all_logs_success, @@ -11,7 +11,7 @@ use komodo_client::entities::{ }; use mogh_cache::TimeoutCache; -use crate::get_commit_hash_log; +use crate::{check_installed, get_commit_hash_log}; /// Wait this long after a pull to allow another pull through const PULL_TIMEOUT: i64 = 5_000; @@ -27,7 +27,6 @@ fn pull_cache() /// This will pull in a way that handles edge cases /// from possible state of the repo. For example, the user /// can change branch after clone, or even the remote. -#[allow(clippy::too_many_arguments)] pub async fn pull( clone_args: T, root_repo_dir: &Path, @@ -36,6 +35,8 @@ pub async fn pull( where T: Into + std::fmt::Debug, { + check_installed().await?; + let args: RepoExecutionArgs = clone_args.into(); let repo_url = args.remote_url(access_token.as_deref())?; @@ -78,8 +79,8 @@ where // Set remote url let mut set_remote = run_komodo_standard_command( "Set Git Remote", - res.path.as_ref(), format!("git remote set-url origin {repo_url}"), + CommandOptions::default().path(res.path.as_ref()), ) .await; // Sanitize the output @@ -99,8 +100,8 @@ where // First fetch remote branches before checkout let fetch = run_komodo_standard_command( "Git Fetch", - res.path.as_ref(), "git fetch --all --prune", + CommandOptions::default().path(res.path.as_ref()), ) .await; if !fetch.success { @@ -110,8 +111,8 @@ where let checkout = run_komodo_standard_command( "Checkout branch", - res.path.as_ref(), format!("git checkout -f {}", args.branch), + CommandOptions::default().path(res.path.as_ref()), ) .await; res.logs.push(checkout); @@ -121,8 +122,8 @@ where let pull_log = run_komodo_standard_command( "Git pull", - res.path.as_ref(), format!("git pull --rebase --force origin {}", args.branch), + CommandOptions::default().path(res.path.as_ref()), ) .await; res.logs.push(pull_log); @@ -133,8 +134,8 @@ where if let Some(commit) = args.commit { let reset_log = run_komodo_standard_command( "Set commit", - res.path.as_ref(), format!("git reset --hard {commit}"), + CommandOptions::default().path(res.path.as_ref()), ) .await; res.logs.push(reset_log); diff --git a/lib/interpolate/Cargo.toml b/lib/interpolate/Cargo.toml index 15389a53bd..22d498469b 100644 --- a/lib/interpolate/Cargo.toml +++ b/lib/interpolate/Cargo.toml @@ -2,7 +2,6 @@ name = "interpolate" version.workspace = true edition.workspace = true -authors.workspace = true license.workspace = true repository.workspace = true homepage.workspace = true diff --git a/lib/transport/Cargo.toml b/lib/transport/Cargo.toml index eba3756ee9..e83955371a 100644 --- a/lib/transport/Cargo.toml +++ b/lib/transport/Cargo.toml @@ -2,7 +2,6 @@ name = "transport" version.workspace = true edition.workspace = true -authors.workspace = true license.workspace = true repository.workspace = true homepage.workspace = true diff --git a/readme.md b/readme.md index ad92ff14bd..3f962daba2 100644 --- a/readme.md +++ b/readme.md @@ -14,7 +14,7 @@ A tool to build and deploy software across many servers. The Komodo dragon is the largest living member of the [*Monitor* family of lizards](https://en.wikipedia.org/wiki/Monitor_lizard). -There is no limit to the number of servers you can connect, and there will never be. There is no limit to what API you can use for automation, and there never will be. No "business edition" here. +There is no limit to the number of servers you can connect, and there will never be. There is no limit to what API you can use for automation, and there never will be. ## Disclaimer diff --git a/roadmap.md b/roadmap.md index 2033190740..5ca08b458f 100644 --- a/roadmap.md +++ b/roadmap.md @@ -13,7 +13,7 @@ If you have an idea for Komodo, feel free to open an issue beginning with the `[ - **v1.16**: "Action" resource: Run requests on the Komodo API using snippets of typescript. ✅ - **v1.17**: Procedure Schedules: Run procedures / Actions at scheduled times, like CRON job. Connect to host terminals and exec into containers ✅ - **v1.18**: Upgrade granular role based access control system ✅ -- **v2.0**: Support "Swarm" resource - Manage docker swarms, attach Deployments / Stacks to "Swarm". +- **v2.0**: Support "Swarm" resource - Manage docker swarms, attach Deployments / Stacks to "Swarm". ✅ - **Undecided**: Support "Cluster" resource - Manage Kubernetes cluster, can attach deployments to "Cluster" **Note. The specific versions associated with these features are not final.** \ No newline at end of file diff --git a/runfile.toml b/runfile.toml index 07b76e26ad..8ef787f5d5 100644 --- a/runfile.toml +++ b/runfile.toml @@ -72,9 +72,9 @@ cmd = "cargo doc --no-deps -p komodo_client && http-server -p 8050 target/doc" alias = "dk" cmd = "deno run --allow-all action/deploy.ts" -[deploy-komodo-fe-only] -alias = "dkf" -cmd = "deno run --allow-all action/deploy-fe.ts" +[deploy-komodo-ui-only] +alias = "dku" +cmd = "deno run --allow-all action/deploy-ui.ts" [build-komodo] alias = "bk" diff --git a/scripts/readme.md b/scripts/readme.md index 003099bd64..e787ad840d 100644 --- a/scripts/readme.md +++ b/scripts/readme.md @@ -27,6 +27,8 @@ options: https://raw.githubusercontent.com/moghtech/komodo/refs/heads/main/config/periphery.config.toml) --binary-url BINARY_URL Use alternate binary source (default: https://github.com/moghtech/komodo/releases/download) + --core-public-keys CORE_PUBLIC_KEYS + Comma separate list of accepted public keys to allow Core(s) to connect. May include "file:/..." ``` These scripts will set up Komodo Periphery on your hosts, managed by systemd. diff --git a/scripts/setup-periphery.py b/scripts/setup-periphery.py index 30d58bd6aa..425373ba7f 100644 --- a/scripts/setup-periphery.py +++ b/scripts/setup-periphery.py @@ -64,6 +64,12 @@ def parse_args(): help="Use alternate binary source" ) + p.add_argument( + "--core-public-keys", + default="", + help="Trust Komodo Core public keys. Comma separated list of base58 encoded public keys." + ) + return p.parse_args() def load_paths(args): @@ -143,6 +149,9 @@ def map_config_line(args, home_dir, line): ## Handle onboarding key if line.startswith("# onboarding_key =") and args.onboarding_key != None: return f'onboarding_key = "{args.onboarding_key}"' + ## Handle core public keys + if line.startswith("# core_public_keys =") and args.core_public_keys: + return f'core_public_keys = "{args.core_public_keys}"' return line def write_config(args, home_dir, config_dir): @@ -226,6 +235,9 @@ def main(): print(f'version: {args.version}') print(f'core address: {args.core_address}') + if args.core_public_keys: + print(f'core public keys: {args.core_public_keys}') + print(f'connect as: {args.connect_as}') print(f'user install: {args.user}') print(f'home dir: {home_dir}') diff --git a/ui/index.html b/ui/index.html index 5971c5936f..14427e763d 100644 --- a/ui/index.html +++ b/ui/index.html @@ -2,10 +2,8 @@ - - + - @@ -14,7 +12,7 @@ Komodo - +
diff --git a/ui/package.json b/ui/package.json index e898bf1bbc..2588f2d35b 100644 --- a/ui/package.json +++ b/ui/package.json @@ -10,48 +10,48 @@ "build-client": "cd ../client/core/ts && yarn && yarn build && yarn link" }, "dependencies": { - "@mantine/core": "^9.1.1", - "@mantine/form": "^9.1.1", - "@mantine/hooks": "^9.1.1", - "@mantine/notifications": "^9.1.1", - "@mantine/spotlight": "^9.1.1", + "@mantine/core": "^9.4.1", + "@mantine/form": "^9.4.1", + "@mantine/hooks": "^9.4.1", + "@mantine/notifications": "^9.4.1", + "@mantine/spotlight": "^9.4.1", "@monaco-editor/react": "^4.7.0", - "@tanstack/react-query": "^5.100.9", + "@tanstack/react-query": "^5.101.2", "@tanstack/react-table": "^8.21.3", "@xterm/addon-fit": "^0.11.0", "@xterm/xterm": "^6.0.0", "ansi-to-html": "^0.7.2", - "jotai": "^2.20.0", - "jotai-family": "^1.0.1", + "jotai": "^2.20.1", + "jotai-family": "^1.0.2", "jotai-location": "^0.6.2", - "lucide-react": "^1.14.0", - "mogh_auth_client": "^1.5.0", - "mogh_ui": "^0.6.1", - "monaco-editor": "^0.55.1", - "monaco-yaml": "^5.4.1", - "react": "^19.2.6", - "react-dom": "^19.2.6", + "lucide-react": "^1.22.0", + "mogh_auth_client": "^1.7.0", + "mogh_ui": "^0.7.6", + "monaco-editor": "^0.56.0", + "monaco-yaml": "^5.5.1", + "react": "^19.2.7", + "react-dom": "^19.2.7", "react-minimal-pie-chart": "^9.1.2", - "react-router-dom": "^7.15.0", - "react-xtermjs": "^1.0.11", - "recharts": "^3.8.1", - "sanitize-html": "^2.17.3", - "shell-quote": "^1.8.3" + "react-router-dom": "^7.18.1", + "react-xtermjs": "^1.0.12", + "recharts": "^3.9.1", + "sanitize-html": "^2.17.5", + "shell-quote": "^1.9.0" }, "devDependencies": { - "@types/node": "^25.6.2", - "@types/react": "^19.2.14", + "@types/node": "^26.0.1", + "@types/react": "^19.2.17", "@types/react-dom": "^19.2.3", "@types/sanitize-html": "^2.16.1", "@types/shell-quote": "^1.7.5", - "@vitejs/plugin-react": "^6.0.1", + "@vitejs/plugin-react": "^6.0.3", "dotenv": "^17.4.2", - "postcss": "^8.5.14", + "postcss": "^8.5.16", "postcss-preset-mantine": "^1.18.0", "postcss-simple-vars": "^7.0.1", - "sass-embedded": "^1.99.0", - "typescript": "^6.0.3", - "vite": "^8.0.11" + "sass-embedded": "^1.100.0", + "typescript": "^7.0.2", + "vite": "^8.1.2" }, "packageManager": "yarn@1.22.22+sha512.a6b2f7906b721bba3d67d4aff083df04dad64c399707841b7acf00f6b133b7ac24255f2652fa22ae3534329dc6180534e98d17432037ff6fd140556e2bb3137e" } diff --git a/ui/public/client/responses.d.ts b/ui/public/client/responses.d.ts index 22254a253e..7319afd1a7 100644 --- a/ui/public/client/responses.d.ts +++ b/ui/public/client/responses.d.ts @@ -4,7 +4,7 @@ export type ReadResponses = { GetCoreInfo: Types.GetCoreInfoResponse; ListSecrets: Types.ListSecretsResponse; ListGitProvidersFromConfig: Types.ListGitProvidersFromConfigResponse; - ListDockerRegistriesFromConfig: Types.ListDockerRegistriesFromConfigResponse; + ListImageRegistriesFromConfig: Types.ListImageRegistriesFromConfigResponse; GetSwarmsSummary: Types.GetSwarmsSummaryResponse; GetSwarm: Types.GetSwarmResponse; GetSwarmActionState: Types.GetSwarmActionStateResponse; @@ -34,21 +34,21 @@ export type ReadResponses = { ListServers: Types.ListServersResponse; ListFullServers: Types.ListFullServersResponse; ListTerminals: Types.ListTerminalsResponse; - GetDockerContainersSummary: Types.GetDockerContainersSummaryResponse; - ListAllDockerContainers: Types.ListAllDockerContainersResponse; - ListDockerContainers: Types.ListDockerContainersResponse; - InspectDockerContainer: Types.InspectDockerContainerResponse; + GetContainersSummary: Types.GetContainersSummaryResponse; + ListAllContainers: Types.ListAllContainersResponse; + ListContainers: Types.ListContainersResponse; + InspectContainer: Types.InspectContainerResponse; GetResourceMatchingContainer: Types.GetResourceMatchingContainerResponse; GetContainerLog: Types.GetContainerLogResponse; SearchContainerLog: Types.SearchContainerLogResponse; ListComposeProjects: Types.ListComposeProjectsResponse; - ListDockerNetworks: Types.ListDockerNetworksResponse; - InspectDockerNetwork: Types.InspectDockerNetworkResponse; - ListDockerImages: Types.ListDockerImagesResponse; - InspectDockerImage: Types.InspectDockerImageResponse; - ListDockerImageHistory: Types.ListDockerImageHistoryResponse; - ListDockerVolumes: Types.ListDockerVolumesResponse; - InspectDockerVolume: Types.InspectDockerVolumeResponse; + ListNetworks: Types.ListNetworksResponse; + InspectNetwork: Types.InspectNetworkResponse; + ListImages: Types.ListImagesResponse; + InspectImage: Types.InspectImageResponse; + ListImageHistory: Types.ListImageHistoryResponse; + ListVolumes: Types.ListVolumesResponse; + InspectVolume: Types.InspectVolumeResponse; GetSystemInformation: Types.GetSystemInformationResponse; GetSystemStats: Types.GetSystemStatsResponse; GetHistoricalServerStats: Types.GetHistoricalServerStatsResponse; @@ -63,6 +63,7 @@ export type ReadResponses = { ListStacks: Types.ListStacksResponse; ListFullStacks: Types.ListFullStacksResponse; ListStackServices: Types.ListStackServicesResponse; + ListAllStackServices: Types.ListAllStackServicesResponse; ListCommonStackExtraArgs: Types.ListCommonStackExtraArgsResponse; ListCommonStackBuildExtraArgs: Types.ListCommonStackBuildExtraArgsResponse; GetDeploymentsSummary: Types.GetDeploymentsSummaryResponse; @@ -136,8 +137,8 @@ export type ReadResponses = { ListVariables: Types.ListVariablesResponse; GetGitProviderAccount: Types.GetGitProviderAccountResponse; ListGitProviderAccounts: Types.ListGitProviderAccountsResponse; - GetDockerRegistryAccount: Types.GetDockerRegistryAccountResponse; - ListDockerRegistryAccounts: Types.ListDockerRegistryAccountsResponse; + GetImageRegistryAccount: Types.GetImageRegistryAccountResponse; + ListImageRegistryAccounts: Types.ListImageRegistryAccountsResponse; ListOnboardingKeys: Types.ListOnboardingKeysResponse; }; export type WriteResponses = { @@ -251,9 +252,9 @@ export type WriteResponses = { CreateGitProviderAccount: Types.CreateGitProviderAccountResponse; UpdateGitProviderAccount: Types.UpdateGitProviderAccountResponse; DeleteGitProviderAccount: Types.DeleteGitProviderAccountResponse; - CreateDockerRegistryAccount: Types.CreateDockerRegistryAccountResponse; - UpdateDockerRegistryAccount: Types.UpdateDockerRegistryAccountResponse; - DeleteDockerRegistryAccount: Types.DeleteDockerRegistryAccountResponse; + CreateImageRegistryAccount: Types.CreateImageRegistryAccountResponse; + UpdateImageRegistryAccount: Types.UpdateImageRegistryAccountResponse; + DeleteImageRegistryAccount: Types.DeleteImageRegistryAccountResponse; CloseAlert: Types.NoData; }; export type ExecuteResponses = { @@ -293,8 +294,10 @@ export type ExecuteResponses = { CancelRepoBuild: Types.Update; RunProcedure: Types.Update; BatchRunProcedure: Types.BatchExecutionResponse; + CancelProcedure: Types.Update; RunAction: Types.Update; BatchRunAction: Types.BatchExecutionResponse; + CancelAction: Types.Update; RunSync: Types.Update; TestAlerter: Types.Update; SendAlert: Types.Update; diff --git a/ui/public/client/types.d.ts b/ui/public/client/types.d.ts index 244a4c6fc4..b5abe89bab 100644 --- a/ui/public/client/types.d.ts +++ b/ui/public/client/types.d.ts @@ -140,14 +140,14 @@ export interface ResourceListItem { info: Info; } export declare enum ActionState { - /** Unknown case */ - Unknown = "Unknown", + /** Currently running */ + Running = "Running", /** Last clone / pull successful (or never cloned) */ Ok = "Ok", /** Last clone / pull failed */ Failed = "Failed", - /** Currently running */ - Running = "Running" + /** Unknown case */ + Unknown = "Unknown" } export interface ActionListItemInfo { /** Whether last action run successful */ @@ -166,6 +166,12 @@ export interface ActionListItemInfo { schedule_error?: string; } export type ActionListItem = ResourceListItem; +export declare enum TagQueryBehavior { + /** Returns resources which have strictly all the tags */ + All = "All", + /** Returns resources which have one or more of the tags */ + Any = "Any" +} export declare enum TemplatesQueryBehavior { /** Include templates in results. Default. */ Include = "Include", @@ -174,23 +180,33 @@ export declare enum TemplatesQueryBehavior { /** Results *only* includes templates. */ Only = "Only" } -export declare enum TagQueryBehavior { - /** Returns resources which have strictly all the tags */ - All = "All", - /** Returns resources which have one or more of the tags */ - Any = "Any" -} /** Passing empty Vec is the same as not filtering by that field */ export interface ResourceQuery { + /** + * List of search terms. Names must contain + * all terms to match. + */ + terms?: string[]; + /** List of exact names to return */ names?: string[]; - templates?: TemplatesQueryBehavior; /** Pass Vec of tag ids or tag names */ tags?: string[]; /** 'All' or 'Any' */ tag_behavior?: TagQueryBehavior; + templates?: TemplatesQueryBehavior; specific?: T; } export interface ActionQuerySpecifics { + /** + * Query only for Actions matching these states. + * If empty, does not filter by state. + */ + states?: ActionState[]; + /** + * Query only for Actions with (or without) + * a schedule configured. + */ + scheduled?: boolean; } export type ActionQuery = ResourceQuery; export type AlerterEndpoint = @@ -473,11 +489,13 @@ export declare enum Operation { RenameProcedure = "RenameProcedure", DeleteProcedure = "DeleteProcedure", RunProcedure = "RunProcedure", + CancelProcedure = "CancelProcedure", CreateAction = "CreateAction", UpdateAction = "UpdateAction", RenameAction = "RenameAction", DeleteAction = "DeleteAction", RunAction = "RunAction", + CancelAction = "CancelAction", CreateResourceSync = "CreateResourceSync", UpdateResourceSync = "UpdateResourceSync", RenameResourceSync = "RenameResourceSync", @@ -785,6 +803,8 @@ export interface BuildListItemInfo { dockerfile_contents: boolean; /** Linked repo, if one is attached. */ linked_repo: string; + /** The name of the linked repo, if one is attached. */ + linked_repo_name?: string; /** The git provider domain */ git_provider: string; /** The repo used as the source of the build */ @@ -804,11 +824,21 @@ export type BuildListItem = ResourceListItem; export interface BuildQuerySpecifics { builder_ids?: string[]; repos?: string[]; + /** + * Query only for Builds with these linked repos. + * Only accepts Repo id (not name). + */ + linked_repos?: string[]; /** * query for builds last built more recently than this timestamp * defaults to 0 which is a no op */ built_since?: I64; + /** + * Query only for Builds matching these states. + * If empty, does not filter by state. + */ + states?: BuildState[]; } export type BuildQuery = ResourceQuery; export type BuilderConfig = @@ -966,6 +996,9 @@ export type Execution = } | { type: "BatchRunProcedure"; params: BatchRunProcedure; +} | { + type: "CancelProcedure"; + params: CancelProcedure; } /** Run the target action. (alias: `action`, `ac`) */ | { @@ -974,6 +1007,9 @@ export type Execution = } | { type: "BatchRunAction"; params: BatchRunAction; +} | { + type: "CancelAction"; + params: CancelAction; } /** Execute a Resource Sync. (alias: `sync`) */ | { @@ -1181,22 +1217,26 @@ export interface CreateApiKeyResponse { secret: string; } export type CreateApiKeyForServiceUserResponse = CreateApiKeyResponse; -/** Configuration to access private image repositories on various registries. */ -export interface DockerRegistryAccount { +/** + * Configuration to access private git repos from various git providers. + * Note. Cannot create two accounts with the same domain and username. + */ +export interface GitProviderAccount { /** - * The Mongo ID of the docker registry account. + * The Mongo ID of the git provider account. * This field is de/serialized from/to JSON as - * `{ "_id": { "$oid": "..." }, ...(rest of DockerRegistryAccount) }` + * `{ "_id": { "$oid": "..." }, ...(rest of serialized User) }` */ _id?: MongoId; /** * The domain of the provider. * - * For docker registry, this can include 'http://...', - * however this is not recommended and won't work unless "insecure registries" are enabled - * on your hosts. See . + * For git, this cannot include the protocol eg 'http://', + * which is controlled with 'https' field. */ domain: string; + /** Whether git provider is accessed over http or https. */ + https: boolean; /** The account username */ username?: string; /** @@ -1205,27 +1245,23 @@ export interface DockerRegistryAccount { */ token?: string; } -export type CreateDockerRegistryAccountResponse = DockerRegistryAccount; -/** - * Configuration to access private git repos from various git providers. - * Note. Cannot create two accounts with the same domain and username. - */ -export interface GitProviderAccount { +export type CreateGitProviderAccountResponse = GitProviderAccount; +/** Configuration to access private image repositories on various registries. */ +export interface ImageRegistryAccount { /** - * The Mongo ID of the git provider account. + * The Mongo ID of the docker registry account. * This field is de/serialized from/to JSON as - * `{ "_id": { "$oid": "..." }, ...(rest of serialized User) }` + * `{ "_id": { "$oid": "..." }, ...(rest of ImageRegistryAccount) }` */ _id?: MongoId; /** * The domain of the provider. * - * For git, this cannot include the protocol eg 'http://', - * which is controlled with 'https' field. + * For docker registry, this can include 'http://...', + * however this is not recommended and won't work unless "insecure registries" are enabled + * on your hosts. See . */ domain: string; - /** Whether git provider is accessed over http or https. */ - https: boolean; /** The account username */ username?: string; /** @@ -1234,7 +1270,7 @@ export interface GitProviderAccount { */ token?: string; } -export type CreateGitProviderAccountResponse = GitProviderAccount; +export type CreateImageRegistryAccountResponse = ImageRegistryAccount; export type UserConfig = /** User that logs in with username / password */ { @@ -1359,8 +1395,8 @@ export interface Variable { } export type CreateVariableResponse = Variable; export type DeleteApiKeyForServiceUserResponse = NoData; -export type DeleteDockerRegistryAccountResponse = DockerRegistryAccount; export type DeleteGitProviderAccountResponse = GitProviderAccount; +export type DeleteImageRegistryAccountResponse = ImageRegistryAccount; /** * An public key used to authenticate new Periphery -> Core connections * to join Komodo as a newly created Server. @@ -1453,6 +1489,11 @@ export interface DeploymentConfig { * swarm_id overrides server_id and the Deployment will be in Swarm mode. */ server_id?: string; + /** + * Specify a custom container / service name, + * if different from Deployment name. + */ + custom_name?: string; /** * The image which the deployment deploys. * Can either be a user inputted image, or a Komodo Build. @@ -1550,6 +1591,12 @@ export interface DeploymentInfo { * This includes both the image name / tag, and the specific digest hash. */ latest_image_digest?: ImageDigest; + /** + * The container / service name used at the time of the last deploy. + * Kept to match the Deployment to its container / service + * even if the name configuration changes before the next deploy. + */ + deployed_name?: string; } export type Deployment = Resource; /** @@ -1591,14 +1638,24 @@ export interface DeploymentListItemInfo { state: DeploymentState; /** The status of the docker container (eg. up 12 hours, exited 5 minutes ago.) */ status?: string; + /** + * The container / service name, if different than + * the deployment name. Uses the currently deployed name + * if deployed, else the configured custom name. + */ + custom_name: string; /** The image attached to the deployment. */ image: string; /** Whether there is a newer image available at the same tag. */ update_available: boolean; /** The swarm that deployment is deployed on, when in Swarm mode. */ swarm_id: string; + /** The name of the swarm that deployment is deployed on, when in Swarm mode. */ + swarm_name?: string; /** The server that deployment is deployed on, when in Server mode. */ server_id: string; + /** The name of the server that deployment is deployed on, when in Server mode. */ + server_name?: string; /** An attached Komodo Build, if it exists. */ build_id?: string; } @@ -1610,6 +1667,12 @@ export interface DeploymentQuerySpecifics { * Only accepts Server id (not name). */ server_ids?: string[]; + /** + * Query only for Deployments on these Swarms. + * If empty, does not filter by Swarm. + * Only accepts Swarm id (not name). + */ + swarm_ids?: string[]; /** * Query only for Deployments with these Builds attached. * If empty, does not filter by Build. @@ -1618,6 +1681,11 @@ export interface DeploymentQuerySpecifics { build_ids?: string[]; /** Query only for Deployments with available image updates. */ update_available?: boolean; + /** + * Query only for Deployments matching these states. + * If empty, does not filter by state. + */ + states?: DeploymentState[]; } export type DeploymentQuery = ResourceQuery; /** Response containing pretty formatted toml contents. */ @@ -2049,8 +2117,8 @@ export interface ContainerStats { pids: string; } export type GetDeploymentStatsResponse = ContainerStats; -export type GetDockerRegistryAccountResponse = DockerRegistryAccount; export type GetGitProviderAccountResponse = GitProviderAccount; +export type GetImageRegistryAccountResponse = ImageRegistryAccount; export declare enum Timelength { /** `1-sec` */ OneSecond = "1-sec", @@ -2407,15 +2475,17 @@ export interface ServerActionState { /** Server currently pruning system */ pruning_system: boolean; /** Server currently starting containers. */ - starting_containers: boolean; + starting_containers: number; /** Server currently restarting containers. */ - restarting_containers: boolean; + restarting_containers: number; /** Server currently pausing containers. */ - pausing_containers: boolean; + pausing_containers: number; /** Server currently unpausing containers. */ - unpausing_containers: boolean; + unpausing_containers: number; /** Server currently stopping containers. */ - stopping_containers: boolean; + stopping_containers: number; + /** Server currently destroying containers. */ + destroying_containers: number; } export type GetServerActionStateResponse = ServerActionState; /** Server configuration. */ @@ -2922,10 +2992,17 @@ export interface SystemInformation { kernel?: string; /** Physical core count */ core_count?: number; + /** + * Logical core count. If available, + * used to interpret system load accurately. + */ + logical_core_count?: number; /** System hostname based off DNS */ host_name?: string; /** The CPU's brand */ - cpu_brand: string; + cpu_brand?: string; + /** CPU architecture (eg. x86_64, aarch64, arm64) */ + cpu_arch?: string; } export type GetSystemInformationResponse = SystemInformation; export interface SystemLoadAverage { @@ -2960,10 +3037,33 @@ export interface SystemStats { * It may be different than mem_total_gb - mem_used_gb. */ mem_free_gb?: number; - /** Used memory in GB. 'Total' - 'Available' (not free) memory. */ + /** + * Used memory in GB. 'Total' - 'Available' (not free) memory, + * with the (reclaimable) ZFS ARC cache subtracted out. + */ mem_used_gb: number; /** Total memory in GB */ mem_total_gb: number; + /** + * [2.3.0+] + * Reclaimable page cache + buffers in GB. + */ + mem_buff_cache_gb?: number; + /** + * [2.3.0+] + * ZFS ARC cache in GB. 0 when ZFS is not present. + */ + mem_zfs_arc_gb?: number; + /** + * [2.3.0+] + * Total swap in GB. + */ + swap_total_gb?: number; + /** + * [2.3.0+] + * Used swap in GB. + */ + swap_used_gb?: number; /** Breakdown of individual disks, ie their usages, sizes, and mount points */ disks: SingleDiskUsage[]; /** Network ingress usage in MB */ @@ -3079,11 +3179,11 @@ export type GetVariableResponse = Variable; export declare enum ContainerStateStatusEnum { Running = "running", Created = "created", - Paused = "paused", Restarting = "restarting", - Exited = "exited", Stopping = "stopping", Removing = "removing", + Paused = "paused", + Exited = "exited", Dead = "dead", Empty = "" } @@ -3593,6 +3693,7 @@ export interface Container { Config?: ContainerConfig; NetworkSettings?: NetworkSettings; } +export type InspectContainerResponse = Container; export type InspectDeploymentContainerResponse = Container; /** The service mode. */ export declare enum SwarmServiceMode { @@ -4097,7 +4198,6 @@ export interface SwarmService { JobStatus?: ServiceJobStatus; } export type InspectDeploymentSwarmServiceResponse = SwarmService; -export type InspectDockerContainerResponse = Container; /** Describes the platform which the image in the manifest runs on, as defined in the [OCI Image Index Specification](https://github.com/opencontainers/image-spec/blob/v1.0.1/image-index.md). */ export interface OciPlatform { /** The CPU architecture, for example `amd64` or `ppc64`. */ @@ -4242,7 +4342,7 @@ export interface Image { RootFS?: ImageInspectRootFs; Metadata?: ImageInspectMetadata; } -export type InspectDockerImageResponse = Image; +export type InspectImageResponse = Image; export interface IpamConfig { Subnet?: string; IPRange?: string; @@ -4282,140 +4382,7 @@ export interface Network { Options?: Record; Labels?: Record; } -export type InspectDockerNetworkResponse = Network; -export declare enum VolumeScopeEnum { - Empty = "", - Local = "local", - Global = "global" -} -export declare enum ClusterVolumeSpecAccessModeScopeEnum { - Empty = "", - Single = "single", - Multi = "multi" -} -export declare enum ClusterVolumeSpecAccessModeSharingEnum { - Empty = "", - None = "none", - Readonly = "readonly", - Onewriter = "onewriter", - All = "all" -} -/** One cluster volume secret entry. Defines a key-value pair that is passed to the plugin. */ -export interface ClusterVolumeSpecAccessModeSecrets { - /** Key is the name of the key of the key-value pair passed to the plugin. */ - Key?: string; - /** Secret is the swarm Secret object from which to read data. This can be a Secret name or ID. The Secret data is retrieved by swarm and used as the value of the key-value pair passed to the plugin. */ - Secret?: string; -} -/** A map of topological domains to topological segments. For in depth details, see documentation for the Topology object in the CSI specification. */ -export interface Topology { - Segments?: Record; -} -/** Requirements for the accessible topology of the volume. These fields are optional. For an in-depth description of what these fields mean, see the CSI specification. */ -export interface ClusterVolumeSpecAccessModeAccessibilityRequirements { - /** A list of required topologies, at least one of which the volume must be accessible from. */ - Requisite?: Topology[]; - /** A list of topologies that the volume should attempt to be provisioned in. */ - Preferred?: Topology[]; -} -/** The desired capacity that the volume should be created with. If empty, the plugin will decide the capacity. */ -export interface ClusterVolumeSpecAccessModeCapacityRange { - /** The volume must be at least this big. The value of 0 indicates an unspecified minimum */ - RequiredBytes?: I64; - /** The volume must not be bigger than this. The value of 0 indicates an unspecified maximum. */ - LimitBytes?: I64; -} -export declare enum ClusterVolumeSpecAccessModeAvailabilityEnum { - Empty = "", - Active = "active", - Pause = "pause", - Drain = "drain" -} -/** Defines how the volume is used by tasks. */ -export interface ClusterVolumeSpecAccessMode { - /** The set of nodes this volume can be used on at one time. - `single` The volume may only be scheduled to one node at a time. - `multi` the volume may be scheduled to any supported number of nodes at a time. */ - Scope?: ClusterVolumeSpecAccessModeScopeEnum; - /** The number and way that different tasks can use this volume at one time. - `none` The volume may only be used by one task at a time. - `readonly` The volume may be used by any number of tasks, but they all must mount the volume as readonly - `onewriter` The volume may be used by any number of tasks, but only one may mount it as read/write. - `all` The volume may have any number of readers and writers. */ - Sharing?: ClusterVolumeSpecAccessModeSharingEnum; - /** Swarm Secrets that are passed to the CSI storage plugin when operating on this volume. */ - Secrets?: ClusterVolumeSpecAccessModeSecrets[]; - AccessibilityRequirements?: ClusterVolumeSpecAccessModeAccessibilityRequirements; - CapacityRange?: ClusterVolumeSpecAccessModeCapacityRange; - /** The availability of the volume for use in tasks. - `active` The volume is fully available for scheduling on the cluster - `pause` No new workloads should use the volume, but existing workloads are not stopped. - `drain` All workloads using this volume should be stopped and rescheduled, and no new ones should be started. */ - Availability?: ClusterVolumeSpecAccessModeAvailabilityEnum; -} -/** Cluster-specific options used to create the volume. */ -export interface ClusterVolumeSpec { - /** Group defines the volume group of this volume. Volumes belonging to the same group can be referred to by group name when creating Services. Referring to a volume by group instructs Swarm to treat volumes in that group interchangeably for the purpose of scheduling. Volumes with an empty string for a group technically all belong to the same, emptystring group. */ - Group?: string; - AccessMode?: ClusterVolumeSpecAccessMode; -} -/** Information about the global status of the volume. */ -export interface ClusterVolumeInfo { - /** The capacity of the volume in bytes. A value of 0 indicates that the capacity is unknown. */ - CapacityBytes?: I64; - /** A map of strings to strings returned from the storage plugin when the volume is created. */ - VolumeContext?: Record; - /** The ID of the volume as returned by the CSI storage plugin. This is distinct from the volume's ID as provided by Docker. This ID is never used by the user when communicating with Docker to refer to this volume. If the ID is blank, then the Volume has not been successfully created in the plugin yet. */ - VolumeID?: string; - /** The topology this volume is actually accessible from. */ - AccessibleTopology?: Topology[]; -} -export declare enum ClusterVolumePublishStatusStateEnum { - Empty = "", - PendingPublish = "pending-publish", - Published = "published", - PendingNodeUnpublish = "pending-node-unpublish", - PendingControllerUnpublish = "pending-controller-unpublish" -} -export interface ClusterVolumePublishStatus { - /** The ID of the Swarm node the volume is published on. */ - NodeID?: string; - /** The published state of the volume. * `pending-publish` The volume should be published to this node, but the call to the controller plugin to do so has not yet been successfully completed. * `published` The volume is published successfully to the node. * `pending-node-unpublish` The volume should be unpublished from the node, and the manager is awaiting confirmation from the worker that it has done so. * `pending-controller-unpublish` The volume is successfully unpublished from the node, but has not yet been successfully unpublished on the controller. */ - State?: ClusterVolumePublishStatusStateEnum; - /** A map of strings to strings returned by the CSI controller plugin when a volume is published. */ - PublishContext?: Record; -} -/** Options and information specific to, and only present on, Swarm CSI cluster volumes. */ -export interface ClusterVolume { - /** The Swarm ID of this volume. Because cluster volumes are Swarm objects, they have an ID, unlike non-cluster volumes. This ID can be used to refer to the Volume instead of the name. */ - ID?: string; - Version?: ObjectVersion; - CreatedAt?: string; - UpdatedAt?: string; - Spec?: ClusterVolumeSpec; - Info?: ClusterVolumeInfo; - /** The status of the volume as it pertains to its publishing and use on specific nodes */ - PublishStatus?: ClusterVolumePublishStatus[]; -} -/** Usage details about the volume. This information is used by the `GET /system/df` endpoint, and omitted in other endpoints. */ -export interface VolumeUsageData { - /** Amount of disk space used by the volume (in bytes). This information is only available for volumes created with the `\"local\"` volume driver. For volumes created with other volume drivers, this field is set to `-1` (\"not available\") */ - Size: I64; - /** The number of containers referencing this volume. This field is set to `-1` if the reference-count is not available. */ - RefCount: I64; -} -export interface Volume { - /** Name of the volume. */ - Name: string; - /** Name of the volume driver used by the volume. */ - Driver: string; - /** Mount path of the volume on the host. */ - Mountpoint: string; - /** Date/Time the volume was created. */ - CreatedAt?: string; - /** Low-level details about the volume, provided by the volume driver. Details are returned as a map with key/value pairs: `{\"key\":\"value\",\"key2\":\"value2\"}`. The `Status` field is optional, and is omitted if the volume driver does not support this feature. */ - Status?: string[]; - /** User-defined key/value metadata. */ - Labels?: Record; - /** The level at which the volume exists. Either `global` for cluster-wide, or `local` for machine level. */ - Scope?: VolumeScopeEnum; - ClusterVolume?: ClusterVolume; - /** The driver specific options used when creating the volume. */ - Options?: Record; - UsageData?: VolumeUsageData; -} -export type InspectDockerVolumeResponse = Volume; +export type InspectNetworkResponse = Network; export type InspectStackContainerResponse = Container; /** Swarm service list item. */ export interface SwarmServiceListItem { @@ -4842,30 +4809,165 @@ export interface SwarmTask { JobIteration?: ObjectVersion; } export type InspectSwarmTaskResponse = SwarmTask; -export type JsonObject = any; -export type ListActionsResponse = ActionListItem[]; -export type ListAlertersResponse = AlerterListItem[]; -export declare enum PortTypeEnum { - EMPTY = "", - TCP = "tcp", - UDP = "udp", - SCTP = "sctp" +export declare enum VolumeScopeEnum { + Empty = "", + Local = "local", + Global = "global" } -/** An open port on a container */ -export interface Port { - /** Host IP address that the container's port is mapped to */ - IP?: string; - /** Port on the container */ - PrivatePort?: number; - /** Port exposed on the host */ - PublicPort?: number; - Type?: PortTypeEnum; +export declare enum ClusterVolumeSpecAccessModeScopeEnum { + Empty = "", + Single = "single", + Multi = "multi" } -/** Container summary returned by container list apis. */ -export interface ContainerListItem { - /** The Server which hosts the container. */ - server_id?: string; - /** The first name in Names, not including the initial '/' */ +export declare enum ClusterVolumeSpecAccessModeSharingEnum { + Empty = "", + None = "none", + Readonly = "readonly", + Onewriter = "onewriter", + All = "all" +} +/** One cluster volume secret entry. Defines a key-value pair that is passed to the plugin. */ +export interface ClusterVolumeSpecAccessModeSecrets { + /** Key is the name of the key of the key-value pair passed to the plugin. */ + Key?: string; + /** Secret is the swarm Secret object from which to read data. This can be a Secret name or ID. The Secret data is retrieved by swarm and used as the value of the key-value pair passed to the plugin. */ + Secret?: string; +} +/** A map of topological domains to topological segments. For in depth details, see documentation for the Topology object in the CSI specification. */ +export interface Topology { + Segments?: Record; +} +/** Requirements for the accessible topology of the volume. These fields are optional. For an in-depth description of what these fields mean, see the CSI specification. */ +export interface ClusterVolumeSpecAccessModeAccessibilityRequirements { + /** A list of required topologies, at least one of which the volume must be accessible from. */ + Requisite?: Topology[]; + /** A list of topologies that the volume should attempt to be provisioned in. */ + Preferred?: Topology[]; +} +/** The desired capacity that the volume should be created with. If empty, the plugin will decide the capacity. */ +export interface ClusterVolumeSpecAccessModeCapacityRange { + /** The volume must be at least this big. The value of 0 indicates an unspecified minimum */ + RequiredBytes?: I64; + /** The volume must not be bigger than this. The value of 0 indicates an unspecified maximum. */ + LimitBytes?: I64; +} +export declare enum ClusterVolumeSpecAccessModeAvailabilityEnum { + Empty = "", + Active = "active", + Pause = "pause", + Drain = "drain" +} +/** Defines how the volume is used by tasks. */ +export interface ClusterVolumeSpecAccessMode { + /** The set of nodes this volume can be used on at one time. - `single` The volume may only be scheduled to one node at a time. - `multi` the volume may be scheduled to any supported number of nodes at a time. */ + Scope?: ClusterVolumeSpecAccessModeScopeEnum; + /** The number and way that different tasks can use this volume at one time. - `none` The volume may only be used by one task at a time. - `readonly` The volume may be used by any number of tasks, but they all must mount the volume as readonly - `onewriter` The volume may be used by any number of tasks, but only one may mount it as read/write. - `all` The volume may have any number of readers and writers. */ + Sharing?: ClusterVolumeSpecAccessModeSharingEnum; + /** Swarm Secrets that are passed to the CSI storage plugin when operating on this volume. */ + Secrets?: ClusterVolumeSpecAccessModeSecrets[]; + AccessibilityRequirements?: ClusterVolumeSpecAccessModeAccessibilityRequirements; + CapacityRange?: ClusterVolumeSpecAccessModeCapacityRange; + /** The availability of the volume for use in tasks. - `active` The volume is fully available for scheduling on the cluster - `pause` No new workloads should use the volume, but existing workloads are not stopped. - `drain` All workloads using this volume should be stopped and rescheduled, and no new ones should be started. */ + Availability?: ClusterVolumeSpecAccessModeAvailabilityEnum; +} +/** Cluster-specific options used to create the volume. */ +export interface ClusterVolumeSpec { + /** Group defines the volume group of this volume. Volumes belonging to the same group can be referred to by group name when creating Services. Referring to a volume by group instructs Swarm to treat volumes in that group interchangeably for the purpose of scheduling. Volumes with an empty string for a group technically all belong to the same, emptystring group. */ + Group?: string; + AccessMode?: ClusterVolumeSpecAccessMode; +} +/** Information about the global status of the volume. */ +export interface ClusterVolumeInfo { + /** The capacity of the volume in bytes. A value of 0 indicates that the capacity is unknown. */ + CapacityBytes?: I64; + /** A map of strings to strings returned from the storage plugin when the volume is created. */ + VolumeContext?: Record; + /** The ID of the volume as returned by the CSI storage plugin. This is distinct from the volume's ID as provided by Docker. This ID is never used by the user when communicating with Docker to refer to this volume. If the ID is blank, then the Volume has not been successfully created in the plugin yet. */ + VolumeID?: string; + /** The topology this volume is actually accessible from. */ + AccessibleTopology?: Topology[]; +} +export declare enum ClusterVolumePublishStatusStateEnum { + Empty = "", + PendingPublish = "pending-publish", + Published = "published", + PendingNodeUnpublish = "pending-node-unpublish", + PendingControllerUnpublish = "pending-controller-unpublish" +} +export interface ClusterVolumePublishStatus { + /** The ID of the Swarm node the volume is published on. */ + NodeID?: string; + /** The published state of the volume. * `pending-publish` The volume should be published to this node, but the call to the controller plugin to do so has not yet been successfully completed. * `published` The volume is published successfully to the node. * `pending-node-unpublish` The volume should be unpublished from the node, and the manager is awaiting confirmation from the worker that it has done so. * `pending-controller-unpublish` The volume is successfully unpublished from the node, but has not yet been successfully unpublished on the controller. */ + State?: ClusterVolumePublishStatusStateEnum; + /** A map of strings to strings returned by the CSI controller plugin when a volume is published. */ + PublishContext?: Record; +} +/** Options and information specific to, and only present on, Swarm CSI cluster volumes. */ +export interface ClusterVolume { + /** The Swarm ID of this volume. Because cluster volumes are Swarm objects, they have an ID, unlike non-cluster volumes. This ID can be used to refer to the Volume instead of the name. */ + ID?: string; + Version?: ObjectVersion; + CreatedAt?: string; + UpdatedAt?: string; + Spec?: ClusterVolumeSpec; + Info?: ClusterVolumeInfo; + /** The status of the volume as it pertains to its publishing and use on specific nodes */ + PublishStatus?: ClusterVolumePublishStatus[]; +} +/** Usage details about the volume. This information is used by the `GET /system/df` endpoint, and omitted in other endpoints. */ +export interface VolumeUsageData { + /** Amount of disk space used by the volume (in bytes). This information is only available for volumes created with the `\"local\"` volume driver. For volumes created with other volume drivers, this field is set to `-1` (\"not available\") */ + Size: I64; + /** The number of containers referencing this volume. This field is set to `-1` if the reference-count is not available. */ + RefCount: I64; +} +export interface Volume { + /** Name of the volume. */ + Name: string; + /** Name of the volume driver used by the volume. */ + Driver: string; + /** Mount path of the volume on the host. */ + Mountpoint: string; + /** Date/Time the volume was created. */ + CreatedAt?: string; + /** Low-level details about the volume, provided by the volume driver. Details are returned as a map with key/value pairs: `{\"key\":\"value\",\"key2\":\"value2\"}`. The `Status` field is optional, and is omitted if the volume driver does not support this feature. */ + Status?: string[]; + /** User-defined key/value metadata. */ + Labels?: Record; + /** The level at which the volume exists. Either `global` for cluster-wide, or `local` for machine level. */ + Scope?: VolumeScopeEnum; + ClusterVolume?: ClusterVolume; + /** The driver specific options used when creating the volume. */ + Options?: Record; + UsageData?: VolumeUsageData; +} +export type InspectVolumeResponse = Volume; +export type JsonObject = any; +export type ListActionsResponse = ActionListItem[]; +export type ListAlertersResponse = AlerterListItem[]; +export declare enum PortTypeEnum { + EMPTY = "", + TCP = "tcp", + UDP = "udp", + SCTP = "sctp" +} +/** An open port on a container */ +export interface Port { + /** Host IP address that the container's port is mapped to */ + IP?: string; + /** Port on the container */ + PrivatePort?: number; + /** Port exposed on the host */ + PublicPort?: number; + Type?: PortTypeEnum; +} +/** Container summary returned by container list apis. */ +export interface ContainerListItem { + /** The Server which hosts the container. */ + server_id?: string; + /** The name of the Server which hosts the container. */ + server_name?: string; + /** The first name in Names, not including the initial '/' */ name: string; /** The ID of this container */ id?: string; @@ -4900,7 +5002,57 @@ export interface ContainerListItem { */ labels?: Record; } -export type ListAllDockerContainersResponse = ContainerListItem[]; +export type ListAllContainersResponse = ContainerListItem[]; +/** + * Combined state options for + * both Server and Swarm based Stacks. + */ +export declare enum StackServiceState { + /** (Swarm) All tasks OK */ + Healthy = "Healthy", + /** (Swarm) Some tasks don't match desired state */ + Unhealthy = "Unhealthy", + /** (Swarm) All tasks down. */ + Down = "Down", + /** (Container) Container is running */ + Running = "Running", + /** (Container) Container is created */ + Created = "Created", + /** (Container) Container is paused */ + Paused = "Paused", + /** (Container) Container is restarting */ + Restarting = "Restarting", + /** (Container) Container is exited */ + Exited = "Exited", + /** (Container) Container is stopping */ + Stopping = "Stopping", + /** (Container) Container is removing */ + Removing = "Removing", + /** (Container) Container is dead */ + Dead = "Dead", + /** Unknown case */ + Unknown = "Unknown" +} +/** A stack service, whether server or swarm based. */ +export interface StackService { + /** The stack which the service is a part of. */ + stack_id: string; + /** The name of the stack which the service is a part of. */ + stack_name?: string; + /** The service name */ + service: string; + /** The service image */ + image: string; + /** The container (Server mode) */ + container?: ContainerListItem; + /** The service (Swarm mode) */ + swarm_service?: SwarmServiceListItem; + /** The service state */ + state: StackServiceState; + /** The service image digests */ + image_digests?: ImageDigest[]; +} +export type ListAllStackServicesResponse = StackService[]; /** An api key used to authenticate requests via request headers. */ export interface ApiKey { /** Unique key associated with secret */ @@ -4938,8 +5090,35 @@ export interface ComposeProject { compose_files: string[]; } export type ListComposeProjectsResponse = ComposeProject[]; +export type ListContainersResponse = ContainerListItem[]; export type ListDeploymentsResponse = DeploymentListItem[]; -export type ListDockerContainersResponse = ContainerListItem[]; +export type ListFullActionsResponse = Action[]; +export type ListFullAlertersResponse = Alerter[]; +export type ListFullBuildersResponse = Builder[]; +export type ListFullBuildsResponse = Build[]; +export type ListFullDeploymentsResponse = Deployment[]; +export type ListFullProceduresResponse = Procedure[]; +export type ListFullReposResponse = Repo[]; +export type ListFullResourceSyncsResponse = ResourceSync[]; +export type ListFullServersResponse = Server[]; +export type ListFullStacksResponse = Stack[]; +export type ListFullSwarmsResponse = Swarm[]; +export type ListGitProviderAccountsResponse = GitProviderAccount[]; +export interface ProviderAccount { + /** The account username. Required. */ + username: string; + /** The account access token. Required. */ + token?: string; +} +export interface GitProvider { + /** The git provider domain. Default: `github.com`. */ + domain: string; + /** Whether to use https. Default: true. */ + https: boolean; + /** The accounts on the git provider. Required. */ + accounts: ProviderAccount[]; +} +export type ListGitProvidersFromConfigResponse = GitProvider[]; /** individual image layer information in response to ImageHistory operation */ export interface ImageHistoryResponseItem { Id: string; @@ -4949,7 +5128,20 @@ export interface ImageHistoryResponseItem { Size: I64; Comment: string; } -export type ListDockerImageHistoryResponse = ImageHistoryResponseItem[]; +export type ListImageHistoryResponse = ImageHistoryResponseItem[]; +export interface ImageRegistry { + /** The image provider domain. Default: `docker.io`. */ + domain: string; + /** The accounts on the registry. Required. */ + accounts: ProviderAccount[]; + /** + * Available organizations on the registry provider. + * Used to push an image under an organization's repo rather than an account's repo. + */ + organizations?: string[]; +} +export type ListImageRegistriesFromConfigResponse = ImageRegistry[]; +export type ListImageRegistryAccountsResponse = ImageRegistryAccount[]; export interface ImageListItem { /** * ID is the content-addressable ID of an image. @@ -4976,7 +5168,7 @@ export interface ImageListItem { /** Whether the image is in use by any container */ in_use: boolean; } -export type ListDockerImagesResponse = ImageListItem[]; +export type ListImagesResponse = ImageListItem[]; export interface NetworkListItem { name?: string; id?: string; @@ -4993,60 +5185,7 @@ export interface NetworkListItem { /** Whether the network is attached to one or more containers */ in_use: boolean; } -export type ListDockerNetworksResponse = NetworkListItem[]; -export interface ProviderAccount { - /** The account username. Required. */ - username: string; - /** The account access token. Required. */ - token?: string; -} -export interface DockerRegistry { - /** The docker provider domain. Default: `docker.io`. */ - domain: string; - /** The accounts on the registry. Required. */ - accounts: ProviderAccount[]; - /** - * Available organizations on the registry provider. - * Used to push an image under an organization's repo rather than an account's repo. - */ - organizations?: string[]; -} -export type ListDockerRegistriesFromConfigResponse = DockerRegistry[]; -export type ListDockerRegistryAccountsResponse = DockerRegistryAccount[]; -export interface VolumeListItem { - /** The name of the volume */ - name: string; - driver: string; - mountpoint: string; - created?: string; - scope: VolumeScopeEnum; - /** Amount of disk space used by the volume (in bytes). This information is only available for volumes created with the `\"local\"` volume driver. For volumes created with other volume drivers, this field is set to `-1` (\"not available\") */ - size?: I64; - /** Whether the volume is currently attached to any container */ - in_use: boolean; -} -export type ListDockerVolumesResponse = VolumeListItem[]; -export type ListFullActionsResponse = Action[]; -export type ListFullAlertersResponse = Alerter[]; -export type ListFullBuildersResponse = Builder[]; -export type ListFullBuildsResponse = Build[]; -export type ListFullDeploymentsResponse = Deployment[]; -export type ListFullProceduresResponse = Procedure[]; -export type ListFullReposResponse = Repo[]; -export type ListFullResourceSyncsResponse = ResourceSync[]; -export type ListFullServersResponse = Server[]; -export type ListFullStacksResponse = Stack[]; -export type ListFullSwarmsResponse = Swarm[]; -export type ListGitProviderAccountsResponse = GitProviderAccount[]; -export interface GitProvider { - /** The git provider domain. Default: `github.com`. */ - domain: string; - /** Whether to use https. Default: true. */ - https: boolean; - /** The accounts on the git provider. Required. */ - accounts: ProviderAccount[]; -} -export type ListGitProvidersFromConfigResponse = GitProvider[]; +export type ListNetworksResponse = NetworkListItem[]; export type ListOnboardingKeysResponse = OnboardingKey[]; export type UserTarget = /** User Id */ @@ -5104,22 +5243,24 @@ export interface ProcedureListItemInfo { export type ProcedureListItem = ResourceListItem; export type ListProceduresResponse = ProcedureListItem[]; export declare enum RepoState { - /** Unknown case */ - Unknown = "Unknown", - /** Last clone / pull successful (or never cloned) */ - Ok = "Ok", - /** Last clone / pull failed */ - Failed = "Failed", /** Currently cloning */ Cloning = "Cloning", /** Currently pulling */ Pulling = "Pulling", /** Currently building */ - Building = "Building" + Building = "Building", + /** Last clone / pull successful (or never cloned) */ + Ok = "Ok", + /** Last clone / pull failed */ + Failed = "Failed", + /** Unknown case */ + Unknown = "Unknown" } export interface RepoListItemInfo { /** The server that repo sits on. */ server_id: string; + /** The name of the server that repo sits on. */ + server_name?: string; /** The builder that builds the repo. */ builder_id: string; /** Repo last cloned / pulled timestamp in ms. */ @@ -5172,6 +5313,8 @@ export interface ResourceSyncListItemInfo { resource_path: string[]; /** Linked repo, if one is attached. */ linked_repo: string; + /** The name of the linked repo, if one is attached. */ + linked_repo_name?: string; /** The git provider domain. */ git_provider: string; /** The Github repo used as the source of the sync resources */ @@ -5230,6 +5373,47 @@ export interface __Serror { trace: string[]; } export type _Serror = __Serror; +/** Realtime minimal system stats data (zero allocation) */ +export interface MinimalSystemStats { + /** Cpu usage percentage */ + cpu_perc: number; + /** Load average (1m, 5m, 15m) */ + load_average: SystemLoadAverage; + /** + * This is really the 'Free' memory, not the 'Available' memory. + * It may be different than mem_total_gb - mem_used_gb. + */ + mem_free_gb: number; + /** + * Used memory in GB. 'Total' - 'Available' (not free) memory, + * with the (reclaimable) ZFS ARC cache subtracted out. + */ + mem_used_gb: number; + /** Total memory in GB */ + mem_total_gb: number; + /** Reclaimable page cache + buffers in GB. */ + mem_buff_cache_gb: number; + /** ZFS ARC cache in GB. 0 when ZFS is not present. */ + mem_zfs_arc_gb: number; + /** Total swap in GB. */ + swap_total_gb: number; + /** Used swap in GB. */ + swap_used_gb: number; + /** Total size of all disks combined in GB */ + disk_total_gb: number; + /** Used portion of all disks combined in GB */ + disk_used_gb: number; + /** Network ingress usage in MB */ + network_ingress_bytes: number; + /** Network egress usage in MB */ + network_egress_bytes: number; + /** The rate the system stats are being polled from the system */ + polling_rate: Timelength; + /** Unix timestamp in milliseconds when stats were last polled */ + refresh_ts: I64; + /** Unix timestamp in milliseconds when disk list was last refreshed */ + refresh_list_ts: I64; +} export interface ServerListItemInfo { /** The server's state. */ state: ServerState; @@ -5238,6 +5422,12 @@ export interface ServerListItemInfo { * the server, message will be given here. */ err?: _Serror; + /** System stats, if available */ + stats?: MinimalSystemStats; + /** The server's number of physical cores. */ + core_count?: number; + /** The server's number of logical cores. */ + logical_core_count?: number; /** Region of the server. */ region: string; /** Address of the server, or null if empty. */ @@ -5278,18 +5468,6 @@ export interface ServerListItemInfo { } export type ServerListItem = ResourceListItem; export type ListServersResponse = ServerListItem[]; -export interface StackService { - /** The service name */ - service: string; - /** The service image */ - image: string; - /** The container (Server mode) */ - container?: ContainerListItem; - /** The service (Swarm mode) */ - swarm_service?: SwarmServiceListItem; - /** The service image digests */ - image_digests?: ImageDigest[]; -} export type ListStackServicesResponse = StackService[]; export declare enum StackState { /** The stack is currently re/deploying */ @@ -5318,14 +5496,20 @@ export declare enum StackState { export interface StackListItemInfo { /** The swarm that stack is deployed on, when in Swarm mode. */ swarm_id: string; + /** The name of the swarm that stack is deployed on, when in Swarm mode. */ + swarm_name?: string; /** The server that stack is deployed on, when in Server mode. */ server_id: string; + /** The name of the server that stack is deployed on, when in Server mode. */ + server_name?: string; /** Whether stack is using files on host mode */ files_on_host: boolean; /** Whether stack has file contents defined. */ file_contents: boolean; /** Linked repo, if one is attached. */ linked_repo: string; + /** The name of the linked repo, if one is attached. */ + linked_repo_name?: string; /** The git provider domain */ git_provider: string; /** The configured repo */ @@ -5522,6 +5706,11 @@ export interface Terminal { name: string; /** The target resource of the Terminal. */ target: TerminalTarget; + /** + * The name of the target resource (Server / Stack / Deployment). + * Resolved by Core when listing all terminals for a user. + */ + target_name?: string; /** The command used to init the shell. */ command: string; /** The size of the terminal history in memory. */ @@ -5537,19 +5726,58 @@ export type ListUserGroupsResponse = UserGroup[]; export type ListUserTargetPermissionsResponse = Permission[]; export type ListUsersResponse = User[]; export type ListVariablesResponse = Variable[]; +export interface VolumeListItem { + /** The name of the volume */ + name: string; + driver: string; + mountpoint: string; + created?: string; + scope: VolumeScopeEnum; + /** Amount of disk space used by the volume (in bytes). This information is only available for volumes created with the `\"local\"` volume driver. For volumes created with other volume drivers, this field is set to `-1` (\"not available\") */ + size?: I64; + /** Whether the volume is currently attached to any container */ + in_use: boolean; +} +export type ListVolumesResponse = VolumeListItem[]; export type MongoDocument = any; export interface ProcedureQuerySpecifics { + /** + * Query only for Procedures matching these states. + * If empty, does not filter by state. + */ + states?: ProcedureState[]; + /** + * Query only for Procedures with (or without) + * a schedule configured. + */ + scheduled?: boolean; } export type ProcedureQuery = ResourceQuery; export type PushRecentlyViewedResponse = NoData; export interface RepoQuerySpecifics { /** Filter repos by their repo. */ - repos: string[]; + repos?: string[]; + /** + * Query only for Repos on these Servers. + * If empty, does not filter by Server. + * Only accepts Server id (not name). + */ + server_ids?: string[]; + /** + * Query only for Repos matching these states. + * If empty, does not filter by state. + */ + states?: RepoState[]; } export type RepoQuery = ResourceQuery; export interface ResourceSyncQuerySpecifics { /** Filter syncs by their repo. */ - repos: string[]; + repos?: string[]; + /** + * Query only for Builds with these linked repos. + * Only accepts Repo id (not name). + */ + linked_repos?: string[]; } export type ResourceSyncQuery = ResourceQuery; export type SearchContainerLogResponse = Log; @@ -5557,6 +5785,11 @@ export type SearchDeploymentLogResponse = Log; export type SearchStackLogResponse = Log; export type SearchSwarmServiceLogResponse = Log; export interface ServerQuerySpecifics { + /** + * Query only for Servers matching these states. + * If empty, does not filter by state. + */ + states?: ServerState[]; } /** Server-specific query */ export type ServerQuery = ResourceQuery; @@ -5568,6 +5801,12 @@ export interface StackQuerySpecifics { * Only accepts Server id (not name). */ server_ids?: string[]; + /** + * Query only for Stacks on these Swarms. + * If empty, does not filter by Swarm. + * Only accepts Swarm id (not name). + */ + swarm_ids?: string[]; /** * Query only for Stacks with these linked repos. * Only accepts Repo id (not name). @@ -5577,6 +5816,11 @@ export interface StackQuerySpecifics { repos?: string[]; /** Query only for Stack with available image updates. */ update_available?: boolean; + /** + * Query only for Stacks matching these states. + * If empty, does not filter by state. + */ + states?: StackState[]; } export type StackQuery = ResourceQuery; export interface SwarmQuerySpecifics { @@ -5584,8 +5828,8 @@ export interface SwarmQuerySpecifics { servers: string[]; } export type SwarmQuery = ResourceQuery; -export type UpdateDockerRegistryAccountResponse = DockerRegistryAccount; export type UpdateGitProviderAccountResponse = GitProviderAccount; +export type UpdateImageRegistryAccountResponse = ImageRegistryAccount; export type UpdateOnboardingKeyResponse = OnboardingKey; export type UpdatePermissionOnResourceTypeResponse = NoData; export type UpdatePermissionOnTargetResponse = NoData; @@ -5603,8 +5847,8 @@ export type _PartialAwsBuilderConfig = Partial; export type _PartialBuildConfig = Partial; export type _PartialBuilderConfig = Partial; export type _PartialDeploymentConfig = Partial; -export type _PartialDockerRegistryAccount = Partial; export type _PartialGitProviderAccount = Partial; +export type _PartialImageRegistryAccount = Partial; export type _PartialProcedureConfig = Partial; export type _PartialRepoConfig = Partial; export type _PartialResourceSyncConfig = Partial; @@ -5671,8 +5915,12 @@ export interface AwsBuilderConfig { insecure_tls: boolean; /** Which git providers are available on the AMI */ git_providers?: GitProvider[]; - /** Which docker registries are available on the AMI. */ - docker_registries?: DockerRegistry[]; + /** + * Which image registries are available on the AMI. + * + * Pre v2.3.0, called `docker_registries` + */ + image_registries?: ImageRegistry[]; /** Which secrets are available on the AMI. */ secrets?: string[]; } @@ -5703,6 +5951,11 @@ export interface BatchBuildRepo { * ``` */ pattern: string; + /** + * Filter matches by tag. + * If empty, skips tag filtering. + */ + tags?: string[]; } /** Checks for newer image than what is deployed. Response: [BatchCheckDeploymentForUpdateResponse] */ export interface BatchCheckDeploymentForUpdate { @@ -5719,6 +5972,11 @@ export interface BatchCheckDeploymentForUpdate { * ``` */ pattern: string; + /** + * Filter matches by tag. + * If empty, skips tag filtering. + */ + tags?: string[]; /** * Normally resources with 'auto_update' will be * redeployed immediately if updates are found. @@ -5747,6 +6005,11 @@ export interface BatchCheckStackForUpdate { * ``` */ pattern: string; + /** + * Filter matches by tag. + * If empty, skips tag filtering. + */ + tags?: string[]; /** * Normally resources with 'auto_update' will be * redeployed immediately if updates are found. @@ -5780,6 +6043,11 @@ export interface BatchCloneRepo { * ``` */ pattern: string; + /** + * Filter matches by tag. + * If empty, skips tag filtering. + */ + tags?: string[]; } /** * Delete all terminals on many or all Servers. @@ -5804,6 +6072,11 @@ export interface BatchDeploy { * ``` */ pattern: string; + /** + * Filter matches by tag. + * If empty, skips tag filtering. + */ + tags?: string[]; } /** Deploys multiple Stacks in parallel that match pattern. Response: [BatchExecutionResponse]. */ export interface BatchDeployStack { @@ -5820,6 +6093,11 @@ export interface BatchDeployStack { * ``` */ pattern: string; + /** + * Filter matches by tag. + * If empty, skips tag filtering. + */ + tags?: string[]; } /** Deploys multiple Stacks if changed in parallel that match pattern. Response: [BatchExecutionResponse]. */ export interface BatchDeployStackIfChanged { @@ -5836,6 +6114,11 @@ export interface BatchDeployStackIfChanged { * ``` */ pattern: string; + /** + * Filter matches by tag. + * If empty, skips tag filtering. + */ + tags?: string[]; } /** Destroys multiple Deployments in parallel that match pattern. Response: [BatchExecutionResponse]. */ export interface BatchDestroyDeployment { @@ -5852,6 +6135,11 @@ export interface BatchDestroyDeployment { * ``` */ pattern: string; + /** + * Filter matches by tag. + * If empty, skips tag filtering. + */ + tags?: string[]; } /** Destroys multiple Stacks in parallel that match pattern. Response: [BatchExecutionResponse]. */ export interface BatchDestroyStack { @@ -5868,6 +6156,11 @@ export interface BatchDestroyStack { * ``` */ pattern: string; + /** + * Filter matches by tag. + * If empty, skips tag filtering. + */ + tags?: string[]; } export interface BatchExecutionResponseItemErr { name: string; @@ -5888,6 +6181,11 @@ export interface BatchPullRepo { * ``` */ pattern: string; + /** + * Filter matches by tag. + * If empty, skips tag filtering. + */ + tags?: string[]; } /** Pulls multiple Stacks in parallel that match pattern. Response: [BatchExecutionResponse]. */ export interface BatchPullStack { @@ -5904,6 +6202,11 @@ export interface BatchPullStack { * ``` */ pattern: string; + /** + * Filter matches by tag. + * If empty, skips tag filtering. + */ + tags?: string[]; } /** Runs multiple Actions in parallel that match pattern. Response: [BatchExecutionResponse] */ export interface BatchRunAction { @@ -5920,6 +6223,11 @@ export interface BatchRunAction { * ``` */ pattern: string; + /** + * Filter matches by tag. + * If empty, skips tag filtering. + */ + tags?: string[]; } /** Runs multiple builds in parallel that match pattern. Response: [BatchExecutionResponse]. */ export interface BatchRunBuild { @@ -5936,6 +6244,11 @@ export interface BatchRunBuild { * ``` */ pattern: string; + /** + * Filter matches by tag. + * If empty, skips tag filtering. + */ + tags?: string[]; } /** Runs multiple Procedures in parallel that match pattern. Response: [BatchExecutionResponse]. */ export interface BatchRunProcedure { @@ -5952,6 +6265,11 @@ export interface BatchRunProcedure { * ``` */ pattern: string; + /** + * Filter matches by tag. + * If empty, skips tag filtering. + */ + tags?: string[]; } /** * Builds the target repo, using the attached builder. Response: [Update]. @@ -5975,6 +6293,18 @@ export interface BuildStatsDay { count: number; ts: number; } +/** Cancels the target action run. Response: [Update] */ +export interface CancelAction { + /** Id or name */ + action: string; + /** + * The update id associated with the specific + * run to cancel + * Must provide either `action` + * or `update_id` + */ + update_id?: string; +} /** * Cancels the target build. * Only does anything if the build is `building` when called. @@ -5984,6 +6314,18 @@ export interface CancelBuild { /** Can be id or name */ build: string; } +/** Cancels the target procedure run. Response: [Update] */ +export interface CancelProcedure { + /** Id or name */ + procedure: string; + /** + * The update id associated with the specific + * run to cancel + * Must provide either `action` + * or `update_id` + */ + update_id?: string; +} /** * Cancels the target repo build. * Only does anything if the repo build is `building` when called. @@ -6513,13 +6855,6 @@ export interface CreateDeploymentFromContainer { /** The server id or name on which container exists. */ server: string; } -/** - * **Admin only.** Create a docker registry account. - * Response: [DockerRegistryAccount]. - */ -export interface CreateDockerRegistryAccount { - account: _PartialDockerRegistryAccount; -} /** * **Admin only.** Create a git provider account. * Response: [GitProviderAccount]. @@ -6531,6 +6866,15 @@ export interface CreateGitProviderAccount { */ account: _PartialGitProviderAccount; } +/** + * **Admin only.** Create an image registry account. + * Response: [ImageRegistryAccount]. + * + * Pre v2.3.0, called `CreateDockerRegistryAccount` + */ +export interface CreateImageRegistryAccount { + account: _PartialImageRegistryAccount; +} /** * **Admin only.** Create a local user. * Response: [User]. @@ -6810,14 +7154,6 @@ export interface DeleteDeployment { /** The id or name of the deployment to delete. */ id: string; } -/** - * **Admin only.** Delete a docker registry account. - * Response: [DockerRegistryAccount]. - */ -export interface DeleteDockerRegistryAccount { - /** The id of the docker registry account to delete */ - id: string; -} /** * **Admin only.** Delete a git provider account. * Response: [DeleteGitProviderAccountResponse]. @@ -6836,6 +7172,16 @@ export interface DeleteImage { /** The name of the image to delete. */ name: string; } +/** + * **Admin only.** Delete an image registry account. + * Response: [ImageRegistryAccount]. + * + * Pre v2.3.0, called `DeleteDockerRegistryAccount` + */ +export interface DeleteImageRegistryAccount { + /** The id of the image registry account to delete */ + id: string; +} /** * Delete a docker network. * Response: [Update] @@ -7406,6 +7752,27 @@ export interface GetContainerLog { /** Enable `--timestamps` */ timestamps?: boolean; } +/** + * Gets a summary of data relating to all containers. + * Response: [GetContainersSummaryResponse]. + * + * Pre v2.3.0, called `GetDockerContainersSummary` + */ +export interface GetContainersSummary { +} +/** Response for [GetContainersSummary] */ +export interface GetContainersSummaryResponse { + /** The total number of Containers */ + total: number; + /** The number of Containers with Running state */ + running: number; + /** The number of Containers with Stopped or Paused or Created state */ + stopped: number; + /** The number of Containers with Restarting or Dead state */ + unhealthy: number; + /** The number of Containers with Unknown state */ + unknown: number; +} /** * Get information about the Komodo Core API configuration. * Response: [GetCoreInfoResponse]. @@ -7436,6 +7803,8 @@ export interface GetCoreInfoResponse { timezone: string; /** Public key for Core / Periphery authentication. */ public_key: string; + /** Default pagination limit for the UI to use. */ + default_pagination_limit: U64; } /** Get a specific deployment by name or id. Response: [Deployment]. */ export interface GetDeployment { @@ -7516,32 +7885,6 @@ export interface GetDeploymentsSummaryResponse { /** The number of Deployments with Unknown state */ unknown: I64; } -/** - * Gets a summary of data relating to all containers. - * Response: [GetDockerContainersSummaryResponse]. - */ -export interface GetDockerContainersSummary { -} -/** Response for [GetDockerContainersSummary] */ -export interface GetDockerContainersSummaryResponse { - /** The total number of Containers */ - total: number; - /** The number of Containers with Running state */ - running: number; - /** The number of Containers with Stopped or Paused or Created state */ - stopped: number; - /** The number of Containers with Restarting or Dead state */ - unhealthy: number; - /** The number of Containers with Unknown state */ - unknown: number; -} -/** - * Get a specific docker registry account. - * Response: [GetDockerRegistryAccountResponse]. - */ -export interface GetDockerRegistryAccount { - id: string; -} /** * Get a specific git provider account. * Response: [GetGitProviderAccountResponse]. @@ -7578,6 +7921,26 @@ export interface SystemStatsRecord { mem_used_gb: number; /** Total memory in GB */ mem_total_gb: number; + /** + * [2.3.0+] + * Reclaimable page cache + buffers in GB. + */ + mem_buff_cache_gb?: number; + /** + * [2.3.0+] + * ZFS ARC cache in GB. 0 when ZFS is not present. + */ + mem_zfs_arc_gb?: number; + /** + * [2.3.0+] + * Total swap in GB. + */ + swap_total_gb?: number; + /** + * [2.3.0+] + * Used swap in GB. + */ + swap_used_gb?: number; /** Disk used in GB */ disk_used_gb: number; /** Total disk size in GB */ @@ -7596,6 +7959,15 @@ export interface GetHistoricalServerStatsResponse { /** If there is a next page of data, pass this to `page` to get it. */ next_page?: number; } +/** + * Get a specific image registry account. + * Response: [GetImageRegistryAccountResponse]. + * + * Pre v2.3.0, called `GetDockerRegistryAccount` + */ +export interface GetImageRegistryAccount { + id: string; +} /** * Get the Periphery information of the target server, * including the Periphery version and public key. @@ -7959,6 +8331,17 @@ export interface GlobalAutoUpdate { */ skip_auto_update?: boolean; } +/** + * Inspect a container on the server. Response: [Container]. + * + * Pre v2.3.0, called `InspectDockerContainer` + */ +export interface InspectContainer { + /** Id or name */ + server: string; + /** The container name */ + container: string; +} /** * Inspect the docker container associated with the Deployment. * Response: [Container]. @@ -7975,34 +8358,28 @@ export interface InspectDeploymentSwarmService { /** Id or name */ deployment: string; } -/** Inspect a docker container on the server. Response: [Container]. */ -export interface InspectDockerContainer { - /** Id or name */ - server: string; - /** The container name */ - container: string; -} -/** Inspect a docker image on the server. Response: [Image]. */ -export interface InspectDockerImage { +/** + * Inspect a container image on the server. Response: [Image]. + * + * Pre v2.3.0, called `InspectDockerImage` + */ +export interface InspectImage { /** Id or name */ server: string; /** The image name */ image: string; } -/** Inspect a docker network on the server. Response: [InspectDockerNetworkResponse]. */ -export interface InspectDockerNetwork { +/** + * Inspect a container network on the server. Response: [InspectNetworkResponse]. + * + * Pre v2.3.0, called `InspectDockerNetwork` + */ +export interface InspectNetwork { /** Id or name */ server: string; /** The network name */ network: string; } -/** Inspect a docker volume on the server. Response: [Volume]. */ -export interface InspectDockerVolume { - /** Id or name */ - server: string; - /** The volume name */ - volume: string; -} /** * Inspect a docker container associated with a Stack. * Response: [Container]. @@ -8099,19 +8476,94 @@ export interface InspectSwarmTask { /** Task id */ task: string; } +/** + * Inspect a container volume on the server. Response: [Volume]. + * + * Pre v2.3.0, called `InspectDockerVolume` + */ +export interface InspectVolume { + /** Id or name */ + server: string; + /** The volume name */ + volume: string; +} export interface LatestCommit { hash: string; message: string; } +export declare enum ActionSortBy { + /** Sort by name. Default. */ + Name = "Name", + /** Sort by state. */ + State = "State", + /** Sort by next scheduled run. */ + NextRun = "NextRun" +} /** List actions matching optional query. Response: [ListActionsResponse]. */ export interface ListActions { /** optional structured query to filter actions. */ query?: ActionQuery; + /** + * Retrieve more results by incrementing the page. + * `page: 0` is default. + */ + page?: U64; + /** + * Set the limit for number of resources per-page. + * If not provided, uses the Core config + * `default_pagination_limit` (default: 30). + * + * Passing `limit: 0` returns all results (unlimited). + * + * Note: the page logic relies on this being consistent + * across queries for more pages. + */ + limit?: U64; + /** + * Sort the results by this field. + * Defaults to Name. Non-Name sorts are applied in memory + * after querying all matching resources. + */ + sort_by?: ActionSortBy; + /** Reverse the sort direction. */ + sort_desc?: boolean; +} +export declare enum AlerterSortBy { + /** Sort by name. Default. */ + Name = "Name", + /** Sort by endpoint type. */ + Type = "Type", + /** Sort by enabled. */ + Enabled = "Enabled" } /** List alerters matching optional query. Response: [ListAlertersResponse]. */ export interface ListAlerters { /** Structured query to filter alerters. */ query?: AlerterQuery; + /** + * Retrieve more results by incrementing the page. + * `page: 0` is default. + */ + page?: U64; + /** + * Set the limit for number of resources per-page. + * If not provided, uses the Core config + * `default_pagination_limit` (default: 30). + * + * Passing `limit: 0` returns all results (unlimited). + * + * Note: the page logic relies on this being consistent + * across queries for more pages. + */ + limit?: U64; + /** + * Sort the results by this field. + * Defaults to Name. Non-Name sorts are applied in memory + * after querying all matching resources. + */ + sort_by?: AlerterSortBy; + /** Reverse the sort direction. */ + sort_desc?: boolean; } /** * Get a paginated list of alerts sorted by timestamp descending. @@ -8160,15 +8612,96 @@ export interface ListAlertsResponse { */ next_page?: I64; } -/** - * List all docker containers on the target servers. - * Response: [ListDockerContainersResponse]. +export declare enum ContainerSortBy { + /** Sort by container name. Default. */ + Name = "Name", + /** Sort by host Server name. */ + Server = "Server", + /** Sort by container state. */ + State = "State", + /** Sort by image. */ + Image = "Image", + /** Sort by first network. */ + Networks = "Networks", + /** Sort by first port. */ + Ports = "Ports", + /** Sort by first volume. */ + Volumes = "Volumes" +} +/** + * List all containers on the target servers. + * Response: [ListAllContainersResponse]. + * + * Pre v2.3.0, called `ListAllDockerContainers` */ -export interface ListAllDockerContainers { +export interface ListAllContainers { /** Filter by server id or name. */ servers?: string[]; - /** Filter by container name. */ - containers?: string[]; + /** Filter servers by tag. */ + tags?: string[]; + /** + * Filter by container name. + * Returned containers have names which contain all terms. + */ + terms?: string[]; + /** Filter by container state. */ + state?: ContainerStateStatusEnum[]; + /** + * Retrieve more results by incrementing the page. + * `page: 0` is default. + */ + page?: U64; + /** + * Set the limit for number of containers per-page. + * If not provided, uses the Core config + * `default_pagination_limit` (default: 30). + * + * Passing `limit: 0` returns all results (unlimited). + * + * Note: the page logic relies on this being consistent + * across queries for more pages. + */ + limit?: U64; + /** + * Sort the results by this field. + * Defaults to Name. + */ + sort_by?: ContainerSortBy; + /** Reverse the sort direction. */ + sort_desc?: boolean; +} +/** + * List all stack services part of the target stacks. + * Response: [ListStackServicesResponse]. + */ +export interface ListAllStackServices { + /** Filter by stack name. */ + stacks?: string[]; + /** Filter stacks by tag. */ + tags?: string[]; + /** + * Filter by service name. + * Returned services have names which contain all terms. + */ + terms?: string[]; + /** Filter by service state. */ + state?: StackServiceState[]; + /** + * Retrieve more results by incrementing the page. + * `page: 0` is default. + */ + page?: U64; + /** + * Set the limit for number of services per-page. + * If not provided, uses the Core config + * `default_pagination_limit` (default: 30). + * + * Passing `limit: 0` returns all results (unlimited). + * + * Note: the page logic relies on this being consistent + * across queries for more pages. + */ + limit?: U64; } /** * Gets list of api keys for the calling user. @@ -8203,14 +8736,78 @@ export interface ListBuildVersions { /** Limit the number of included results. Default is no limit. */ limit?: I64; } +export declare enum BuilderSortBy { + /** Sort by name. Default. */ + Name = "Name", + /** Sort by builder provider type. */ + Provider = "Provider", + /** Sort by instance type. */ + InstanceType = "InstanceType" +} /** List builders matching structured query. Response: [ListBuildersResponse]. */ export interface ListBuilders { query?: BuilderQuery; + /** + * Retrieve more results by incrementing the page. + * `page: 0` is default. + */ + page?: U64; + /** + * Set the limit for number of resources per-page. + * If not provided, uses the Core config + * `default_pagination_limit` (default: 30). + * + * Passing `limit: 0` returns all results (unlimited). + * + * Note: the page logic relies on this being consistent + * across queries for more pages. + */ + limit?: U64; + /** + * Sort the results by this field. + * Defaults to Name. Non-Name sorts are applied in memory + * after querying all matching resources. + */ + sort_by?: BuilderSortBy; + /** Reverse the sort direction. */ + sort_desc?: boolean; +} +export declare enum BuildSortBy { + /** Sort by name. Default. */ + Name = "Name", + /** Sort by source repo. */ + Source = "Source", + /** Sort by state. */ + State = "State" } /** List builds matching optional query. Response: [ListBuildsResponse]. */ export interface ListBuilds { /** optional structured query to filter builds. */ query?: BuildQuery; + /** + * Retrieve more results by incrementing the page. + * `page: 0` is default. + */ + page?: U64; + /** + * Set the limit for number of resources per-page. + * If not provided, uses the Core config + * `default_pagination_limit` (default: 30). + * + * Passing `limit: 0` returns all results (unlimited). + * + * Note: the page logic relies on this being consistent + * across queries for more pages. + */ + limit?: U64; + /** + * Sort the results by this field. + * Defaults to Name. Non-Name sorts are applied in memory + * after querying all matching resources. + */ + sort_by?: BuildSortBy; + /** Reverse the sort direction. */ + sort_desc?: boolean; } /** * Gets a list of existing values used as extra args across other builds. @@ -8245,7 +8842,7 @@ export interface ListCommonStackExtraArgs { query?: StackQuery; } /** - * List all docker compose projects on the target server. + * List all compose projects on the target server. * Response: [ListComposeProjectsResponse]. */ export interface ListComposeProjects { @@ -8253,93 +8850,139 @@ export interface ListComposeProjects { server: string; } /** - * List deployments matching optional query. - * Response: [ListDeploymentsResponse]. - */ -export interface ListDeployments { - /** optional structured query to filter deployments. */ - query?: DeploymentQuery; -} -/** - * List all docker containers on the target server. - * Response: [ListDockerContainersResponse]. - */ -export interface ListDockerContainers { - /** Id or name */ - server: string; -} -/** Get image history from the server. Response: [ListDockerImageHistoryResponse]. */ -export interface ListDockerImageHistory { - /** Id or name */ - server: string; - /** The image name */ - image: string; -} -/** - * List the docker images locally cached on the target server. - * Response: [ListDockerImagesResponse]. + * List all containers on the target server. + * Response: [ListContainersResponse]. + * + * Pre v2.3.0, called `ListDockerContainers` */ -export interface ListDockerImages { - /** Id or name */ - server: string; -} -/** List the docker networks on the server. Response: [ListDockerNetworksResponse]. */ -export interface ListDockerNetworks { +export interface ListContainers { /** Id or name */ server: string; } -/** - * List the docker registry providers available in Core / Periphery config files. - * Response: [ListDockerRegistriesFromConfigResponse]. - * - * Includes: - * - registries in core config - * - registries configured on builds, deployments - * - registries on the optional Server or Builder - */ -export interface ListDockerRegistriesFromConfig { - /** - * Accepts an optional Server or Builder target to expand the core list with - * providers available on that specific resource. - */ - target?: ResourceTarget; -} -/** - * List docker registry accounts matching optional query. - * Response: [ListDockerRegistryAccountsResponse]. - */ -export interface ListDockerRegistryAccounts { - /** Optionally filter by accounts with a specific domain. */ - domain?: string; - /** Optionally filter by accounts with a specific username. */ - username?: string; +export declare enum DeploymentSortBy { + /** Sort by name. Default. */ + Name = "Name", + /** Sort by image. */ + Image = "Image", + /** Sort by host Server / Swarm name. */ + Host = "Host", + /** Sort by state. */ + State = "State" } /** - * List all docker volumes on the target server. - * Response: [ListDockerVolumesResponse]. + * List deployments matching optional query. + * Response: [ListDeploymentsResponse]. */ -export interface ListDockerVolumes { - /** Id or name */ - server: string; +export interface ListDeployments { + /** optional structured query to filter deployments. */ + query?: DeploymentQuery; + /** + * Retrieve more results by incrementing the page. + * `page: 0` is default. + */ + page?: U64; + /** + * Set the limit for number of resources per-page. + * If not provided, uses the Core config + * `default_pagination_limit` (default: 30). + * + * Passing `limit: 0` returns all results (unlimited). + * + * Note: the page logic relies on this being consistent + * across queries for more pages. + */ + limit?: U64; + /** + * Sort the results by this field. + * Defaults to Name. Non-Name sorts are applied in memory + * after querying all matching resources. + */ + sort_by?: DeploymentSortBy; + /** Reverse the sort direction. */ + sort_desc?: boolean; } /** List actions matching optional query. Response: [ListFullActionsResponse]. */ export interface ListFullActions { /** optional structured query to filter actions. */ query?: ActionQuery; + /** + * Retrieve more results by incrementing the page. + * `page: 0` is default. + */ + page?: U64; + /** + * Set the limit for number of resources per-page. + * If not provided, uses the Core config + * `default_pagination_limit` (default: 30). + * + * Passing `limit: 0` returns all results (unlimited). + * + * Note: the page logic relies on this being consistent + * across queries for more pages. + */ + limit?: U64; } /** List full alerters matching optional query. Response: [ListFullAlertersResponse]. */ export interface ListFullAlerters { /** Structured query to filter alerters. */ query?: AlerterQuery; + /** + * Retrieve more results by incrementing the page. + * `page: 0` is default. + */ + page?: U64; + /** + * Set the limit for number of resources per-page. + * If not provided, uses the Core config + * `default_pagination_limit` (default: 30). + * + * Passing `limit: 0` returns all results (unlimited). + * + * Note: the page logic relies on this being consistent + * across queries for more pages. + */ + limit?: U64; } /** List builders matching structured query. Response: [ListFullBuildersResponse]. */ export interface ListFullBuilders { query?: BuilderQuery; + /** + * Retrieve more results by incrementing the page. + * `page: 0` is default. + */ + page?: U64; + /** + * Set the limit for number of resources per-page. + * If not provided, uses the Core config + * `default_pagination_limit` (default: 30). + * + * Passing `limit: 0` returns all results (unlimited). + * + * Note: the page logic relies on this being consistent + * across queries for more pages. + */ + limit?: U64; } /** List builds matching optional query. Response: [ListFullBuildsResponse]. */ export interface ListFullBuilds { /** optional structured query to filter builds. */ query?: BuildQuery; + /** + * Retrieve more results by incrementing the page. + * `page: 0` is default. + */ + page?: U64; + /** + * Set the limit for number of resources per-page. + * If not provided, uses the Core config + * `default_pagination_limit` (default: 30). + * + * Passing `limit: 0` returns all results (unlimited). + * + * Note: the page logic relies on this being consistent + * across queries for more pages. + */ + limit?: U64; } /** * List deployments matching optional query. @@ -8348,36 +8991,148 @@ export interface ListFullBuilds { export interface ListFullDeployments { /** optional structured query to filter deployments. */ query?: DeploymentQuery; + /** + * Retrieve more results by incrementing the page. + * `page: 0` is default. + */ + page?: U64; + /** + * Set the limit for number of resources per-page. + * If not provided, uses the Core config + * `default_pagination_limit` (default: 30). + * + * Passing `limit: 0` returns all results (unlimited). + * + * Note: the page logic relies on this being consistent + * across queries for more pages. + */ + limit?: U64; } /** List procedures matching optional query. Response: [ListFullProceduresResponse]. */ export interface ListFullProcedures { /** optional structured query to filter procedures. */ query?: ProcedureQuery; + /** + * Retrieve more results by incrementing the page. + * `page: 0` is default. + */ + page?: U64; + /** + * Set the limit for number of resources per-page. + * If not provided, uses the Core config + * `default_pagination_limit` (default: 30). + * + * Passing `limit: 0` returns all results (unlimited). + * + * Note: the page logic relies on this being consistent + * across queries for more pages. + */ + limit?: U64; } /** List repos matching optional query. Response: [ListFullReposResponse]. */ export interface ListFullRepos { /** optional structured query to filter repos. */ query?: RepoQuery; + /** + * Retrieve more results by incrementing the page. + * `page: 0` is default. + */ + page?: U64; + /** + * Set the limit for number of resources per-page. + * If not provided, uses the Core config + * `default_pagination_limit` (default: 30). + * + * Passing `limit: 0` returns all results (unlimited). + * + * Note: the page logic relies on this being consistent + * across queries for more pages. + */ + limit?: U64; } /** List syncs matching optional query. Response: [ListFullResourceSyncsResponse]. */ export interface ListFullResourceSyncs { /** optional structured query to filter syncs. */ query?: ResourceSyncQuery; + /** + * Retrieve more results by incrementing the page. + * `page: 0` is default. + */ + page?: U64; + /** + * Set the limit for number of resources per-page. + * If not provided, uses the Core config + * `default_pagination_limit` (default: 30). + * + * Passing `limit: 0` returns all results (unlimited). + * + * Note: the page logic relies on this being consistent + * across queries for more pages. + */ + limit?: U64; } /** List servers matching optional query. Response: [ListFullServersResponse]. */ export interface ListFullServers { /** optional structured query to filter servers. */ query?: ServerQuery; + /** + * Retrieve more results by incrementing the page. + * `page: 0` is default. + */ + page?: U64; + /** + * Set the limit for number of resources per-page. + * If not provided, uses the Core config + * `default_pagination_limit` (default: 30). + * + * Passing `limit: 0` returns all results (unlimited). + * + * Note: the page logic relies on this being consistent + * across queries for more pages. + */ + limit?: U64; } /** List stacks matching optional query. Response: [ListFullStacksResponse]. */ export interface ListFullStacks { /** optional structured query to filter stacks. */ query?: StackQuery; + /** + * Retrieve more results by incrementing the page. + * `page: 0` is default. + */ + page?: U64; + /** + * Set the limit for number of resources per-page. + * If not provided, uses the Core config + * `default_pagination_limit` (default: 30). + * + * Passing `limit: 0` returns all results (unlimited). + * + * Note: the page logic relies on this being consistent + * across queries for more pages. + */ + limit?: U64; } /** List Swarms matching optional query. Response: [ListFullSwarmsResponse]. */ export interface ListFullSwarms { /** optional structured query to filter swarms. */ query?: SwarmQuery; + /** + * Retrieve more results by incrementing the page. + * `page: 0` is default. + */ + page?: U64; + /** + * Set the limit for number of resources per-page. + * If not provided, uses the Core config + * `default_pagination_limit` (default: 30). + * + * Passing `limit: 0` returns all results (unlimited). + * + * Note: the page logic relies on this being consistent + * across queries for more pages. + */ + limit?: U64; } /** * List git provider accounts matching optional query. @@ -8405,6 +9160,66 @@ export interface ListGitProvidersFromConfig { */ target?: ResourceTarget; } +/** + * Get image history from the server. Response: [ListImageHistoryResponse]. + * + * Pre v2.3.0, called `ListDockerImageHistory` + */ +export interface ListImageHistory { + /** Id or name */ + server: string; + /** The image name */ + image: string; +} +/** + * List the image registry providers available in Core / Periphery config files. + * Response: [ListImageRegistriesFromConfigResponse]. + * + * Includes: + * - registries in core config + * - registries configured on builds, deployments + * - registries on the optional Server or Builder + * + * Pre v2.3.0, called `ListDockerRegistriesFromConfig` + */ +export interface ListImageRegistriesFromConfig { + /** + * Accepts an optional Server or Builder target to expand the core list with + * providers available on that specific resource. + */ + target?: ResourceTarget; +} +/** + * List image registry accounts matching optional query. + * Response: [ListImageRegistryAccountsResponse]. + * + * Pre v2.3.0, called `ListDockerRegistryAccounts` + */ +export interface ListImageRegistryAccounts { + /** Optionally filter by accounts with a specific domain. */ + domain?: string; + /** Optionally filter by accounts with a specific username. */ + username?: string; +} +/** + * List the container images locally cached on the target server. + * Response: [ListImagesResponse]. + * + * Pre v2.3.0, called `ListDockerImages` + */ +export interface ListImages { + /** Id or name */ + server: string; +} +/** + * List the container networks on the server. Response: [ListNetworksResponse]. + * + * Pre v2.3.0, called `ListDockerNetworks` + */ +export interface ListNetworks { + /** Id or name */ + server: string; +} /** * **Admin only.** Gets list of onboarding keys. * Response: [ListOnboardingKeysResponse] @@ -8418,20 +9233,130 @@ export interface ListOnboardingKeys { */ export interface ListPermissions { } +export declare enum ProcedureSortBy { + /** Sort by name. Default. */ + Name = "Name", + /** Sort by state. */ + State = "State", + /** Sort by next scheduled run. */ + NextRun = "NextRun" +} /** List procedures matching optional query. Response: [ListProceduresResponse]. */ export interface ListProcedures { /** optional structured query to filter procedures. */ query?: ProcedureQuery; + /** + * Retrieve more results by incrementing the page. + * `page: 0` is default. + */ + page?: U64; + /** + * Set the limit for number of resources per-page. + * If not provided, uses the Core config + * `default_pagination_limit` (default: 30). + * + * Passing `limit: 0` returns all results (unlimited). + * + * Note: the page logic relies on this being consistent + * across queries for more pages. + */ + limit?: U64; + /** + * Sort the results by this field. + * Defaults to Name. Non-Name sorts are applied in memory + * after querying all matching resources. + */ + sort_by?: ProcedureSortBy; + /** Reverse the sort direction. */ + sort_desc?: boolean; +} +export declare enum RepoSortBy { + /** Sort by name. Default. */ + Name = "Name", + /** Sort by the git repo. */ + Repo = "Repo", + /** Sort by branch. */ + Branch = "Branch", + /** Sort by state. */ + State = "State" } /** List repos matching optional query. Response: [ListReposResponse]. */ export interface ListRepos { /** optional structured query to filter repos. */ query?: RepoQuery; + /** + * Retrieve more results by incrementing the page. + * `page: 0` is default. + */ + page?: U64; + /** + * Set the limit for number of resources per-page. + * If not provided, uses the Core config + * `default_pagination_limit` (default: 30). + * + * Passing `limit: 0` returns all results (unlimited). + * + * Note: the page logic relies on this being consistent + * across queries for more pages. + */ + limit?: U64; + /** + * Sort the results by this field. + * Defaults to Name. Non-Name sorts are applied in memory + * after querying all matching resources. + */ + sort_by?: RepoSortBy; + /** Reverse the sort direction. */ + sort_desc?: boolean; +} +export declare enum ResourceSyncSortBy { + /** Sort by name. Default. */ + Name = "Name", + /** Sort by source repo. */ + Source = "Source", + /** Sort by branch. */ + Branch = "Branch", + /** Sort by state. */ + State = "State" } /** List syncs matching optional query. Response: [ListResourceSyncsResponse]. */ export interface ListResourceSyncs { /** optional structured query to filter syncs. */ query?: ResourceSyncQuery; + /** + * Retrieve more results by incrementing the page. + * `page: 0` is default. + */ + page?: U64; + /** + * Set the limit for number of resources per-page. + * If not provided, uses the Core config + * `default_pagination_limit` (default: 30). + * + * Passing `limit: 0` returns all results (unlimited). + * + * Note: the page logic relies on this being consistent + * across queries for more pages. + */ + limit?: U64; + /** + * Sort the results by this field. + * Defaults to Name. Non-Name sorts are applied in memory + * after querying all matching resources. + */ + sort_by?: ResourceSyncSortBy; + /** Reverse the sort direction. */ + sort_desc?: boolean; +} +export declare enum ScheduleSortBy { + /** Sort by target name. Default. */ + Name = "Name", + /** Sort by the schedule expression. */ + Schedule = "Schedule", + /** Sort by next scheduled run. */ + NextRun = "NextRun", + /** Sort by enabled. */ + Enabled = "Enabled" } /** * List configured schedules. @@ -8442,6 +9367,34 @@ export interface ListSchedules { tags?: string[]; /** 'All' or 'Any' */ tag_behavior?: TagQueryBehavior; + /** + * Filter by target name. + * Returned schedules have names which contain all terms. + */ + terms?: string[]; + /** + * Retrieve more results by incrementing the page. + * `page: 0` is default. + */ + page?: U64; + /** + * Set the limit for number of schedules per-page. + * If not provided, uses the Core config + * `default_pagination_limit` (default: 30). + * + * Passing `limit: 0` returns all results (unlimited). + * + * Note: the page logic relies on this being consistent + * across queries for more pages. + */ + limit?: U64; + /** + * Sort the results by this field. + * Defaults to Name. + */ + sort_by?: ScheduleSortBy; + /** Reverse the sort direction. */ + sort_desc?: boolean; } /** * List the secret keys (not values) in the core configuration file. @@ -8454,20 +9407,98 @@ export interface ListSecrets { */ target?: ResourceTarget; } +export declare enum ServerSortBy { + /** Sort by name. Default. */ + Name = "Name", + /** Sort by region. */ + Region = "Region", + /** Sort by periphery version. */ + Version = "Version", + /** Sort by state. */ + State = "State", + /** Sort by current cpu usage percentage. */ + Cpu = "Cpu", + /** Sort by current memory usage percentage. */ + Memory = "Memory", + /** Sort by current disk usage percentage. */ + Disk = "Disk", + /** Sort by current 1m load average. */ + LoadAverage = "LoadAverage", + /** Sort by current network usage (ingress + egress). */ + Network = "Network" +} /** List servers matching optional query. Response: [ListServersResponse]. */ export interface ListServers { /** optional structured query to filter servers. */ query?: ServerQuery; + /** + * Retrieve more results by incrementing the page. + * `page: 0` is default. + */ + page?: U64; + /** + * Set the limit for number of resources per-page. + * If not provided, uses the Core config + * `default_pagination_limit` (default: 30). + * + * Passing `limit: 0` returns all results (unlimited). + * + * Note: the page logic relies on this being consistent + * across queries for more pages. + */ + limit?: U64; + /** + * Sort the results by this field. + * Defaults to Name. Non-Name sorts are applied in memory + * after querying all matching resources. + */ + sort_by?: ServerSortBy; + /** Reverse the sort direction. */ + sort_desc?: boolean; } /** Lists a specific stacks services (the containers). Response: [ListStackServicesResponse]. */ export interface ListStackServices { /** Id or name */ stack: string; } +export declare enum StackSortBy { + /** Sort by name. Default. */ + Name = "Name", + /** Sort by source repo. */ + Source = "Source", + /** Sort by host Server / Swarm name. */ + Host = "Host", + /** Sort by state. */ + State = "State" +} /** List stacks matching optional query. Response: [ListStacksResponse]. */ export interface ListStacks { /** optional structured query to filter stacks. */ query?: StackQuery; + /** + * Retrieve more results by incrementing the page. + * `page: 0` is default. + */ + page?: U64; + /** + * Set the limit for number of resources per-page. + * If not provided, uses the Core config + * `default_pagination_limit` (default: 30). + * + * Passing `limit: 0` returns all results (unlimited). + * + * Note: the page logic relies on this being consistent + * across queries for more pages. + */ + limit?: U64; + /** + * Sort the results by this field. + * Defaults to Name. Non-Name sorts are applied in memory + * after querying all matching resources. + */ + sort_by?: StackSortBy; + /** Reverse the sort direction. */ + sort_desc?: boolean; } /** * List configs on the target Swarm. @@ -8527,10 +9558,40 @@ export interface ListSwarmTasks { /** Id or name */ swarm: string; } +export declare enum SwarmSortBy { + /** Sort by name. Default. */ + Name = "Name", + /** Sort by state. */ + State = "State" +} /** List Swarms matching optional query. Response: [ListSwarmsResponse]. */ export interface ListSwarms { /** Optional structured query to filter Swarms. */ query?: SwarmQuery; + /** + * Retrieve more results by incrementing the page. + * `page: 0` is default. + */ + page?: U64; + /** + * Set the limit for number of resources per-page. + * If not provided, uses the Core config + * `default_pagination_limit` (default: 30). + * + * Passing `limit: 0` returns all results (unlimited). + * + * Note: the page logic relies on this being consistent + * across queries for more pages. + */ + limit?: U64; + /** + * Sort the results by this field. + * Defaults to Name. Non-Name sorts are applied in memory + * after querying all matching resources. + */ + sort_by?: SwarmSortBy; + /** Reverse the sort direction. */ + sort_desc?: boolean; } /** * List the processes running on the target server. @@ -8551,6 +9612,18 @@ export interface ListSystemProcesses { export interface ListTags { query?: MongoDocument; } +export declare enum TerminalSortBy { + /** Sort by name. Default. */ + Name = "Name", + /** Sort by target. */ + Target = "Target", + /** Sort by init command. */ + Command = "Command", + /** Sort by stored size. */ + Size = "Size", + /** Sort by created timestamp. */ + Created = "Created" +} /** * List Terminals. * Response: [ListTerminalsResponse]. @@ -8560,6 +9633,34 @@ export interface ListTerminals { target?: TerminalTarget; /** Return results with resource names instead of ids. */ use_names?: boolean; + /** + * Filter by terminal name. + * Returned terminals have names which contain all terms. + */ + terms?: string[]; + /** + * Retrieve more results by incrementing the page. + * `page: 0` is default. + */ + page?: U64; + /** + * Set the limit for number of terminals per-page. + * If not provided, uses the Core config + * `default_pagination_limit` (default: 30). + * + * Passing `limit: 0` returns all results (unlimited). + * + * Note: the page logic relies on this being consistent + * across queries for more pages. + */ + limit?: U64; + /** + * Sort the results by this field. + * Defaults to Name. + */ + sort_by?: TerminalSortBy; + /** Reverse the sort direction. */ + sort_desc?: boolean; } /** * Paginated endpoint for updates matching optional query. @@ -8663,6 +9764,16 @@ export interface ListUsers { */ export interface ListVariables { } +/** + * List all container volumes on the target server. + * Response: [ListVolumesResponse]. + * + * Pre v2.3.0, called `ListDockerVolumes` + */ +export interface ListVolumes { + /** Id or name */ + server: string; +} export interface NameAndId { name: string; id: string; @@ -9400,8 +10511,12 @@ export interface SendAlert { } /** Configuration for a Komodo Server Builder. */ export interface ServerBuilderConfig { - /** The server id of the builder */ - server_id?: string; + /** + * The server ids of the builders. + * If multiple are given, builds will overflow + * to later specified servers as needed. + */ + server_ids?: string[]; } /** The health of a part of the server. */ export interface ServerHealthState { @@ -9709,16 +10824,6 @@ export interface UpdateDeployment { /** The partial config update. */ config: _PartialDeploymentConfig; } -/** - * **Admin only.** Update a docker registry account. - * Response: [DockerRegistryAccount]. - */ -export interface UpdateDockerRegistryAccount { - /** The id of the docker registry to update */ - id: string; - /** The partial docker registry account. */ - account: _PartialDockerRegistryAccount; -} /** * **Admin only.** Update a git provider account. * Response: [GitProviderAccount]. @@ -9729,6 +10834,18 @@ export interface UpdateGitProviderAccount { /** The partial git provider account. */ account: _PartialGitProviderAccount; } +/** + * **Admin only.** Update a image registry account. + * Response: [ImageRegistryAccount]. + * + * Pre v2.3.0, called `UpdateDockerRegistryAccount` + */ +export interface UpdateImageRegistryAccount { + /** The id of the image registry to update */ + id: string; + /** The partial image registry account. */ + account: _PartialImageRegistryAccount; +} /** * **Admin only.** Update an onboarding key. * Response: The updated [OnboardingKey]. @@ -10173,12 +11290,18 @@ export type ExecuteRequest = { } | { type: "BatchRunProcedure"; params: BatchRunProcedure; +} | { + type: "CancelProcedure"; + params: CancelProcedure; } | { type: "RunAction"; params: RunAction; } | { type: "BatchRunAction"; params: BatchRunAction; +} | { + type: "CancelAction"; + params: CancelAction; } | { type: "RunSync"; params: RunSync; @@ -10395,8 +11518,8 @@ export type ReadRequest = { type: "ListGitProvidersFromConfig"; params: ListGitProvidersFromConfig; } | { - type: "ListDockerRegistriesFromConfig"; - params: ListDockerRegistriesFromConfig; + type: "ListImageRegistriesFromConfig"; + params: ListImageRegistriesFromConfig; } | { type: "GetSwarmsSummary"; params: GetSwarmsSummary; @@ -10485,17 +11608,17 @@ export type ReadRequest = { type: "ListTerminals"; params: ListTerminals; } | { - type: "GetDockerContainersSummary"; - params: GetDockerContainersSummary; + type: "GetContainersSummary"; + params: GetContainersSummary; } | { - type: "ListAllDockerContainers"; - params: ListAllDockerContainers; + type: "ListAllContainers"; + params: ListAllContainers; } | { - type: "ListDockerContainers"; - params: ListDockerContainers; + type: "ListContainers"; + params: ListContainers; } | { - type: "InspectDockerContainer"; - params: InspectDockerContainer; + type: "InspectContainer"; + params: InspectContainer; } | { type: "GetResourceMatchingContainer"; params: GetResourceMatchingContainer; @@ -10509,26 +11632,26 @@ export type ReadRequest = { type: "ListComposeProjects"; params: ListComposeProjects; } | { - type: "ListDockerNetworks"; - params: ListDockerNetworks; + type: "ListNetworks"; + params: ListNetworks; } | { - type: "InspectDockerNetwork"; - params: InspectDockerNetwork; + type: "InspectNetwork"; + params: InspectNetwork; } | { - type: "ListDockerImages"; - params: ListDockerImages; + type: "ListImages"; + params: ListImages; } | { - type: "InspectDockerImage"; - params: InspectDockerImage; + type: "InspectImage"; + params: InspectImage; } | { - type: "ListDockerImageHistory"; - params: ListDockerImageHistory; + type: "ListImageHistory"; + params: ListImageHistory; } | { - type: "ListDockerVolumes"; - params: ListDockerVolumes; + type: "ListVolumes"; + params: ListVolumes; } | { - type: "InspectDockerVolume"; - params: InspectDockerVolume; + type: "InspectVolume"; + params: InspectVolume; } | { type: "GetSystemInformation"; params: GetSystemInformation; @@ -10571,6 +11694,9 @@ export type ReadRequest = { } | { type: "ListStackServices"; params: ListStackServices; +} | { + type: "ListAllStackServices"; + params: ListAllStackServices; } | { type: "ListCommonStackExtraArgs"; params: ListCommonStackExtraArgs; @@ -10791,11 +11917,11 @@ export type ReadRequest = { type: "ListGitProviderAccounts"; params: ListGitProviderAccounts; } | { - type: "GetDockerRegistryAccount"; - params: GetDockerRegistryAccount; + type: "GetImageRegistryAccount"; + params: GetImageRegistryAccount; } | { - type: "ListDockerRegistryAccounts"; - params: ListDockerRegistryAccounts; + type: "ListImageRegistryAccounts"; + params: ListImageRegistryAccounts; } | { type: "ListOnboardingKeys"; params: ListOnboardingKeys; @@ -11182,14 +12308,14 @@ export type WriteRequest = { type: "DeleteGitProviderAccount"; params: DeleteGitProviderAccount; } | { - type: "CreateDockerRegistryAccount"; - params: CreateDockerRegistryAccount; + type: "CreateImageRegistryAccount"; + params: CreateImageRegistryAccount; } | { - type: "UpdateDockerRegistryAccount"; - params: UpdateDockerRegistryAccount; + type: "UpdateImageRegistryAccount"; + params: UpdateImageRegistryAccount; } | { - type: "DeleteDockerRegistryAccount"; - params: DeleteDockerRegistryAccount; + type: "DeleteImageRegistryAccount"; + params: DeleteImageRegistryAccount; } | { type: "CloseAlert"; params: CloseAlert; diff --git a/ui/public/client/types.js b/ui/public/client/types.js index d48eebac66..4743bae09f 100644 --- a/ui/public/client/types.js +++ b/ui/public/client/types.js @@ -27,15 +27,22 @@ export var FileFormat; })(FileFormat || (FileFormat = {})); export var ActionState; (function (ActionState) { - /** Unknown case */ - ActionState["Unknown"] = "Unknown"; + /** Currently running */ + ActionState["Running"] = "Running"; /** Last clone / pull successful (or never cloned) */ ActionState["Ok"] = "Ok"; /** Last clone / pull failed */ ActionState["Failed"] = "Failed"; - /** Currently running */ - ActionState["Running"] = "Running"; + /** Unknown case */ + ActionState["Unknown"] = "Unknown"; })(ActionState || (ActionState = {})); +export var TagQueryBehavior; +(function (TagQueryBehavior) { + /** Returns resources which have strictly all the tags */ + TagQueryBehavior["All"] = "All"; + /** Returns resources which have one or more of the tags */ + TagQueryBehavior["Any"] = "Any"; +})(TagQueryBehavior || (TagQueryBehavior = {})); export var TemplatesQueryBehavior; (function (TemplatesQueryBehavior) { /** Include templates in results. Default. */ @@ -45,13 +52,6 @@ export var TemplatesQueryBehavior; /** Results *only* includes templates. */ TemplatesQueryBehavior["Only"] = "Only"; })(TemplatesQueryBehavior || (TemplatesQueryBehavior = {})); -export var TagQueryBehavior; -(function (TagQueryBehavior) { - /** Returns resources which have strictly all the tags */ - TagQueryBehavior["All"] = "All"; - /** Returns resources which have one or more of the tags */ - TagQueryBehavior["Any"] = "Any"; -})(TagQueryBehavior || (TagQueryBehavior = {})); /** Types of maintenance schedules */ export var MaintenanceScheduleType; (function (MaintenanceScheduleType) { @@ -163,11 +163,13 @@ export var Operation; Operation["RenameProcedure"] = "RenameProcedure"; Operation["DeleteProcedure"] = "DeleteProcedure"; Operation["RunProcedure"] = "RunProcedure"; + Operation["CancelProcedure"] = "CancelProcedure"; Operation["CreateAction"] = "CreateAction"; Operation["UpdateAction"] = "UpdateAction"; Operation["RenameAction"] = "RenameAction"; Operation["DeleteAction"] = "DeleteAction"; Operation["RunAction"] = "RunAction"; + Operation["CancelAction"] = "CancelAction"; Operation["CreateResourceSync"] = "CreateResourceSync"; Operation["UpdateResourceSync"] = "UpdateResourceSync"; Operation["RenameResourceSync"] = "RenameResourceSync"; @@ -417,11 +419,11 @@ export var ContainerStateStatusEnum; (function (ContainerStateStatusEnum) { ContainerStateStatusEnum["Running"] = "running"; ContainerStateStatusEnum["Created"] = "created"; - ContainerStateStatusEnum["Paused"] = "paused"; ContainerStateStatusEnum["Restarting"] = "restarting"; - ContainerStateStatusEnum["Exited"] = "exited"; ContainerStateStatusEnum["Stopping"] = "stopping"; ContainerStateStatusEnum["Removing"] = "removing"; + ContainerStateStatusEnum["Paused"] = "paused"; + ContainerStateStatusEnum["Exited"] = "exited"; ContainerStateStatusEnum["Dead"] = "dead"; ContainerStateStatusEnum["Empty"] = ""; })(ContainerStateStatusEnum || (ContainerStateStatusEnum = {})); @@ -598,41 +600,6 @@ export var ImageManifestSummaryKindEnum; ImageManifestSummaryKindEnum["Attestation"] = "attestation"; ImageManifestSummaryKindEnum["Unknown"] = "unknown"; })(ImageManifestSummaryKindEnum || (ImageManifestSummaryKindEnum = {})); -export var VolumeScopeEnum; -(function (VolumeScopeEnum) { - VolumeScopeEnum["Empty"] = ""; - VolumeScopeEnum["Local"] = "local"; - VolumeScopeEnum["Global"] = "global"; -})(VolumeScopeEnum || (VolumeScopeEnum = {})); -export var ClusterVolumeSpecAccessModeScopeEnum; -(function (ClusterVolumeSpecAccessModeScopeEnum) { - ClusterVolumeSpecAccessModeScopeEnum["Empty"] = ""; - ClusterVolumeSpecAccessModeScopeEnum["Single"] = "single"; - ClusterVolumeSpecAccessModeScopeEnum["Multi"] = "multi"; -})(ClusterVolumeSpecAccessModeScopeEnum || (ClusterVolumeSpecAccessModeScopeEnum = {})); -export var ClusterVolumeSpecAccessModeSharingEnum; -(function (ClusterVolumeSpecAccessModeSharingEnum) { - ClusterVolumeSpecAccessModeSharingEnum["Empty"] = ""; - ClusterVolumeSpecAccessModeSharingEnum["None"] = "none"; - ClusterVolumeSpecAccessModeSharingEnum["Readonly"] = "readonly"; - ClusterVolumeSpecAccessModeSharingEnum["Onewriter"] = "onewriter"; - ClusterVolumeSpecAccessModeSharingEnum["All"] = "all"; -})(ClusterVolumeSpecAccessModeSharingEnum || (ClusterVolumeSpecAccessModeSharingEnum = {})); -export var ClusterVolumeSpecAccessModeAvailabilityEnum; -(function (ClusterVolumeSpecAccessModeAvailabilityEnum) { - ClusterVolumeSpecAccessModeAvailabilityEnum["Empty"] = ""; - ClusterVolumeSpecAccessModeAvailabilityEnum["Active"] = "active"; - ClusterVolumeSpecAccessModeAvailabilityEnum["Pause"] = "pause"; - ClusterVolumeSpecAccessModeAvailabilityEnum["Drain"] = "drain"; -})(ClusterVolumeSpecAccessModeAvailabilityEnum || (ClusterVolumeSpecAccessModeAvailabilityEnum = {})); -export var ClusterVolumePublishStatusStateEnum; -(function (ClusterVolumePublishStatusStateEnum) { - ClusterVolumePublishStatusStateEnum["Empty"] = ""; - ClusterVolumePublishStatusStateEnum["PendingPublish"] = "pending-publish"; - ClusterVolumePublishStatusStateEnum["Published"] = "published"; - ClusterVolumePublishStatusStateEnum["PendingNodeUnpublish"] = "pending-node-unpublish"; - ClusterVolumePublishStatusStateEnum["PendingControllerUnpublish"] = "pending-controller-unpublish"; -})(ClusterVolumePublishStatusStateEnum || (ClusterVolumePublishStatusStateEnum = {})); export var TaskState; (function (TaskState) { TaskState["NEW"] = "new"; @@ -684,6 +651,41 @@ export var SwarmSpecCaConfigExternalCasProtocolEnum; SwarmSpecCaConfigExternalCasProtocolEnum["EMPTY"] = ""; SwarmSpecCaConfigExternalCasProtocolEnum["CFSSL"] = "cfssl"; })(SwarmSpecCaConfigExternalCasProtocolEnum || (SwarmSpecCaConfigExternalCasProtocolEnum = {})); +export var VolumeScopeEnum; +(function (VolumeScopeEnum) { + VolumeScopeEnum["Empty"] = ""; + VolumeScopeEnum["Local"] = "local"; + VolumeScopeEnum["Global"] = "global"; +})(VolumeScopeEnum || (VolumeScopeEnum = {})); +export var ClusterVolumeSpecAccessModeScopeEnum; +(function (ClusterVolumeSpecAccessModeScopeEnum) { + ClusterVolumeSpecAccessModeScopeEnum["Empty"] = ""; + ClusterVolumeSpecAccessModeScopeEnum["Single"] = "single"; + ClusterVolumeSpecAccessModeScopeEnum["Multi"] = "multi"; +})(ClusterVolumeSpecAccessModeScopeEnum || (ClusterVolumeSpecAccessModeScopeEnum = {})); +export var ClusterVolumeSpecAccessModeSharingEnum; +(function (ClusterVolumeSpecAccessModeSharingEnum) { + ClusterVolumeSpecAccessModeSharingEnum["Empty"] = ""; + ClusterVolumeSpecAccessModeSharingEnum["None"] = "none"; + ClusterVolumeSpecAccessModeSharingEnum["Readonly"] = "readonly"; + ClusterVolumeSpecAccessModeSharingEnum["Onewriter"] = "onewriter"; + ClusterVolumeSpecAccessModeSharingEnum["All"] = "all"; +})(ClusterVolumeSpecAccessModeSharingEnum || (ClusterVolumeSpecAccessModeSharingEnum = {})); +export var ClusterVolumeSpecAccessModeAvailabilityEnum; +(function (ClusterVolumeSpecAccessModeAvailabilityEnum) { + ClusterVolumeSpecAccessModeAvailabilityEnum["Empty"] = ""; + ClusterVolumeSpecAccessModeAvailabilityEnum["Active"] = "active"; + ClusterVolumeSpecAccessModeAvailabilityEnum["Pause"] = "pause"; + ClusterVolumeSpecAccessModeAvailabilityEnum["Drain"] = "drain"; +})(ClusterVolumeSpecAccessModeAvailabilityEnum || (ClusterVolumeSpecAccessModeAvailabilityEnum = {})); +export var ClusterVolumePublishStatusStateEnum; +(function (ClusterVolumePublishStatusStateEnum) { + ClusterVolumePublishStatusStateEnum["Empty"] = ""; + ClusterVolumePublishStatusStateEnum["PendingPublish"] = "pending-publish"; + ClusterVolumePublishStatusStateEnum["Published"] = "published"; + ClusterVolumePublishStatusStateEnum["PendingNodeUnpublish"] = "pending-node-unpublish"; + ClusterVolumePublishStatusStateEnum["PendingControllerUnpublish"] = "pending-controller-unpublish"; +})(ClusterVolumePublishStatusStateEnum || (ClusterVolumePublishStatusStateEnum = {})); export var PortTypeEnum; (function (PortTypeEnum) { PortTypeEnum["EMPTY"] = ""; @@ -691,6 +693,37 @@ export var PortTypeEnum; PortTypeEnum["UDP"] = "udp"; PortTypeEnum["SCTP"] = "sctp"; })(PortTypeEnum || (PortTypeEnum = {})); +/** + * Combined state options for + * both Server and Swarm based Stacks. + */ +export var StackServiceState; +(function (StackServiceState) { + /** (Swarm) All tasks OK */ + StackServiceState["Healthy"] = "Healthy"; + /** (Swarm) Some tasks don't match desired state */ + StackServiceState["Unhealthy"] = "Unhealthy"; + /** (Swarm) All tasks down. */ + StackServiceState["Down"] = "Down"; + /** (Container) Container is running */ + StackServiceState["Running"] = "Running"; + /** (Container) Container is created */ + StackServiceState["Created"] = "Created"; + /** (Container) Container is paused */ + StackServiceState["Paused"] = "Paused"; + /** (Container) Container is restarting */ + StackServiceState["Restarting"] = "Restarting"; + /** (Container) Container is exited */ + StackServiceState["Exited"] = "Exited"; + /** (Container) Container is stopping */ + StackServiceState["Stopping"] = "Stopping"; + /** (Container) Container is removing */ + StackServiceState["Removing"] = "Removing"; + /** (Container) Container is dead */ + StackServiceState["Dead"] = "Dead"; + /** Unknown case */ + StackServiceState["Unknown"] = "Unknown"; +})(StackServiceState || (StackServiceState = {})); export var ProcedureState; (function (ProcedureState) { /** Currently running */ @@ -704,18 +737,18 @@ export var ProcedureState; })(ProcedureState || (ProcedureState = {})); export var RepoState; (function (RepoState) { - /** Unknown case */ - RepoState["Unknown"] = "Unknown"; - /** Last clone / pull successful (or never cloned) */ - RepoState["Ok"] = "Ok"; - /** Last clone / pull failed */ - RepoState["Failed"] = "Failed"; /** Currently cloning */ RepoState["Cloning"] = "Cloning"; /** Currently pulling */ RepoState["Pulling"] = "Pulling"; /** Currently building */ RepoState["Building"] = "Building"; + /** Last clone / pull successful (or never cloned) */ + RepoState["Ok"] = "Ok"; + /** Last clone / pull failed */ + RepoState["Failed"] = "Failed"; + /** Unknown case */ + RepoState["Unknown"] = "Unknown"; })(RepoState || (RepoState = {})); export var ResourceSyncState; (function (ResourceSyncState) { @@ -786,6 +819,164 @@ export var ContainerTerminalMode; ContainerTerminalMode["Exec"] = "exec"; ContainerTerminalMode["Attach"] = "attach"; })(ContainerTerminalMode || (ContainerTerminalMode = {})); +export var ActionSortBy; +(function (ActionSortBy) { + /** Sort by name. Default. */ + ActionSortBy["Name"] = "Name"; + /** Sort by state. */ + ActionSortBy["State"] = "State"; + /** Sort by next scheduled run. */ + ActionSortBy["NextRun"] = "NextRun"; +})(ActionSortBy || (ActionSortBy = {})); +export var AlerterSortBy; +(function (AlerterSortBy) { + /** Sort by name. Default. */ + AlerterSortBy["Name"] = "Name"; + /** Sort by endpoint type. */ + AlerterSortBy["Type"] = "Type"; + /** Sort by enabled. */ + AlerterSortBy["Enabled"] = "Enabled"; +})(AlerterSortBy || (AlerterSortBy = {})); +export var ContainerSortBy; +(function (ContainerSortBy) { + /** Sort by container name. Default. */ + ContainerSortBy["Name"] = "Name"; + /** Sort by host Server name. */ + ContainerSortBy["Server"] = "Server"; + /** Sort by container state. */ + ContainerSortBy["State"] = "State"; + /** Sort by image. */ + ContainerSortBy["Image"] = "Image"; + /** Sort by first network. */ + ContainerSortBy["Networks"] = "Networks"; + /** Sort by first port. */ + ContainerSortBy["Ports"] = "Ports"; + /** Sort by first volume. */ + ContainerSortBy["Volumes"] = "Volumes"; +})(ContainerSortBy || (ContainerSortBy = {})); +export var BuilderSortBy; +(function (BuilderSortBy) { + /** Sort by name. Default. */ + BuilderSortBy["Name"] = "Name"; + /** Sort by builder provider type. */ + BuilderSortBy["Provider"] = "Provider"; + /** Sort by instance type. */ + BuilderSortBy["InstanceType"] = "InstanceType"; +})(BuilderSortBy || (BuilderSortBy = {})); +export var BuildSortBy; +(function (BuildSortBy) { + /** Sort by name. Default. */ + BuildSortBy["Name"] = "Name"; + /** Sort by source repo. */ + BuildSortBy["Source"] = "Source"; + /** Sort by state. */ + BuildSortBy["State"] = "State"; +})(BuildSortBy || (BuildSortBy = {})); +export var DeploymentSortBy; +(function (DeploymentSortBy) { + /** Sort by name. Default. */ + DeploymentSortBy["Name"] = "Name"; + /** Sort by image. */ + DeploymentSortBy["Image"] = "Image"; + /** Sort by host Server / Swarm name. */ + DeploymentSortBy["Host"] = "Host"; + /** Sort by state. */ + DeploymentSortBy["State"] = "State"; +})(DeploymentSortBy || (DeploymentSortBy = {})); +export var ProcedureSortBy; +(function (ProcedureSortBy) { + /** Sort by name. Default. */ + ProcedureSortBy["Name"] = "Name"; + /** Sort by state. */ + ProcedureSortBy["State"] = "State"; + /** Sort by next scheduled run. */ + ProcedureSortBy["NextRun"] = "NextRun"; +})(ProcedureSortBy || (ProcedureSortBy = {})); +export var RepoSortBy; +(function (RepoSortBy) { + /** Sort by name. Default. */ + RepoSortBy["Name"] = "Name"; + /** Sort by the git repo. */ + RepoSortBy["Repo"] = "Repo"; + /** Sort by branch. */ + RepoSortBy["Branch"] = "Branch"; + /** Sort by state. */ + RepoSortBy["State"] = "State"; +})(RepoSortBy || (RepoSortBy = {})); +export var ResourceSyncSortBy; +(function (ResourceSyncSortBy) { + /** Sort by name. Default. */ + ResourceSyncSortBy["Name"] = "Name"; + /** Sort by source repo. */ + ResourceSyncSortBy["Source"] = "Source"; + /** Sort by branch. */ + ResourceSyncSortBy["Branch"] = "Branch"; + /** Sort by state. */ + ResourceSyncSortBy["State"] = "State"; +})(ResourceSyncSortBy || (ResourceSyncSortBy = {})); +export var ScheduleSortBy; +(function (ScheduleSortBy) { + /** Sort by target name. Default. */ + ScheduleSortBy["Name"] = "Name"; + /** Sort by the schedule expression. */ + ScheduleSortBy["Schedule"] = "Schedule"; + /** Sort by next scheduled run. */ + ScheduleSortBy["NextRun"] = "NextRun"; + /** Sort by enabled. */ + ScheduleSortBy["Enabled"] = "Enabled"; +})(ScheduleSortBy || (ScheduleSortBy = {})); +export var ServerSortBy; +(function (ServerSortBy) { + /** Sort by name. Default. */ + ServerSortBy["Name"] = "Name"; + /** Sort by region. */ + ServerSortBy["Region"] = "Region"; + /** Sort by periphery version. */ + ServerSortBy["Version"] = "Version"; + /** Sort by state. */ + ServerSortBy["State"] = "State"; + /** Sort by current cpu usage percentage. */ + ServerSortBy["Cpu"] = "Cpu"; + /** Sort by current memory usage percentage. */ + ServerSortBy["Memory"] = "Memory"; + /** Sort by current disk usage percentage. */ + ServerSortBy["Disk"] = "Disk"; + /** Sort by current 1m load average. */ + ServerSortBy["LoadAverage"] = "LoadAverage"; + /** Sort by current network usage (ingress + egress). */ + ServerSortBy["Network"] = "Network"; +})(ServerSortBy || (ServerSortBy = {})); +export var StackSortBy; +(function (StackSortBy) { + /** Sort by name. Default. */ + StackSortBy["Name"] = "Name"; + /** Sort by source repo. */ + StackSortBy["Source"] = "Source"; + /** Sort by host Server / Swarm name. */ + StackSortBy["Host"] = "Host"; + /** Sort by state. */ + StackSortBy["State"] = "State"; +})(StackSortBy || (StackSortBy = {})); +export var SwarmSortBy; +(function (SwarmSortBy) { + /** Sort by name. Default. */ + SwarmSortBy["Name"] = "Name"; + /** Sort by state. */ + SwarmSortBy["State"] = "State"; +})(SwarmSortBy || (SwarmSortBy = {})); +export var TerminalSortBy; +(function (TerminalSortBy) { + /** Sort by name. Default. */ + TerminalSortBy["Name"] = "Name"; + /** Sort by target. */ + TerminalSortBy["Target"] = "Target"; + /** Sort by init command. */ + TerminalSortBy["Command"] = "Command"; + /** Sort by stored size. */ + TerminalSortBy["Size"] = "Size"; + /** Sort by created timestamp. */ + TerminalSortBy["Created"] = "Created"; +})(TerminalSortBy || (TerminalSortBy = {})); export var ServiceUserQueryBehavior; (function (ServiceUserQueryBehavior) { /** Include service users in results. Default. */ diff --git a/ui/public/favicon-96x96.png b/ui/public/favicon-96x96.png deleted file mode 100644 index f248e8eff4..0000000000 Binary files a/ui/public/favicon-96x96.png and /dev/null differ diff --git a/ui/public/favicon.ico b/ui/public/favicon.ico index 1c900b85f3..ee480c56b4 100644 Binary files a/ui/public/favicon.ico and b/ui/public/favicon.ico differ diff --git a/ui/public/favicon.svg b/ui/public/favicon.svg index ed4edc02de..5ae0c11c7a 100644 --- a/ui/public/favicon.svg +++ b/ui/public/favicon.svg @@ -1,71 +1,14 @@ - - - - - - - - \ No newline at end of file +RealFaviconGeneratorhttps://realfavicongenerator.net + Layer 1 + + + + + + + + + + + \ No newline at end of file diff --git a/ui/public/schema/resources.json b/ui/public/schema/resources.json index c3a8609551..81a6d006c6 100644 --- a/ui/public/schema/resources.json +++ b/ui/public/schema/resources.json @@ -617,6 +617,16 @@ "null" ] }, + "auto_update_skip_services": { + "description": "Ignore certain services during Global Auto Update polling.\nServices listed here are skipped only in the global auto-update flow.\nManual checks still include all services.", + "type": [ + "array", + "null" + ], + "items": { + "type": "string" + } + }, "destroy_before_deploy": { "description": "Whether to run `docker compose down` before `compose up`.", "type": [ @@ -2537,6 +2547,22 @@ "params" ] }, + { + "type": "object", + "properties": { + "type": { + "type": "string", + "const": "CancelProcedure" + }, + "params": { + "$ref": "#/definitions/CancelProcedure" + } + }, + "required": [ + "type", + "params" + ] + }, { "description": "Run the target action. (alias: `action`, `ac`)", "type": "object", @@ -2570,6 +2596,22 @@ "params" ] }, + { + "type": "object", + "properties": { + "type": { + "type": "string", + "const": "CancelAction" + }, + "params": { + "$ref": "#/definitions/CancelAction" + } + }, + "required": [ + "type", + "params" + ] + }, { "description": "Execute a Resource Sync. (alias: `sync`)", "type": "object", @@ -3941,6 +3983,26 @@ "pattern" ] }, + "CancelProcedure": { + "description": "Cancels the target procedure run. Response: [Update]", + "type": "object", + "properties": { + "procedure": { + "description": "Id or name", + "type": "string" + }, + "update_id": { + "description": "The update id associated with the specific\nrun to cancel\nMust provide either `action`\nor `update_id`", + "type": [ + "string", + "null" + ] + } + }, + "required": [ + "procedure" + ] + }, "RunAction": { "description": "Runs the target Action. Response: [Update]", "type": "object", @@ -3975,6 +4037,26 @@ "pattern" ] }, + "CancelAction": { + "description": "Cancels the target action run. Response: [Update]", + "type": "object", + "properties": { + "action": { + "description": "Id or name", + "type": "string" + }, + "update_id": { + "description": "The update id associated with the specific\nrun to cancel\nMust provide either `action`\nor `update_id`", + "type": [ + "string", + "null" + ] + } + }, + "required": [ + "action" + ] + }, "RunSync": { "description": "Runs the target resource sync. Response: [Update]", "type": "object", @@ -5703,12 +5785,15 @@ "PartialServerBuilderConfig": { "type": "object", "properties": { - "server": { - "description": "The server id of the builder", + "servers": { + "description": "The server ids of the builders.\nIf multiple are given, builds will overflow\nto later specified servers as needed.", "type": [ - "string", + "array", "null" - ] + ], + "items": { + "type": "string" + } } } }, @@ -5827,14 +5912,14 @@ "$ref": "#/definitions/GitProvider" } }, - "docker_registries": { - "description": "Which docker registries are available on the AMI.", + "image_registries": { + "description": "Which image registries are available on the AMI.\n\nPre v2.3.0, called `docker_registries`", "type": [ "array", "null" ], "items": { - "$ref": "#/definitions/DockerRegistry" + "$ref": "#/definitions/ImageRegistry" } }, "secrets": { @@ -5891,11 +5976,11 @@ "username" ] }, - "DockerRegistry": { + "ImageRegistry": { "type": "object", "properties": { "domain": { - "description": "The docker provider domain. Default: `docker.io`.", + "description": "The image provider domain. Default: `docker.io`.", "type": "string", "default": "docker.io" }, diff --git a/ui/src/app/sidebar.tsx b/ui/src/app/sidebar.tsx index bfff9c1f67..998bcb0092 100644 --- a/ui/src/app/sidebar.tsx +++ b/ui/src/app/sidebar.tsx @@ -31,6 +31,12 @@ const Sidebar = ({ close }: { close: () => void }) => { to="/terminals" {...linkProps} /> + } + to="/stats" + {...linkProps} + /> void; - actions: SpotlightActionGroupData[]; -} { +// Use a dedicated store to allow selection control below. +const [spotlightStore, spotlight] = createSpotlight(); + +export function useOmniSearch(opened: boolean) { const navigate = useNavigate(); const nav = useCallback( (to: string) => { @@ -43,42 +47,69 @@ export function useOmniSearch(): { search .toLowerCase() .split(" ") + .map((term) => term.trim()) .filter((term) => term), [search], ); - const _containers = useRead( - "ListAllDockerContainers", - {}, - { refetchInterval: 15_000 }, - ).data; - const containers = useMemo(() => { - return _containers?.filter((c) => { - if (searchTerms.length === 0) return true; - const lower = c.name.toLowerCase(); - return searchTerms.every( - (term) => lower.includes(term) || "containers".includes(term), - ); - }); - }, [_containers, searchTerms]); + const debouncedTerms = useDebounce(searchTerms, 500); - const _terminals = useRead( - "ListTerminals", - {}, - { refetchInterval: 15_000 }, - ).data; - const terminals = useMemo(() => { - return _terminals?.filter((c) => { - if (searchTerms.length === 0) return true; - const lower = c.name.toLowerCase(); - return searchTerms.every( - (term) => lower.includes(term) || "terminals".includes(term), - ); - }); - }, [_terminals, searchTerms]); + const containersQuery: Types.ListAllContainers = useMemo( + () => ({ + terms: debouncedTerms + // Allows search like 'cont my name' to return containers matching 'my name' + .filter((term) => !termMatchesTypeKeyword("containers", term)), + limit: 10, + page: 0, + }), + [debouncedTerms], + ); + + const containers = useRead("ListAllContainers", containersQuery, { + refetchInterval: 15_000, + // Only fetch when open and there is query typed + enabled: opened && !!debouncedTerms.length, + // Keep the previous results visible while fetching after a search + // change to prevent resource flashing. + placeholderData: keepPreviousData, + }).data; + + const servicesQuery: Types.ListAllStackServices = useMemo( + () => ({ + terms: debouncedTerms + // Allows search like 'serv my name' to return services matching 'my name' + .filter((term) => !termMatchesTypeKeyword("services", term)), + limit: 10, + page: 0, + }), + [debouncedTerms], + ); + + const services = useRead("ListAllStackServices", servicesQuery, { + refetchInterval: 15_000, + enabled: opened && !!debouncedTerms.length, + placeholderData: keepPreviousData, + }).data; + + const terminalsQuery: Types.ListTerminals = useMemo( + () => ({ + terms: debouncedTerms + // Allows search like 'term my name' to return terminals matching 'my name' + .filter((term) => !termMatchesTypeKeyword("terminals", term)), + limit: 10, + page: 0, + }), + [debouncedTerms], + ); + + const terminals = useRead("ListTerminals", terminalsQuery, { + refetchInterval: 15_000, + enabled: opened, + placeholderData: keepPreviousData, + }).data; const user = useUser().data; - const resources = useAllResources(15_000); + const resources = useAllResources(debouncedTerms, 10, 15_000, opened); const [_, setSettingsView] = useSettingsView(); const _actions = useMemo(() => { @@ -123,6 +154,12 @@ export function useOmniSearch(): { leftSection: , onClick: () => nav("/terminals"), }, + { + id: "Stats", + label: "Stats", + leftSection: , + onClick: () => nav("/stats"), + }, { id: "Schedules", label: "Schedules", @@ -159,7 +196,8 @@ export function useOmniSearch(): { ...RESOURCE_TARGETS.map((_type) => { const type = _type === "ResourceSync" ? "Sync" : _type; - const lowerType = type.toLowerCase(); + // Matches the server side term stripping in useAllResources + const typeKeyword = type.toLowerCase() + "s"; const Components = ResourceComponents[_type]; return { group: type + "s", @@ -171,14 +209,17 @@ export function useOmniSearch(): { searchTerms.length === 0 || searchTerms.every( (term) => - lowerName.includes(term) || lowerType.includes(term), + lowerName.includes(term) || + termMatchesTypeKeyword(typeKeyword, term), ) ); }) .map((resource) => { const info = resource.info as { - swarm_id: string; - server_id: string; + swarm_id?: string; + swarm_name?: string; + server_id?: string; + server_name?: string; }; return { id: type + " " + resource.name, @@ -192,32 +233,41 @@ export function useOmniSearch(): { ), description: info.swarm_id - ? "Swarm: " + - resources.Swarm?.find( - (swarm) => info.swarm_id === swarm.id, - )?.name + ? "Swarm: " + info.swarm_name : info.server_id - ? "Server: " + - resources.Server?.find( - (server) => info.server_id === server.id, - )?.name + ? "Server: " + info.server_name : undefined, }; }) ?? [], }; }), + { + group: "Services", + actions: + services?.map((service) => { + const intention = service?.swarm_service?.State + ? swarmStateIntention(service?.swarm_service?.State) + : containerStateIntention(service?.container?.state); + const color = hexColorByIntention(intention); + return { + id: service.stack_id + " " + service.service, + label: service.service, + description: "Stack: " + service.stack_name, + onClick: () => + nav(`/stacks/${service.stack_id}/service/${service.service}`), + leftSection: , + }; + }) ?? [], + }, + { group: "Containers", actions: containers?.map((container) => ({ - id: container.server_id ?? "" + " " + container.name, + id: (container.server_id ?? "") + " " + container.name, label: container.name, - description: - "Server: " + - resources.Server?.find( - (server) => container.server_id === server.id, - )?.name, + description: "Server: " + container.server_name, onClick: () => nav( `/servers/${container.server_id}/container/${container.name}`, @@ -238,31 +288,62 @@ export function useOmniSearch(): { terminals?.map((terminal) => ({ id: JSON.stringify(terminal.target) + " " + terminal.name, label: terminal.name, - description: terminalTargetDescription(terminal.target, resources), + description: terminalTargetDescription( + terminal.target, + terminal.target_name, + ), onClick: () => nav(terminalLink(terminal)), leftSection: , })) ?? [], }, ]; - }, [resources]); + }, [ + resources, + containers, + services, + terminals, + searchTerms, + user, + nav, + setSettingsView, + ]); + + useEffect(() => { + // Mantine only auto selects the first action when the query changes, + // but the omni search actions settle later (debounce + fetch), + // which can leave nothing selected. Select the first action + // whenever newly settled actions render. + const { listId } = spotlightStore.getState(); + const list = listId ? document.getElementById(listId) : null; + if (!list) return; + list.querySelector("[data-selected]")?.removeAttribute("data-selected"); + const first = list.querySelector("[data-action]"); + if (first) { + first.setAttribute("data-selected", "true"); + first.scrollIntoView({ block: "nearest" }); + } + spotlightStore.updateState((state) => ({ + ...state, + selected: first ? 0 : -1, + })); + }, [_actions]); // LIMIT the action count for performance. - // Reset count on render before creating actual actions. - count = 0; + let count = 0; const actions: SpotlightActionGroupData[] = []; for (const group of _actions) { const groupActions = []; for (const action of group.actions) { - groupActions.push(action); - count += 1; - if (count > ITEM_LIMIT) { + if (count >= ITEM_LIMIT) { break; } + groupActions.push(action); + count += 1; } if (groupActions.length) { actions.push({ group: group.group, actions: groupActions }); } - if (count > ITEM_LIMIT) { + if (count >= ITEM_LIMIT) { break; } } @@ -271,42 +352,25 @@ export function useOmniSearch(): { search, setSearch, actions, + spotlight, + spotlightStore, }; } function terminalTargetDescription( target: Types.TerminalTarget, - resources: ResourceMap, + target_name: string | undefined, ) { switch (target.type) { case "Server": - return ( - "Server: " + - resources.Server?.find((server) => target.params.server === server.id) - ?.name - ); + return "Server: " + target_name; case "Container": return ( - "Server: " + - resources.Server?.find((server) => target.params.server === server.id) - ?.name + - ", Container: " + - target.params.container + "Server: " + target_name + ", Container: " + target.params.container ); case "Stack": - return ( - "Stack: " + - resources.Stack?.find((stack) => target.params.stack === stack.id) - ?.name + - ", Service: " + - target.params.service - ); + return "Stack: " + target_name + ", Service: " + target.params.service; case "Deployment": - return ( - "Deployment: " + - resources.Deployment?.find( - (deployment) => target.params.deployment === deployment.id, - )?.name - ); + return "Deployment: " + target_name; } } diff --git a/ui/src/app/topbar/omni-search/index.tsx b/ui/src/app/topbar/omni-search/index.tsx index c5b8d9ee70..45a0908299 100644 --- a/ui/src/app/topbar/omni-search/index.tsx +++ b/ui/src/app/topbar/omni-search/index.tsx @@ -1,13 +1,21 @@ -import { ActionIcon, Badge, Button, Group } from "@mantine/core"; -import { Spotlight, spotlight } from "@mantine/spotlight"; +import { ActionIcon, Badge, Button, Group, Switch, Text } from "@mantine/core"; +import { Spotlight } from "@mantine/spotlight"; import { useOmniSearch } from "./hooks"; import { ICONS } from "@/lib/icons"; import { useShiftKeyListener } from "mogh_ui"; import classes from "./index.module.scss"; +import { useLocalStorage } from "@mantine/hooks"; +import { useState } from "react"; export default function OmniSearch({}: {}) { - const { search, setSearch, actions } = useOmniSearch(); + const [opened, setOpened] = useState(false); + const { search, setSearch, actions, spotlight, spotlightStore } = + useOmniSearch(opened); useShiftKeyListener("S", () => spotlight.open()); + const [clearOnClose, setClearOnClose] = useLocalStorage({ + key: "omnisearch.clearOnClose", + defaultValue: true, + }); return ( <> setOpened(true)} + onSpotlightClose={() => setOpened(false)} + styles={{ content: { borderRadius: "var(--mantine-radius-sm)" } }} > } placeholder="search..." /> + + setClearOnClose((v) => !v)} + p="0.5rem 1rem" + style={{ cursor: "pointer" }} + > + Clear on close + setClearOnClose((v) => !v)} + /> + + {actions.map((group) => ( diff --git a/ui/src/components/config/account-selector.tsx b/ui/src/components/config/account-selector.tsx index cdf74096fe..58c9339aaf 100644 --- a/ui/src/components/config/account-selector.tsx +++ b/ui/src/components/config/account-selector.tsx @@ -28,10 +28,10 @@ export default function AccountSelector({ }: AccountSelectorProps) { const [dbRequest, configRequest]: | ["ListGitProviderAccounts", "ListGitProvidersFromConfig"] - | ["ListDockerRegistryAccounts", "ListDockerRegistriesFromConfig"] = + | ["ListImageRegistryAccounts", "ListImageRegistriesFromConfig"] = accountType === "git" ? ["ListGitProviderAccounts", "ListGitProvidersFromConfig"] - : ["ListDockerRegistryAccounts", "ListDockerRegistriesFromConfig"]; + : ["ListImageRegistryAccounts", "ListImageRegistriesFromConfig"]; const dbAccounts = useRead(dbRequest, {}).data?.filter( (account) => account.domain === provider, ); diff --git a/ui/src/components/config/image-registry-config.tsx b/ui/src/components/config/image-registry-config.tsx index 4451735b26..63dcc43fdb 100644 --- a/ui/src/components/config/image-registry-config.tsx +++ b/ui/src/components/config/image-registry-config.tsx @@ -25,7 +25,7 @@ export default function ImageRegistryConfig({ imageName, }: ImageRegistryConfig) { // This is the only way to get organizations for now - const config_provider = useRead("ListDockerRegistriesFromConfig", { + const config_provider = useRead("ListImageRegistriesFromConfig", { target: builderId ? { type: "Builder", id: builderId } : undefined, }).data?.find((provider) => { return provider.domain === registry?.domain; diff --git a/ui/src/components/config/provider-selector.tsx b/ui/src/components/config/provider-selector.tsx index f35949029e..864ea990c4 100644 --- a/ui/src/components/config/provider-selector.tsx +++ b/ui/src/components/config/provider-selector.tsx @@ -25,10 +25,10 @@ export default function ProviderSelector({ }: ProviderSelectorProps) { const [dbRequest, configRequest]: | ["ListGitProviderAccounts", "ListGitProvidersFromConfig"] - | ["ListDockerRegistryAccounts", "ListDockerRegistriesFromConfig"] = + | ["ListImageRegistryAccounts", "ListImageRegistriesFromConfig"] = accountType === "git" ? ["ListGitProviderAccounts", "ListGitProvidersFromConfig"] - : ["ListDockerRegistryAccounts", "ListDockerRegistriesFromConfig"]; + : ["ListImageRegistryAccounts", "ListImageRegistriesFromConfig"]; const dbProviders = useRead(dbRequest, {}).data; const configProviders = useRead(configRequest, {}).data; const [customMode, setCustomMode] = useState(false); diff --git a/ui/src/components/confirm-modal-with-disable.tsx b/ui/src/components/confirm-modal-with-disable.tsx index 368b922c57..a0d9940409 100644 --- a/ui/src/components/confirm-modal-with-disable.tsx +++ b/ui/src/components/confirm-modal-with-disable.tsx @@ -10,5 +10,26 @@ export default function ConfirmModalWithDisable({ ...props }: ConfirmModalWithDisableProps) { const disabled = useRead("GetCoreInfo", {}).data?.disable_confirm_dialog; - return ; + return ( + { + const target = event.target; + const confirmation = + target instanceof Element ? target.closest("b") : null; + + if (confirmation?.textContent !== props.confirmText) return; + + event.preventDefault(); + const selection = window.getSelection(); + if (!selection) return; + + const range = document.createRange(); + range.selectNodeContents(confirmation); + selection.removeAllRanges(); + selection.addRange(range); + }} + {...props} + /> + ); } diff --git a/ui/src/components/docker/batch-executions.tsx b/ui/src/components/docker/batch-executions.tsx new file mode 100644 index 0000000000..57fd505044 --- /dev/null +++ b/ui/src/components/docker/batch-executions.tsx @@ -0,0 +1,282 @@ +import { fmtUpperCamelcase, sendCopyNotification } from "mogh_ui"; +import { useExecute, useRead, useSelectedDockerResources } from "@/lib/hooks"; +import { ICONS } from "@/lib/icons"; +import { + Box, + Button, + Divider, + Group, + List, + Loader, + Menu, + Modal, + Stack, + Text, + TextInput, + useMatches, +} from "@mantine/core"; +import { ChevronDown } from "lucide-react"; +import { FC, useState } from "react"; +import { DockerResourceType } from "."; + +type DockerExecution = + | "StartContainer" + | "RestartContainer" + | "PauseContainer" + | "UnpauseContainer" + | "StopContainer" + | "DestroyContainer" + | "DeleteNetwork" + | "DeleteImage" + | "DeleteVolume"; + +const DOCKER_EXECUTIONS: { + [type in DockerResourceType]: [ + DockerExecution, + FC<{ size?: string | number }>, + ][]; +} = { + Container: [ + ["StartContainer", ICONS.Start], + ["RestartContainer", ICONS.Restart], + ["PauseContainer", ICONS.Pause], + ["UnpauseContainer", ICONS.Start], + ["StopContainer", ICONS.Stop], + ], + Network: [], + Image: [], + Volume: [], +}; + +const DELETE_EXECUTIONS: { [type in DockerResourceType]: DockerExecution } = { + Container: "DestroyContainer", + Network: "DeleteNetwork", + Image: "DeleteImage", + Volume: "DeleteVolume", +}; + +/** Splits `${server_id} ${resource_name}` held in + * useSelectedDockerResources into its parts. */ +function splitDockerResource(resource: string): [string, string] { + const i = resource.indexOf(" "); + return [resource.slice(0, i), resource.slice(i + 1)]; +} + +export interface DockerBatchExecutionsProps { + type: DockerResourceType; +} + +export default function DockerBatchExecutions({ + type, +}: DockerBatchExecutionsProps) { + const [execution, setExecution] = useState(); + const [selected] = useSelectedDockerResources(type); + + return ( + <> + + e[0] === execution)?.[1] ?? + ICONS.Delete + } + onClose={() => setExecution(undefined)} + /> + + ); +} + +function DockerBatchExecutionsDropdownMenu({ + type, + onSelect, + disabled, +}: { + type: DockerResourceType; + onSelect: (item: DockerExecution) => void; + disabled: boolean; +}) { + const executions = DOCKER_EXECUTIONS[type]; + const width = useMatches({ + base: "target", + xs: 250, + }); + + if (executions.length === 0) { + return ( + + ); + } + + return ( + + + + + + + {executions.map(([execution, Icon]) => ( + } + onClick={() => onSelect(execution)} + renderRoot={(props) => + ); +} + +function DockerBatchExecutionsModal({ + type, + execution, + icon: Icon, + onClose: _onClose, +}: { + type: DockerResourceType; + execution: DockerExecution | undefined; + icon: FC<{ size?: string | number }>; + onClose: () => void; +}) { + const [selected, setSelected] = useSelectedDockerResources(type); + const [input, setInput] = useState(""); + const onClose = () => { + setInput(""); + _onClose(); + }; + + const { mutate: execute, isPending } = useExecute(execution!, { + onSuccess: onClose, + }); + + if (!execution) return; + + const formatted = fmtUpperCamelcase(execution.replaceAll(type, "")); + const isDelete = execution === DELETE_EXECUTIONS[type]; + + return ( + onClose()} + title={Group Execute - {formatted}} + size="lg" + > + + + + {selected.map((resource) => ( + + ))} + + + + { + navigator.clipboard.writeText(formatted); + sendCopyNotification(); + }} + style={{ cursor: "pointer" }} + > + Please enter {formatted} below to confirm this action. + {(location.origin.startsWith("https") || + // For dev + location.origin.startsWith("http://localhost:")) && ( + + You may click the text in bold to copy it + + )} + + + setInput(e.target.value)} + error={input === formatted ? undefined : "Does not match"} + /> + + + + + + + ); +} + +function SelectedDockerResource({ resource }: { resource: string }) { + const [serverId, name] = splitDockerResource(resource); + const server = useRead("ListServers", {}).data?.find( + (server) => server.id === serverId, + ); + return ( + + + {name} + + {server?.name ?? serverId} + + + + ); +} diff --git a/ui/src/components/docker/container-ports.tsx b/ui/src/components/docker/container-ports.tsx index fd4a7a8753..a36e6f2c09 100644 --- a/ui/src/components/docker/container-ports.tsx +++ b/ui/src/components/docker/container-ports.tsx @@ -31,19 +31,11 @@ export default function ContainerPorts({ }: ContainerPortsProps) { const portsMap = useContainerPortsMap(ports); - if (Object.keys(portsMap).length > 0) { - console.log("mapped", portsMap); - } - const sortedNumericPorts = Object.keys(portsMap) .map(Number) .filter((port) => !Number.isNaN(port)) .sort((a, b) => a - b); - if (Object.keys(portsMap).length > 0) { - console.log("sorted", sortedNumericPorts); - } - type Group = { start: number; end: number; ports: Types.Port[] }; const groupedPorts = sortedNumericPorts.reduce((acc, port) => { @@ -62,8 +54,6 @@ export default function ContainerPorts({ return null; } - console.log("grouped", groupedPorts); - return ( {groupedPorts.map((group) => ( diff --git a/ui/src/components/docker/container-selector.tsx b/ui/src/components/docker/container-selector.tsx index 812e14a50a..387d58b15b 100644 --- a/ui/src/components/docker/container-selector.tsx +++ b/ui/src/components/docker/container-selector.tsx @@ -40,7 +40,7 @@ export default function ContainerSelector({ clearable, ...comboboxProps }: ContainerSelectorProps) { - const containers = useRead("ListDockerContainers", { + const containers = useRead("ListContainers", { server: serverId, }).data?.filter((container) => !state || container.state === state); diff --git a/ui/src/components/docker/containers-section.tsx b/ui/src/components/docker/containers-section.tsx index f63d084c01..2b40fb5d4a 100644 --- a/ui/src/components/docker/containers-section.tsx +++ b/ui/src/components/docker/containers-section.tsx @@ -1,6 +1,7 @@ -import { useRead } from "@/lib/hooks"; +import { useDockerSelectionState, useRead } from "@/lib/hooks"; import { filterBySplit } from "mogh_ui"; import { Prune } from "@/resources/server/executions"; +import DockerBatchExecutions from "./batch-executions"; import { ICONS } from "@/lib/icons"; import { DataTable, SortableHeader } from "mogh_ui"; import { Section, SectionProps } from "mogh_ui"; @@ -16,12 +17,14 @@ import { DividedChildren } from "mogh_ui"; export interface ContainersSectionProps extends SectionProps { serverId: string; - containers: Types.ListDockerContainersResponse; + containers: Types.ListContainersResponse; show?: boolean; setShow?: (show: boolean) => void; pruneButton?: boolean; forceTall?: boolean; _search?: [string, (search: string) => void]; + /** Enables multi select of containers for batch executions. */ + selectable?: boolean; } export default function ContainersSection({ @@ -32,10 +35,12 @@ export default function ContainersSection({ pruneButton, forceTall, _search, + selectable, titleOther, ...sectionProps }: ContainersSectionProps) { - const allRunning = useRead("ListDockerContainers", { + const selectionState = useDockerSelectionState("Container"); + const allRunning = useRead("ListContainers", { server: serverId, }).data?.every( (container) => container.state === Types.ContainerStateStatusEnum.Running, @@ -49,151 +54,169 @@ export default function ContainersSection({ titleOther={titleOther} title={!titleOther ? "Containers" : undefined} icon={!titleOther ? : undefined} - actions={ - (pruneButton && !allRunning) || _search || setShow ? ( - - {pruneButton && !allRunning && ( - - )} - {_search && ( - - )} - {setShow && } - - ) : undefined - } {...sectionProps} > {show && ( - ( - - ), - cell: ({ row }) => ( - - ), - }, - { - accessorKey: "state", - size: 160, - header: ({ column }) => ( - - ), - cell: ({ row }) => { - const state = row.original?.state; - return ( - + {(selectable || + (pruneButton && !allRunning) || + _search || + setShow) && ( + + + {selectable && } + {pruneButton && !allRunning && ( + + )} + + + + {_search && ( + + )} + {setShow && } + + + )} + + `${serverId} ${name}`, + state: selectionState, + } + : undefined + } + columns={[ + { + accessorKey: "name", + size: 260, + header: ({ column }) => ( + + ), + cell: ({ row }) => ( + - ); + ), }, - }, - { - accessorKey: "image", - size: 300, - header: ({ column }) => ( - - ), - cell: ({ row }) => ( - - ), - }, - { - accessorKey: "networks.0", - size: 200, - header: ({ column }) => ( - - ), - cell: ({ row }) => - (row.original.networks?.length ?? 0) > 0 ? ( - - {row.original.networks?.map((network) => ( + { + accessorKey: "state", + size: 160, + header: ({ column }) => ( + + ), + cell: ({ row }) => { + const state = row.original?.state; + return ( + + ); + }, + }, + { + accessorKey: "image", + size: 300, + header: ({ column }) => ( + + ), + cell: ({ row }) => ( + + ), + }, + { + accessorKey: "networks.0", + size: 200, + header: ({ column }) => ( + + ), + cell: ({ row }) => + (row.original.networks?.length ?? 0) > 0 ? ( + + {row.original.networks?.map((network) => ( + + ))} + + ) : ( + row.original.network_mode && ( + ) + ), + }, + { + accessorKey: "ports.0", + size: 200, + sortingFn: (a, b) => { + const getMinHostPort = (row: typeof a) => { + const ports = row.original.ports ?? []; + if (!ports.length) return Number.POSITIVE_INFINITY; + const nums = ports + .map((p) => p.PublicPort) + .filter((p): p is number => typeof p === "number") + .map((n) => Number(n)); + if (!nums.length || nums.some((n) => Number.isNaN(n))) { + return Number.POSITIVE_INFINITY; + } + return Math.min(...nums); + }; + const pa = getMinHostPort(a); + const pb = getMinHostPort(b); + return pa === pb ? 0 : pa > pb ? 1 : -1; + }, + header: ({ column }) => ( + + ), + cell: ({ row }) => ( + + ), + }, + { + accessorKey: "volumes.0", + size: 200, + header: ({ column }) => ( + + ), + cell: ({ row }) => ( + + {row.original.volumes?.map((volume) => ( + ))} - ) : ( - row.original.network_mode && ( - - ) ), - }, - { - accessorKey: "ports.0", - size: 200, - sortingFn: (a, b) => { - const getMinHostPort = (row: typeof a) => { - const ports = row.original.ports ?? []; - if (!ports.length) return Number.POSITIVE_INFINITY; - const nums = ports - .map((p) => p.PublicPort) - .filter((p): p is number => typeof p === "number") - .map((n) => Number(n)); - if (!nums.length || nums.some((n) => Number.isNaN(n))) { - return Number.POSITIVE_INFINITY; - } - return Math.min(...nums); - }; - const pa = getMinHostPort(a); - const pb = getMinHostPort(b); - return pa === pb ? 0 : pa > pb ? 1 : -1; }, - header: ({ column }) => ( - - ), - cell: ({ row }) => ( - - ), - }, - { - accessorKey: "volumes.0", - size: 200, - header: ({ column }) => ( - - ), - cell: ({ row }) => ( - - {row.original.volumes?.map((volume) => ( - - ))} - - ), - }, - ]} - /> + ]} + /> + )} ); diff --git a/ui/src/components/docker/index.tsx b/ui/src/components/docker/index.tsx new file mode 100644 index 0000000000..73b23e0c7b --- /dev/null +++ b/ui/src/components/docker/index.tsx @@ -0,0 +1 @@ +export type DockerResourceType = "Container" | "Network" | "Image" | "Volume"; diff --git a/ui/src/components/docker/link.tsx b/ui/src/components/docker/link.tsx index 0b118392ce..c3d93ef600 100644 --- a/ui/src/components/docker/link.tsx +++ b/ui/src/components/docker/link.tsx @@ -6,8 +6,7 @@ import { containerStateIntention } from "@/lib/color"; import { Box, Group, Text } from "@mantine/core"; import { Link } from "react-router-dom"; import { hexColorByIntention } from "mogh_ui"; - -export type DockerResourceType = "Container" | "Network" | "Image" | "Volume"; +import { DockerResourceType } from "."; export interface DockerResourceLinkProps { type: DockerResourceType; @@ -62,7 +61,7 @@ export const DOCKER_LINK_ICONS: { } = { Container: ({ serverId, name, size = "1rem" }) => { const state = - useRead("ListDockerContainers", { server: serverId }).data?.find( + useRead("ListContainers", { server: serverId }).data?.find( (container) => container.name === name, )?.state ?? Types.ContainerStateStatusEnum.Empty; return ( @@ -74,7 +73,7 @@ export const DOCKER_LINK_ICONS: { }, Network: ({ serverId, name, size = "1rem" }) => { const containers = - useRead("ListDockerContainers", { server: serverId }).data ?? []; + useRead("ListContainers", { server: serverId }).data ?? []; const noContainers = !name ? false : containers.every((container) => !container.networks?.includes(name)); @@ -89,7 +88,7 @@ export const DOCKER_LINK_ICONS: { }, Image: ({ serverId, name, size = "1rem" }) => { const containers = - useRead("ListDockerContainers", { server: serverId }).data ?? []; + useRead("ListContainers", { server: serverId }).data ?? []; const noContainers = !name ? false : containers.every((container) => container.image_id !== name); @@ -98,7 +97,7 @@ export const DOCKER_LINK_ICONS: { }, Volume: ({ serverId, name, size = "1rem" }) => { const containers = - useRead("ListDockerContainers", { server: serverId }).data ?? []; + useRead("ListContainers", { server: serverId }).data ?? []; const noContainers = !name ? false : containers.every((container) => !container.volumes?.includes(name)); diff --git a/ui/src/components/export-toml.tsx b/ui/src/components/export-toml.tsx index 7e622503c4..9a27acffb3 100644 --- a/ui/src/components/export-toml.tsx +++ b/ui/src/components/export-toml.tsx @@ -1,5 +1,6 @@ import { useRead } from "@/lib/hooks"; import { ICONS } from "@/lib/icons"; +import { UsableResource } from "@/resources"; import { Box, Button, Modal } from "@mantine/core"; import { useDisclosure } from "@mantine/hooks"; import { Types } from "komodo_client"; @@ -9,6 +10,15 @@ import { LoadingScreen } from "mogh_ui"; export interface ExportTomlProps { targets?: Types.ResourceTarget[]; + /** + * Export all resources of the type matching the query, + * not just the ones on the current page. + * The matching targets are only fetched when the modal is opened. + */ + listQuery?: { + type: UsableResource; + query: Types.ResourceQuery; + }; userGroups?: string[]; tags?: string[]; includeVariables?: boolean; @@ -37,20 +47,32 @@ export default function ExportToml(props: ExportTomlProps) { function ExportTomlInner({ targets, + listQuery, userGroups, tags, includeVariables, }: ExportTomlProps) { - const useAll = !(targets || userGroups || includeVariables); + const useAll = !(targets || listQuery || userGroups || includeVariables); + + const listTargets = useRead( + `List${listQuery?.type ?? "Server"}s`, + { query: listQuery?.query, limit: 0 }, + { enabled: !!listQuery }, + ).data?.map( + (resource) => + ({ type: listQuery!.type, id: resource.id }) as Types.ResourceTarget, + ); + + const exportTargets = listQuery ? listTargets : targets; const { data: resourcesData, isPending: resourcesPending } = useRead( "ExportResourcesToToml", { - targets: targets ? targets : [], + targets: exportTargets ? exportTargets : [], user_groups: userGroups ? userGroups : [], include_variables: includeVariables, }, - { enabled: !useAll }, + { enabled: !useAll && (!listQuery || !!listTargets) }, ); const { data: allData, isPending: allPending } = useRead( diff --git a/ui/src/components/list-pagination.tsx b/ui/src/components/list-pagination.tsx new file mode 100644 index 0000000000..3fb815fc06 --- /dev/null +++ b/ui/src/components/list-pagination.tsx @@ -0,0 +1,46 @@ +import { useRead } from "@/lib/hooks"; +import { Group, Pagination } from "@mantine/core"; + +/** + * The server side default page size on `List` calls, + * from the Core config `default_pagination_limit`. + */ +export function usePageSize() { + const limit = useRead("GetCoreInfo", {}).data?.default_pagination_limit; + // Fall back to the Core config default while loading. + if (limit === undefined) return 30; + // `limit: 0` disables pagination, so the page is never full. + return limit === 0 ? Infinity : limit; +} + +/** + * Pagination controls for the paginated `List` calls. + * Only renders when needed, ie the results fill the page + * or the user is past the first page. + */ +export default function ListPagination({ + page, + setPage, + count, +}: { + page: number; + setPage: (page: number) => void; + count: number; +}) { + const pageSize = usePageSize(); + if (count < pageSize && page === 0) return null; + return ( + = pageSize ? page + 2 : page + 1} + value={page + 1} + onChange={(page) => setPage(page - 1)} + > + + + + + + + + ); +} diff --git a/ui/src/components/log-viewer.tsx b/ui/src/components/log-viewer.tsx index 9212c3e886..1a4d1a39fd 100644 --- a/ui/src/components/log-viewer.tsx +++ b/ui/src/components/log-viewer.tsx @@ -71,8 +71,6 @@ export default function LogViewer({ } }; - console.log(lines); - return ( { + const permissions: (Types.Permission & { name: string })[] = []; + for (const [resourceType, resources] of Object.entries(allResources)) { + addUserTargetPermissions( + userTarget, + allPermissions, + resourceType as UsableResource, + resources, + permissions, + ); + } + return permissions; + }, [ + userTarget, + allPermissions, + // Diff against resource arrays individually + ...Object.values(allResources), + ]); + + return { + permissions, + search, + setSearch, + }; +} + +function addUserTargetPermissions( + userTarget: Types.UserTarget, + allPermissions: Types.Permission[] | undefined, + resourceType: UsableResource, + resources: Types.ResourceListItem[] | undefined, + permissions: (Types.Permission & { name: string })[], +) { + resources?.forEach((resource) => { + const perm = allPermissions?.find( + (p) => + p.resource_target.type === resourceType && + p.resource_target.id === resource.id, + ); + if (perm) { + permissions.push({ ...perm, name: resource.name }); + } else { + permissions.push({ + user_target: userTarget, + name: resource.name, + level: Types.PermissionLevel.None, + resource_target: { type: resourceType, id: resource.id }, + }); + } + }); +} diff --git a/ui/src/components/permissions/specific-section.tsx b/ui/src/components/permissions/specific-section.tsx index 9a030db3eb..038a04f802 100644 --- a/ui/src/components/permissions/specific-section.tsx +++ b/ui/src/components/permissions/specific-section.tsx @@ -1,4 +1,4 @@ -import { useInvalidate, useUserTargetPermissions, useWrite } from "@/lib/hooks"; +import { useInvalidate, useWrite } from "@/lib/hooks"; import { levelSortingFn } from "@/lib/utils"; import { ResourceComponents, UsableResource } from "@/resources"; import ResourceLink from "@/resources/link"; @@ -8,11 +8,12 @@ import { Section, SectionProps } from "mogh_ui"; import { Group, Text } from "@mantine/core"; import { notifications } from "@mantine/notifications"; import { Types } from "komodo_client"; -import { useState } from "react"; +import { useMemo, useState } from "react"; import ResourceTypeSelector from "../resource-type-selector"; import { LabelledSwitch } from "mogh_ui"; import PermissionLevelSelector from "./level-selector"; import SpecificPermissionSelector from "./specific-selector"; +import { useUserTargetPermissions } from "./hooks"; export interface SpecificPermissionsTableProps extends SectionProps { userTarget: Types.UserTarget; @@ -24,7 +25,7 @@ export default function SpecificPermissionsSection({ }: SpecificPermissionsTableProps) { const [showAll, setShowAll] = useState(false); const [resourceType, setResourceType] = useState(null); - const permissions = useUserTargetPermissions(userTarget); + const inv = useInvalidate(); const { mutate: update } = useWrite("UpdatePermissionOnTarget", { onSuccess: () => { @@ -32,42 +33,20 @@ export default function SpecificPermissionsSection({ notifications.show({ message: "Updated permission", color: "green" }); }, }); - const [search, setSearch] = useState(""); - const searchSplit = search.toLowerCase().split(" "); - const tableData = - permissions?.filter( - (permission) => - (resourceType === null - ? true - : permission.resource_target.type === resourceType) && - (showAll ? true : permission.level !== Types.PermissionLevel.None) && - searchSplit.every( - (search) => - permission.name.toLowerCase().includes(search) || - permission.resource_target.type.toLowerCase().includes(search), - ), - ) ?? []; - return ( -
- - - - - } - {...sectionProps} - > + + const { permissions, search, setSearch } = + useUserTargetPermissions(userTarget); + + const Table = useMemo(() => { + const tableData = + permissions?.filter( + (permission) => + (resourceType === null + ? true + : permission.resource_target.type === resourceType) && + (showAll ? true : permission.level !== Types.PermissionLevel.None), + ) ?? []; + return ( + ); + }, [showAll, resourceType, permissions]); + + return ( +
+ + + + + } + {...sectionProps} + > + {Table}
); } diff --git a/ui/src/components/stack-service-link.tsx b/ui/src/components/stack-service-link.tsx index 82346448cc..acf15c41d2 100644 --- a/ui/src/components/stack-service-link.tsx +++ b/ui/src/components/stack-service-link.tsx @@ -1,8 +1,10 @@ import { containerStateIntention, swarmStateIntention } from "@/lib/color"; import { useRead } from "@/lib/hooks"; import { ICONS } from "@/lib/icons"; +import { useStack } from "@/resources/stack"; import { Group, Text } from "@mantine/core"; -import { hexColorByIntention } from "mogh_ui"; +import { Types } from "komodo_client"; +import { ColorIntention, hexColorByIntention } from "mogh_ui"; import { Link } from "react-router-dom"; export interface StackServiceLinkProps { @@ -14,15 +16,20 @@ export default function StackServiceLink({ id, service: _service, }: StackServiceLinkProps) { + const isUnknown = useStack(id)?.info.state === Types.StackState.Unknown; const services = useRead( "ListStackServices", { stack: id }, { refetchInterval: 10_000 }, ).data; const service = services?.find((s) => s.service === _service); - const intention = service?.swarm_service?.State + const intention: ColorIntention = service?.swarm_service?.State ? swarmStateIntention(service?.swarm_service?.State) - : containerStateIntention(service?.container?.state); + : service?.container?.state + ? containerStateIntention(service?.container?.state) + : isUnknown + ? "Unknown" + : "Neutral"; const color = hexColorByIntention(intention); return ( void; + /** Use tag names as values instead of ids. */ + useName?: boolean; + canCreate?: boolean; +} + +export default function TagMultiSelector({ + title = "Select Tags", + value, + onChange, + useName, + canCreate, + disabled, + position = "bottom-start", + ...props +}: TagMultiSelectorProps) { + const tags = useRead("ListTags", {}).data; + const otherTags = tags?.filter( + (tag) => !value.includes(useName ? tag.name : tag._id?.$oid!), + ); + + return ( + + onChange([...value, tag])} + disabled={disabled} + position={position} + useName={useName} + canCreate={canCreate} + {...props} + /> + + value.includes(tag.name)) + .map((tag) => tag.name) ?? [] + } + onBadgeClick={ + disabled + ? undefined + : (toRemove) => + onChange(value.filter((tagName) => tagName !== toRemove)) + } + icon={} + fz="1rem" + useName={useName} + /> + + ); +} diff --git a/ui/src/components/terminal/index.tsx b/ui/src/components/terminal/index.tsx index 6750f25674..c1a4ed7481 100644 --- a/ui/src/components/terminal/index.tsx +++ b/ui/src/components/terminal/index.tsx @@ -120,7 +120,7 @@ export default function Terminal({ const callbacks: TerminalCallbacks = { on_login: () => { - // console.log("logged in terminal"); + // console.log("Logged into terminal"); }, on_open: resize, on_message: (e: MessageEvent) => { diff --git a/ui/src/components/terminal/section.tsx b/ui/src/components/terminal/section.tsx index 1d83b078ad..c3d6c755c8 100644 --- a/ui/src/components/terminal/section.tsx +++ b/ui/src/components/terminal/section.tsx @@ -24,9 +24,11 @@ export default function TerminalSection({ "ListTerminals", { target, + // Always show all terminals on the resource. + limit: 0, }, { - refetchInterval: 5000, + refetchInterval: 5_000, }, ); diff --git a/ui/src/components/updates/resource.tsx b/ui/src/components/updates/resource.tsx index 3a1d279721..2264ffc4d1 100644 --- a/ui/src/components/updates/resource.tsx +++ b/ui/src/components/updates/resource.tsx @@ -5,11 +5,19 @@ import { useMemo } from "react"; import UpdatesSection from "./section"; export default function ResourceUpdates({ type, id }: Types.ResourceTarget) { - const deployments = useRead("ListDeployments", {}).data; + const deployment = useRead( + "GetDeployment", + { deployment: id }, + { enabled: type === "Deployment" }, + ).data; + const buildId = + deployment?.config?.image?.type === "Build" + ? deployment.config.image.params.build_id + : undefined; const query = useMemo( - () => getUpdateQuery({ type, id }, deployments), - [type, id, deployments], + () => getUpdateQuery({ type, id }, buildId), + [type, id], ); // const alerts = useRead("ListAlerts", { diff --git a/ui/src/lib/hooks.ts b/ui/src/lib/hooks.ts index dd130022c3..7cfd546bc6 100644 --- a/ui/src/lib/hooks.ts +++ b/ui/src/lib/hooks.ts @@ -8,11 +8,20 @@ import { import { UseMutationOptions, UseQueryOptions, + keepPreviousData, useMutation, useQuery, useQueryClient, } from "@tanstack/react-query"; -import { useCallback, useEffect, useMemo } from "react"; +import { + Dispatch, + SetStateAction, + useCallback, + useEffect, + useMemo, + useState, +} from "react"; +import { RowSelectionState } from "@tanstack/react-table"; import { atom, useAtom } from "jotai"; import { atomFamily } from "jotai-family"; import { atomWithHash } from "jotai-location"; @@ -21,8 +30,13 @@ import { UsableResource, RESOURCE_TARGETS } from "@/resources"; import { hasMinimumPermissions, resourceTargetFromTerminalTarget, + setSelectedStateHandler, + termMatchesTypeKeyword, } from "@/lib/utils"; import { notifications } from "@mantine/notifications"; +import { useDebounce } from "mogh_ui"; +import { useLocalStorage } from "@mantine/hooks"; +import { DockerResourceType } from "@/components/docker"; export function komodo_client() { return KomodoClient(KOMODO_BASE_URL, { @@ -180,12 +194,29 @@ export function atomWithStorage(key: string, init: T) { ); } -export function useResourceName(type: UsableResource) { - const resources = useRead(`List${type}s`, {}).data; - return useCallback( - (id: string) => resources?.find((resource) => resource.id === id)?.name, - [resources], - ); +export function useDebouncedTermSearch(props?: { + debounce?: number; + onUpdate?: () => void; + localStorageKey?: string; +}) { + const { debounce = 200, onUpdate, localStorageKey } = props ?? {}; + const [search, setSearch] = localStorageKey + ? useLocalStorage({ key: localStorageKey, defaultValue: "" }) + : useState(""); + const debouncedSearch = useDebounce(search, debounce); + const terms = useMemo(() => { + onUpdate?.(); + return debouncedSearch + .toLowerCase() + .split(" ") + .map((term) => term.trim()) + .filter((term) => term); + }, [debouncedSearch]); + return { + search, + setSearch, + terms, + }; } export function useResourceParamType() { @@ -195,23 +226,257 @@ export function useResourceParamType() { return (type[0].toUpperCase() + type.slice(1, -1)) as UsableResource; } +// ============== BATCHED LIST ITEM LOOKUP ============== + +/** + * All list item lookups mounting within the window (eg. the rows of a + * rendering table) are combined into a single `Lists` request. + */ +const LIST_ITEM_BATCH_MS = 10; + +/** Matches the server side ObjectId parse used to split `query.names` into ids / names. */ +const OBJECT_ID_REGEX = /^[0-9a-f]{24}$/i; + +type ListItemBatch = { + names: Set; + promise: Promise>>; +}; + +const listItemBatches = new Map(); + +function batchedListItemFetch( + type: UsableResource, + name: string, +): Promise[]> { + // The server ANDs the id filter / name filter it extracts from + // `query.names`, so id lookups must be batched separately from + // name lookups to avoid empty intersections. + const bucket = `${type}:${OBJECT_ID_REGEX.test(name) ? "id" : "name"}`; + let batch = listItemBatches.get(bucket); + if (!batch) { + const names = new Set(); + const promise = new Promise[]>( + (resolve, reject) => { + setTimeout(() => { + listItemBatches.delete(bucket); + komodo_client() + .read(`List${type}s`, { + query: { names: [...names] }, + // limit: 0 so large batches aren't truncated to the default page size + limit: 0, + }) + .then(resolve, reject); + }, LIST_ITEM_BATCH_MS); + }, + ).then((resources) => { + const map = new Map>(); + for (const resource of resources) { + map.set(resource.id, resource); + map.set(resource.name, resource); + } + return map; + }); + batch = { names, promise }; + listItemBatches.set(bucket, batch); + } + batch.names.add(name); + return batch.promise.then((map) => { + const resource = map.get(name); + return resource ? [resource] : []; + }); +} + +/** + * The underlying query for [useListItem]. The query key mirrors the + * plain `useRead("Lists", { query: { names: [id] } })` call, so cache + * entries are shared and `useInvalidate(["Lists"])` style prefix + * invalidations reach these too. + * + * Polls by default so state colors etc. stay current for passive + * state changes (which don't come in over the update websocket). + * The refetches re-batch, so this costs one small request + * per resource type per interval. Pass `false` to disable. + */ +export function useListItemQuery( + type: UsableResource, + id: string | undefined, + refetchInterval: number | false = 15_000, +) { + const hasJwt = !!MoghAuth.LOGIN_TOKENS.jwt(); + return useQuery({ + queryKey: [`List${type}s`, { query: { names: id ? [id] : [] } }], + queryFn: () => batchedListItemFetch(type, id!), + enabled: hasJwt && !!id, + refetchInterval, + }); +} + +/** + * Look up a single resource list item by id (or name, with `useName`). + * Concurrent lookups are transparently batched into a single + * `Lists` call per resource type, instead of a request per id. + */ +export function useListItem( + type: T, + id: string | undefined, + useName?: boolean, + refetchInterval?: number | false, +): ReadResponses[`List${T}s`][number] | undefined { + const { data } = useListItemQuery(type, id, refetchInterval); + return (data as Types.ResourceListItem[] | undefined)?.find((r) => + useName ? r.name === id : r.id === id, + ) as ReadResponses[`List${T}s`][number] | undefined; +} + export type ResourceMap = { [Resource in UsableResource]: Types.ResourceListItem[] | undefined; }; -export function useAllResources(refetchInterval?: number): ResourceMap { +export function useAllResources( + terms?: string[], + limit?: number, + refetchInterval?: number, + enabled?: boolean, +): ResourceMap { return { - Swarm: useRead("ListSwarms", {}, { refetchInterval }).data, - Server: useRead("ListServers", {}, { refetchInterval }).data, - Stack: useRead("ListStacks", {}, { refetchInterval }).data, - Deployment: useRead("ListDeployments", {}, { refetchInterval }).data, - Build: useRead("ListBuilds", {}, { refetchInterval }).data, - Repo: useRead("ListRepos", {}, { refetchInterval }).data, - Procedure: useRead("ListProcedures", {}, { refetchInterval }).data, - Action: useRead("ListActions", {}, { refetchInterval }).data, - Builder: useRead("ListBuilders", {}, { refetchInterval }).data, - Alerter: useRead("ListAlerters", {}, { refetchInterval }).data, - ResourceSync: useRead("ListResourceSyncs", {}, { refetchInterval }).data, + Swarm: useRead( + "ListSwarms", + { + query: { + terms: terms?.filter( + (term) => !termMatchesTypeKeyword("swarms", term), + ), + }, + limit, + }, + { + refetchInterval, + enabled, + // Keep the previous results visible while fetching after a search + // change to prevent resource flashing. + placeholderData: keepPreviousData, + }, + ).data, + Server: useRead( + "ListServers", + { + query: { + terms: terms?.filter( + (term) => !termMatchesTypeKeyword("servers", term), + ), + }, + limit, + }, + { refetchInterval, enabled, placeholderData: keepPreviousData }, + ).data, + Stack: useRead( + "ListStacks", + { + query: { + terms: terms?.filter( + (term) => !termMatchesTypeKeyword("stacks", term), + ), + }, + limit, + }, + { refetchInterval, enabled, placeholderData: keepPreviousData }, + ).data, + Deployment: useRead( + "ListDeployments", + { + query: { + terms: terms?.filter( + (term) => !termMatchesTypeKeyword("deployments", term), + ), + }, + limit, + }, + { refetchInterval, enabled, placeholderData: keepPreviousData }, + ).data, + Build: useRead( + "ListBuilds", + { + query: { + terms: terms?.filter( + (term) => !termMatchesTypeKeyword("builds", term), + ), + }, + limit, + }, + { refetchInterval, enabled, placeholderData: keepPreviousData }, + ).data, + Repo: useRead( + "ListRepos", + { + query: { + terms: terms?.filter( + (term) => !termMatchesTypeKeyword("repos", term), + ), + }, + limit, + }, + { refetchInterval, enabled, placeholderData: keepPreviousData }, + ).data, + Procedure: useRead( + "ListProcedures", + { + query: { + terms: terms?.filter( + (term) => !termMatchesTypeKeyword("procedures", term), + ), + }, + limit, + }, + { refetchInterval, enabled, placeholderData: keepPreviousData }, + ).data, + Action: useRead( + "ListActions", + { + query: { + terms: terms?.filter( + (term) => !termMatchesTypeKeyword("actions", term), + ), + }, + limit, + }, + { refetchInterval, enabled, placeholderData: keepPreviousData }, + ).data, + Builder: useRead( + "ListBuilders", + { + query: { + terms: terms?.filter( + (term) => !termMatchesTypeKeyword("builders", term), + ), + }, + limit, + }, + { refetchInterval, enabled, placeholderData: keepPreviousData }, + ).data, + Alerter: useRead( + "ListAlerters", + { + query: { + terms: terms?.filter( + (term) => !termMatchesTypeKeyword("alerters", term), + ), + }, + limit, + }, + { refetchInterval, enabled, placeholderData: keepPreviousData }, + ).data, + ResourceSync: useRead( + "ListResourceSyncs", + { + query: { + terms: terms?.filter( + (term) => !termMatchesTypeKeyword("syncs", term), + ), + }, + limit, + }, + { refetchInterval, enabled, placeholderData: keepPreviousData }, + ).data, }; } @@ -226,37 +491,6 @@ export function useNoResources() { return true; } -/** returns function that takes a resource target and checks if it exists */ -export function useCheckResourceExists() { - const resources = useAllResources(); - return (target: Types.ResourceTarget) => { - return ( - resources[target.type as UsableResource]?.some( - (resource) => resource.id === target.id, - ) || false - ); - }; -} - -export function useFilterResources( - resources?: Types.ResourceListItem[], - search?: string, -) { - const tags = useTagsFilter(); - const searchSplit = search?.toLowerCase()?.split(" ") || []; - return ( - resources?.filter( - (resource) => - tags.every((tag: string) => resource.tags.includes(tag)) && - (searchSplit.length > 0 - ? searchSplit.every((search) => - resource.name.toLowerCase().includes(search), - ) - : true), - ) ?? [] - ); -} - export function usePushRecentlyViewed({ type, id }: Types.ResourceTarget) { const userInvalidate = useUserInvalidate(); @@ -264,11 +498,7 @@ export function usePushRecentlyViewed({ type, id }: Types.ResourceTarget) { onSuccess: userInvalidate, }).mutate; - const exists = useRead(`List${type as UsableResource}s`, {}).data?.find( - (r) => r.id === id, - ) - ? true - : false; + const exists = useListItem(type as UsableResource, id) ? true : false; useEffect(() => { exists && push({ resource: { type, id } }); @@ -365,6 +595,60 @@ export function useSelectedResources(type: UsableResource) { return useAtom(selectedResources(type)); } +/** Controlled DataTable `selectOptions.state` backed by + * the selected resources atom, so batch executions + * (eg. Delete) can clear the table selection state. */ +export function useResourceSelectionState( + type: UsableResource, +): [RowSelectionState, Dispatch>] { + const [selected, setSelected] = useSelectedResources(type); + return useSelectionState(selected, setSelected); +} + +const selectedDockerResources = atomFamily((_: DockerResourceType) => + atom([]), +); +/** Holds list of `${server_id} ${resource_name}` + * (concatenated with a space) + */ +export function useSelectedDockerResources(type: DockerResourceType) { + return useAtom(selectedDockerResources(type)); +} + +/** Controlled DataTable `selectOptions.state` backed by + * the selected docker resources atom, so batch executions + * (eg. Delete) can clear the table selection state. */ +export function useDockerSelectionState( + type: DockerResourceType, +): [RowSelectionState, Dispatch>] { + const [selected, setSelected] = useSelectedDockerResources(type); + return useSelectionState(selected, setSelected); +} + +function useSelectionState( + selected: string[], + setSelected: (list: string[]) => void, +): [RowSelectionState, Dispatch>] { + const selectionState = useMemo( + () => + selected.reduce((state, item) => { + state[item] = true; + return state; + }, {} as RowSelectionState), + [selected], + ); + const setSelectionState = useCallback( + (state: SetStateAction) => + setSelectedStateHandler(state, selectionState, setSelected), + [selectionState, setSelected], + ); + // Stable tuple so it can be used in memo dependencies (eg. Containers page). + return useMemo( + () => [selectionState, setSelectionState], + [selectionState, setSelectionState], + ); +} + const filterByUpdateAvailable = atomWithHash( "filter-update-available", false, @@ -531,46 +815,37 @@ export function useDashboardPreferences() { }; } -export function useUserTargetPermissions(user_target: Types.UserTarget) { - const permissions = useRead("ListUserTargetPermissions", { - user_target, - }).data; - const allResources = useAllResources(); - const perms: (Types.Permission & { name: string })[] = []; - for (const [resource_type, resources] of Object.entries(allResources)) { - addUserTargetPermissions( - user_target, - permissions, - resource_type as UsableResource, - resources, - perms, - ); - } - return perms; -} - -function addUserTargetPermissions( - user_target: Types.UserTarget, - permissions: Types.Permission[] | undefined, - resource_type: UsableResource, - resources: Types.ResourceListItem[] | undefined, - perms: (Types.Permission & { name: string })[], +/** + * Checks if target operation is cancelling + * by querying relevant Updates on the target. + * If a 'Cancel' operation is more recent than + * an InProgress 'Run' operation, the target + * is cancelling. + */ +export function useIsCancelling( + target: Types.ResourceTarget, + runOp: Types.Operation, + cancelOp: Types.Operation, ) { - resources?.forEach((resource) => { - const perm = permissions?.find( - (p) => - p.resource_target.type === resource_type && - p.resource_target.id === resource.id, - ); - if (perm) { - perms.push({ ...perm, name: resource.name }); - } else { - perms.push({ - user_target, - name: resource.name, - level: Types.PermissionLevel.None, - resource_target: { type: resource_type, id: resource.id }, - }); + const updates = useRead("ListUpdates", { + query: { + "target.type": target.type, + "target.id": target.id, + }, + }).data?.updates; + if (!updates) { + return true; + } + let hasCancel = false; + for (const update of updates) { + if (update.operation === cancelOp) { + hasCancel = true; + } else if ( + update.operation === runOp && + update.status === Types.UpdateStatus.InProgress + ) { + return hasCancel; } - }); + } + return false; } diff --git a/ui/src/lib/socket.tsx b/ui/src/lib/socket.tsx index 3e9769093d..b021ad773b 100644 --- a/ui/src/lib/socket.tsx +++ b/ui/src/lib/socket.tsx @@ -200,14 +200,15 @@ function onUpdate( // Invalidate docker infos if (["Server", "Deployment", "Stack"].includes(update.target.type)) { invalidate( - ["ListDockerContainers"], - ["InspectDockerContainer"], - ["ListDockerNetworks"], - ["InspectDockerNetwork"], - ["ListDockerImages"], - ["InspectDockerImage"], - ["ListDockerVolumes"], - ["InspectDockerVolume"], + ["ListContainers"], + ["ListAllContainers"], + ["InspectContainer"], + ["ListNetworks"], + ["InspectNetwork"], + ["ListImages"], + ["InspectImage"], + ["ListVolumes"], + ["InspectVolume"], ["GetResourceMatchingContainer"], ); } @@ -251,9 +252,9 @@ function onUpdate( ["GetStacksSummary"], ["ListCommonStackExtraArgs"], ["ListComposeProjects"], - ["ListDockerContainers"], - ["ListDockerNetworks"], - ["ListDockerImages"], + ["ListContainers"], + ["ListNetworks"], + ["ListImages"], ["GetStackLog", { stack: update.target.id }], ["SearchStackLog", { stack: update.target.id }], ["GetStack"], @@ -265,10 +266,11 @@ function onUpdate( if (update.target.type === "Deployment") { invalidate( ["ListDeployments"], + ["ListFullDeployments"], ["GetDeploymentsSummary"], - ["ListDockerContainers"], - ["ListDockerNetworks"], - ["ListDockerImages"], + ["ListContainers"], + ["ListNetworks"], + ["ListImages"], ["GetDeployment"], ["GetDeploymentLog", { deployment: update.target.id }], ["SearchDeploymentLog", { deployment: update.target.id }], diff --git a/ui/src/lib/utils.ts b/ui/src/lib/utils.ts index a622bd39d5..e5229120ed 100644 --- a/ui/src/lib/utils.ts +++ b/ui/src/lib/utils.ts @@ -2,6 +2,7 @@ import { UsableResource } from "@/resources"; import { Types } from "komodo_client"; import sanitizeHtml from "sanitize-html"; import ConvertAnsiToHtml from "ansi-to-html"; +import { RowSelectionState } from "@tanstack/react-table"; export function objectKeys(o: T): (keyof T)[] { return Object.keys(o) as (keyof T)[]; @@ -122,6 +123,16 @@ export function hasMinimumPermissions( return true; } +/** + * Returns true if the search term is shorthand for the given resource type keyword, + * eg. term 'cont' matches keyword 'containers'. This lets searches like 'cont my-name' + * scope to containers matching 'my-name'. Requires at least 3 characters so short + * terms like 'on' are still used to search by name. + */ +export function termMatchesTypeKeyword(keyword: string, term: string) { + return term.length >= 3 && keyword.startsWith(term); +} + export function usableResourcePath(resource: UsableResource) { if (resource === "ResourceSync") return "resource-syncs"; return `${resource.toLowerCase()}s`; @@ -169,15 +180,8 @@ export function logToHtml(log: string) { return convert_ansi.toHtml(sanitized); } -export function getUpdateQuery( - target: Types.ResourceTarget, - deployments: Types.DeploymentListItem[] | undefined, -) { - const build_id = - target.type === "Deployment" - ? deployments?.find((d) => d.id === target.id)?.info.build_id - : undefined; - if (build_id) { +export function getUpdateQuery(target: Types.ResourceTarget, buildId?: string) { + if (buildId) { return { $or: [ { @@ -186,7 +190,7 @@ export function getUpdateQuery( }, { "target.type": "Build", - "target.id": build_id, + "target.id": buildId, operation: { $in: [Types.Operation.RunBuild, Types.Operation.CancelBuild], }, @@ -278,3 +282,39 @@ export function listsEqual(a: string[], b: string[]) { } return true; } + +export function setSelectedStateHandler( + state: React.SetStateAction, + selectionState: RowSelectionState, + setSelectedList: (list: string[]) => void, +) { + switch (typeof state) { + case "function": + setSelectedList( + Object.entries(state(selectionState)) + .filter((item) => item[1]) + .map((item) => item[0]), + ); + break; + case "object": + setSelectedList( + Object.entries(state) + .filter((item) => item[1]) + .map((item) => item[0]), + ); + break; + } +} + +export function parseVersion(version: string): Types.Version { + const [major, minor, patch] = version + // In case of 'v2.0.0' fmt + .replaceAll("v", "") + .split(".") + .map(Number); + return { + major, + minor, + patch, + }; +} diff --git a/ui/src/pages/alerts.tsx b/ui/src/pages/alerts.tsx index 471c781d6b..74068e95f4 100644 --- a/ui/src/pages/alerts.tsx +++ b/ui/src/pages/alerts.tsx @@ -14,6 +14,7 @@ import { Page } from "mogh_ui"; import { StatusBadge } from "mogh_ui"; import { ActionIcon, Group, Pagination, Select, Stack } from "@mantine/core"; import { Types } from "komodo_client"; +import { keepPreviousData } from "@tanstack/react-query"; import { useMemo, useState } from "react"; import { useSearchParams } from "react-router-dom"; @@ -51,15 +52,21 @@ export default function Alerts() { [params], ); - const { data: alerts } = useRead("ListAlerts", { - query: { - "target.type": type, - "target.id": id, - "data.type": alertType, - resolved: !open, + const { data: alerts } = useRead( + "ListAlerts", + { + query: { + "target.type": type, + "target.id": id, + "data.type": alertType, + resolved: !open, + }, + page, }, - page, - }); + // Keep the previous rows visible while fetching after a + // query key change (page / filters) to prevent table flashing. + { placeholderData: keepPreviousData }, + ); const alertTypes: string[] = type ? (ALERT_TYPES_BY_RESOURCE[type] ?? FALLBACK_ALERT_TYPES) diff --git a/ui/src/pages/containers.tsx b/ui/src/pages/containers.tsx index 3f61d0c3d2..9428f44e32 100644 --- a/ui/src/pages/containers.tsx +++ b/ui/src/pages/containers.tsx @@ -1,219 +1,283 @@ import ContainerPorts from "@/components/docker/container-ports"; import DockerResourceLink from "@/components/docker/link"; import { containerStateIntention } from "@/lib/color"; -import { useRead } from "@/lib/hooks"; +import { + useDebouncedTermSearch, + useDockerSelectionState, + useRead, + useTagsFilter, +} from "@/lib/hooks"; +import { keepPreviousData } from "@tanstack/react-query"; +import { Types } from "komodo_client"; import { ICONS } from "@/lib/icons"; -import { filterBySplit } from "mogh_ui"; -import { DataTable, SortableHeader, useDebounce } from "mogh_ui"; +import { DataTable, SortableHeader } from "mogh_ui"; import { Page } from "mogh_ui"; import { StatusBadge } from "mogh_ui"; -import { Group, MultiSelect, Stack } from "@mantine/core"; -import { useCallback, useMemo, useState } from "react"; +import { Group, Stack } from "@mantine/core"; +import { useEffect, useMemo, useState } from "react"; import { DividedChildren } from "mogh_ui"; import ResourceLink from "@/resources/link"; import { SearchInput } from "mogh_ui"; +import TagsFilter from "@/components/tags/filter"; +import ResourceMultiSelector from "@/resources/multi-selector"; +import ListPagination from "@/components/list-pagination"; +import DockerBatchExecutions from "@/components/docker/batch-executions"; + +const CONTAINER_SORT_KEYS = Object.values(Types.ContainerSortBy); export default function Containers() { - const [search, setSearch] = useState(""); const [selectedServers, setSelectedServers] = useState([]); - const debouncedSearch = useDebounce(search, 300); + const selectionState = useDockerSelectionState("Container"); - const servers = useRead("ListServers", {}).data; - const serverOptions = useMemo( - () => - servers?.map((server) => ({ - label: server.name, - value: server.id, - })) || [], - [servers], - ); + const [page, setPage] = useState(0); - const serverName = useCallback( - (id: string) => servers?.find((server) => server.id === id)?.name, - [servers], - ); + const { search, setSearch, terms } = useDebouncedTermSearch({ + onUpdate: () => setPage(0), + }); - const _containers = useRead("ListAllDockerContainers", {}).data?.filter( - (container) => - !selectedServers.length || - (container.server_id && selectedServers.includes(container.server_id)), - ); + const tags = useTagsFilter(); - const containers = useMemo( - () => filterBySplit(_containers, debouncedSearch, (item) => item.name), - [_containers, debouncedSearch], - ); + // Server side sort, passed up from the table. + const [sort, setSort] = useState<{ + sort_by?: Types.ContainerSortBy; + sort_desc?: boolean; + }>({}); - return ( - - - - - - + // Set to page 0 whenever any filter or the sort changes, + // otherwise the query can point past the last page and come back empty. + useEffect(() => { + setPage(0); + }, [selectedServers, tags, sort.sort_by, sort.sort_desc]); - ( - - ), - cell: ({ row }) => ( - - ), - }, - { - accessorKey: "server_id", - size: 200, - sortingFn: (a, b) => { - const sa = serverName(a.original.server_id!); - const sb = serverName(b.original.server_id!); + const containers = + useRead( + "ListAllContainers", + { + terms, + servers: selectedServers, + tags, + page, + sort_by: sort.sort_by, + sort_desc: sort.sort_desc, + }, + { + refetchInterval: 15_000, + // Keep the previous rows visible while fetching after a query key + // change (page / sort / search / filters) to prevent table flashing. + placeholderData: keepPreviousData, + }, + ).data ?? []; - if (!sa && !sb) return 0; - if (!sa) return -1; - if (!sb) return 1; + const Table = useMemo(() => { + return ( + { + const sort = sorting.find((s) => + CONTAINER_SORT_KEYS.includes(s.id as Types.ContainerSortBy), + ); + setSort( + sort + ? { + sort_by: sort.id as Types.ContainerSortBy, + sort_desc: sort.desc, + } + : {}, + ); + }} + selectOptions={{ + selectKey: ({ server_id, name }) => `${server_id} ${name}`, + state: selectionState, + }} + columns={[ + { + id: "Name", + accessorKey: "name", + size: 260, + header: ({ column }) => ( + + ), + cell: ({ row }) => ( + + ), + }, + { + id: "Server", + accessorKey: "server_id", + size: 200, + sortingFn: (a, b) => { + const sa = a.original.server_name; + const sb = b.original.server_name; - if (sa > sb) return 1; - else if (sa < sb) return -1; - else return 0; - }, - header: ({ column }) => ( - - ), - cell: ({ row }) => ( - - ), - }, - { - accessorKey: "state", - size: 160, - header: ({ column }) => ( - - ), - cell: ({ row }) => { - const state = row.original?.state; - return ( - - ); - }, + if (!sa && !sb) return 0; + if (!sa) return -1; + if (!sb) return 1; + + if (sa > sb) return 1; + else if (sa < sb) return -1; + else return 0; }, - { - accessorKey: "image", - size: 300, - header: ({ column }) => ( - - ), - cell: ({ row }) => ( - ( + + ), + cell: ({ row }) => ( + + ), + }, + { + id: "State", + accessorKey: "state", + size: 160, + header: ({ column }) => ( + + ), + cell: ({ row }) => { + const state = row.original?.state; + return ( + - ), + ); }, - { - accessorKey: "networks.0", - size: 200, - header: ({ column }) => ( - - ), - cell: ({ row }) => - (row.original.networks?.length ?? 0) > 0 ? ( - - {row.original.networks?.map((network) => ( - - ))} - - ) : ( - row.original.network_mode && ( + }, + { + id: "Image", + accessorKey: "image", + size: 300, + header: ({ column }) => ( + + ), + cell: ({ row }) => ( + + ), + }, + { + id: "Networks", + accessorKey: "networks.0", + size: 200, + header: ({ column }) => ( + + ), + cell: ({ row }) => + (row.original.networks?.length ?? 0) > 0 ? ( + + {row.original.networks?.map((network) => ( - ) - ), - }, - { - accessorKey: "ports.0", - size: 200, - sortingFn: (a, b) => { - const getMinHostPort = (row: typeof a) => { - const ports = row.original.ports ?? []; - if (!ports.length) return Number.POSITIVE_INFINITY; - const nums = ports - .map((p) => p.PublicPort) - .filter((p): p is number => typeof p === "number") - .map((n) => Number(n)); - if (!nums.length || nums.some((n) => Number.isNaN(n))) { - return Number.POSITIVE_INFINITY; - } - return Math.min(...nums); - }; - const pa = getMinHostPort(a); - const pb = getMinHostPort(b); - return pa === pb ? 0 : pa > pb ? 1 : -1; - }, - header: ({ column }) => ( - - ), - cell: ({ row }) => ( - - ), - }, - { - accessorKey: "volumes.0", - size: 200, - header: ({ column }) => ( - - ), - cell: ({ row }) => ( - - {row.original.volumes?.map((volume) => ( - ))} + ) : ( + row.original.network_mode && ( + + ) ), + }, + { + id: "Ports", + accessorKey: "ports.0", + size: 200, + sortingFn: (a, b) => { + const getMinHostPort = (row: typeof a) => { + const ports = row.original.ports ?? []; + if (!ports.length) return Number.POSITIVE_INFINITY; + const nums = ports + .map((p) => p.PublicPort) + .filter((p): p is number => typeof p === "number") + .map((n) => Number(n)); + if (!nums.length || nums.some((n) => Number.isNaN(n))) { + return Number.POSITIVE_INFINITY; + } + return Math.min(...nums); + }; + const pa = getMinHostPort(a); + const pb = getMinHostPort(b); + return pa === pb ? 0 : pa > pb ? 1 : -1; }, - ]} - /> + header: ({ column }) => ( + + ), + cell: ({ row }) => ( + + ), + }, + { + id: "Volumes", + accessorKey: "volumes.0", + size: 200, + header: ({ column }) => ( + + ), + cell: ({ row }) => ( + + {row.original.volumes?.map((volume) => ( + + ))} + + ), + }, + ]} + /> + ); + }, [containers, selectionState]); + + return ( + + + + + + + + + + + + + + + + {Table} ); diff --git a/ui/src/pages/dashboard/active.tsx b/ui/src/pages/dashboard/active.tsx index 84b56faf3e..cbd90c229a 100644 --- a/ui/src/pages/dashboard/active.tsx +++ b/ui/src/pages/dashboard/active.tsx @@ -19,41 +19,89 @@ import { ReactNode } from "react"; export default function DashboardActiveResources() { const stacks = - useRead("ListStacks", {}).data?.filter((stack) => - [ - Types.StackState.Deploying, - Types.StackState.Restarting, - Types.StackState.Removing, - ].includes(stack.info.state), - ) ?? []; + useRead( + "ListStacks", + { + query: { + specific: { + states: [ + Types.StackState.Deploying, + Types.StackState.Restarting, + Types.StackState.Removing, + ], + }, + }, + limit: 0, + }, + { refetchInterval: 10_000 }, + ).data ?? []; const deployments = - useRead("ListDeployments", {}).data?.filter((deployment) => - [ - Types.DeploymentState.Deploying, - Types.DeploymentState.Restarting, - Types.DeploymentState.Removing, - ].includes(deployment.info.state), - ) ?? []; + useRead( + "ListDeployments", + { + query: { + specific: { + states: [ + Types.DeploymentState.Deploying, + Types.DeploymentState.Restarting, + Types.DeploymentState.Removing, + ], + }, + }, + limit: 0, + }, + { refetchInterval: 10_000 }, + ).data ?? []; const builds = - useRead("ListBuilds", {}).data?.filter( - (build) => build.info.state === Types.BuildState.Building, - ) ?? []; + useRead( + "ListBuilds", + { + query: { + specific: { states: [Types.BuildState.Building] }, + }, + limit: 0, + }, + { refetchInterval: 10_000 }, + ).data ?? []; const repos = - useRead("ListRepos", {}).data?.filter((repo) => - [ - Types.RepoState.Building, - Types.RepoState.Cloning, - Types.RepoState.Pulling, - ].includes(repo.info.state), - ) ?? []; + useRead( + "ListRepos", + { + query: { + specific: { + states: [ + Types.RepoState.Building, + Types.RepoState.Cloning, + Types.RepoState.Pulling, + ], + }, + }, + limit: 0, + }, + { refetchInterval: 10_000 }, + ).data ?? []; const procedures = - useRead("ListProcedures", {}).data?.filter( - (procedure) => procedure.info.state === Types.ProcedureState.Running, - ) ?? []; + useRead( + "ListProcedures", + { + query: { + specific: { states: [Types.ProcedureState.Running] }, + }, + limit: 0, + }, + { refetchInterval: 10_000 }, + ).data ?? []; const actions = - useRead("ListActions", {}).data?.filter( - (action) => action.info.state === Types.ActionState.Running, - ) ?? []; + useRead( + "ListActions", + { + query: { + specific: { states: [Types.ActionState.Running] }, + }, + limit: 0, + }, + { refetchInterval: 10_000 }, + ).data ?? []; const globalAutoUpdates = useRead("ListUpdates", { query: { diff --git a/ui/src/pages/dashboard/recents.tsx b/ui/src/pages/dashboard/recents.tsx index 6bd326a2c4..c672e99c40 100644 --- a/ui/src/pages/dashboard/recents.tsx +++ b/ui/src/pages/dashboard/recents.tsx @@ -38,8 +38,12 @@ export default function DashboardRecents() { function RecentRow({ type }: { type: UsableResource }) { const nav = useNavigate(); - const _recents = useUser().data?.recents?.[type]?.slice(0, 6); - const _resources = useRead(`List${type}s`, {}).data; + const _recents = useUser().data?.recents?.[type]?.slice(0, 6) ?? []; + const _resources = useRead( + `List${type}s`, + { query: { names: _recents } }, + { enabled: _recents.length > 0 }, + ).data; const recents = _recents?.filter( (recent) => !_resources?.every((resource) => resource.id !== recent), ); diff --git a/ui/src/pages/dashboard/tables.tsx b/ui/src/pages/dashboard/tables.tsx index b177d8048d..30c4b452c4 100644 --- a/ui/src/pages/dashboard/tables.tsx +++ b/ui/src/pages/dashboard/tables.tsx @@ -1,5 +1,11 @@ import TagsFilter from "@/components/tags/filter"; -import { useFilterResources, useRead } from "@/lib/hooks"; +import { keepPreviousData } from "@tanstack/react-query"; +import { + useDebouncedTermSearch, + useRead, + useTagsFilter, + useTemplatesQueryBehavior, +} from "@/lib/hooks"; import { usableResourcePath } from "@/lib/utils"; import { RequiredResourceComponents, @@ -9,32 +15,40 @@ import { import { ICONS } from "@/lib/icons"; import { Section } from "mogh_ui"; import { Group, Stack, Text } from "@mantine/core"; -import { Types } from "komodo_client"; -import { useState } from "react"; +import { useEffect, useMemo, useState } from "react"; +import ListPagination from "@/components/list-pagination"; import { Link } from "react-router-dom"; import DashboardNoResources from "./no-resources"; import { ShowHideButton } from "mogh_ui"; import { SearchInput } from "mogh_ui"; +import TemplateQuerySelector from "@/components/template-query-selector"; export default function DashboardTables() { - const [search, setSearch] = useState(""); + const { search, setSearch, terms } = useDebouncedTermSearch(); + + const Tables = useMemo( + () => + Object.entries(ResourceComponents).map(([type, RC]) => ( + + )), + [terms], + ); return ( + - {Object.entries(ResourceComponents).map(([type, RC]) => ( - - ))} + {Tables} ); } @@ -42,24 +56,53 @@ export default function DashboardTables() { function TableSection({ type, RC, - search, + terms, }: { type: UsableResource; RC: RequiredResourceComponents; - search?: string; + terms: string[]; }) { - const resources = useRead(`List${type}s`, {}).data; + const [show, setShow] = useState(true); + const tags = useTagsFilter(); + const [templates] = useTemplatesQueryBehavior(); - const filtered = useFilterResources( - resources as Types.ResourceListItem[], - search, - ); + const [page, setPage] = useState(0); + // Server side sort, passed up from the table. + const [sort, setSort] = useState<{ + sort_by?: string; + sort_desc?: boolean; + }>({}); + // Set to page 0 whenever any filter or the sort changes, + // otherwise the query can point past the last page and come back empty. + useEffect(() => { + setPage(0); + }, [terms, tags, templates, sort.sort_by, sort.sort_desc]); - let count = filtered.length; + const resources = + useRead( + `List${type}s`, + { + query: { terms, tags, templates }, + page, + sort_by: sort.sort_by as any, + sort_desc: sort.sort_desc, + }, + { + refetchInterval: 15_000, + // Keep the previous rows visible while fetching after a query key + // change (page / sort / search / filters) to prevent table flashing. + placeholderData: keepPreviousData, + }, + ).data ?? []; - const [show, setShow] = useState(true); + const Table = useMemo( + () => show && , + [resources, show], + ); - if (!count) return; + // Keep the section visible on empty pages past the first, + // so the pagination controls remain available to navigate back. + if (!resources.length && page === 0) return; const Icon = ICONS[type]; @@ -67,6 +110,7 @@ function TableSection({
} + gap="0.2rem" titleNode={ } - actions={} + titleRight={ + + + + + + + } > - {show && } + {Table}
); } diff --git a/ui/src/pages/docker/container/executions.tsx b/ui/src/pages/docker/container/executions.tsx index f30159c33a..a9fa120400 100644 --- a/ui/src/pages/docker/container/executions.tsx +++ b/ui/src/pages/docker/container/executions.tsx @@ -6,7 +6,7 @@ import { Pause, Play, RefreshCcw, Square, Trash } from "lucide-react"; import { useNavigate } from "react-router-dom"; const useContainer = (serverId: string, containerName: string) => { - return useRead("ListDockerContainers", { server: serverId }).data?.find( + return useRead("ListContainers", { server: serverId }).data?.find( (container) => container.name === containerName, ); }; @@ -26,7 +26,7 @@ const DestroyContainer = ({ const destroying = useRead( "GetServerActionState", { server: serverId }, - { refetchInterval: 5000 }, + { refetchInterval: 5_000 }, ).data?.pruning_containers; if (!container) { @@ -61,7 +61,7 @@ const RestartContainer = ({ const action_state = useRead( "GetServerActionState", { server: serverId }, - { refetchInterval: 5000 }, + { refetchInterval: 5_000 }, ).data; if (!container || state !== Types.ContainerStateStatusEnum.Running) { @@ -74,7 +74,7 @@ const RestartContainer = ({ icon={} onConfirm={() => restart({ server: serverId, container: containerName })} disabled={restartPending} - loading={restartPending || action_state?.restarting_containers} + loading={restartPending || !!action_state?.restarting_containers} > Restart @@ -96,7 +96,7 @@ const StartStopContainer = ({ const action_state = useRead( "GetServerActionState", { server: serverId }, - { refetchInterval: 5000 }, + { refetchInterval: 5_000 }, ).data; if (!container) { @@ -109,7 +109,7 @@ const StartStopContainer = ({ icon={} onClick={() => start({ server: serverId, container: containerName })} disabled={startPending} - loading={startPending || action_state?.starting_containers} + loading={startPending || !!action_state?.starting_containers} > Start @@ -122,7 +122,7 @@ const StartStopContainer = ({ icon={} onConfirm={() => stop({ server: serverId, container: containerName })} disabled={stopPending} - loading={stopPending || action_state?.stopping_containers} + loading={stopPending || !!action_state?.stopping_containers} > Stop @@ -146,7 +146,7 @@ const PauseUnpauseContainer = ({ const action_state = useRead( "GetServerActionState", { server: serverId }, - { refetchInterval: 5000 }, + { refetchInterval: 5_000 }, ).data; if (!container) { @@ -159,7 +159,7 @@ const PauseUnpauseContainer = ({ icon={} onClick={() => unpause({ server: serverId, container: containerName })} disabled={unpausePending} - loading={unpausePending || action_state?.unpausing_containers} + loading={unpausePending || !!action_state?.unpausing_containers} > Unpause @@ -172,7 +172,7 @@ const PauseUnpauseContainer = ({ icon={} onConfirm={() => pause({ server: serverId, container: containerName })} disabled={pausePending} - loading={pausePending || action_state?.pausing_containers} + loading={pausePending || !!action_state?.pausing_containers} > Pause diff --git a/ui/src/pages/docker/container/index.tsx b/ui/src/pages/docker/container/index.tsx index 329bf37582..dc1f05bc58 100644 --- a/ui/src/pages/docker/container/index.tsx +++ b/ui/src/pages/docker/container/index.tsx @@ -55,14 +55,14 @@ function ContainerInner({ id: serverId, }); const listContainer = useRead( - "ListDockerContainers", + "ListContainers", { server: serverId, }, { refetchInterval: 10_000 }, ).data?.find((container) => container.name === containerName); const inspect = useRead( - "InspectDockerContainer", + "InspectContainer", { server: serverId, container: containerName, diff --git a/ui/src/pages/docker/image.tsx b/ui/src/pages/docker/image.tsx index 43dc0c21f0..fecb1af702 100644 --- a/ui/src/pages/docker/image.tsx +++ b/ui/src/pages/docker/image.tsx @@ -52,13 +52,13 @@ function ImageInner({ data: image, isPending, isError, - } = useRead("InspectDockerImage", { + } = useRead("InspectImage", { server: serverId, image: imageName, }); const containers = useRead( - "ListDockerContainers", + "ListContainers", { server: serverId, }, @@ -67,7 +67,7 @@ function ImageInner({ !image?.Id ? false : container.image_id === image?.Id, ); - const history = useRead("ListDockerImageHistory", { + const history = useRead("ListImageHistory", { server: serverId, image: imageName, }).data; diff --git a/ui/src/pages/docker/network.tsx b/ui/src/pages/docker/network.tsx index 6bf91a3475..2c5e2b732b 100644 --- a/ui/src/pages/docker/network.tsx +++ b/ui/src/pages/docker/network.tsx @@ -50,7 +50,7 @@ function NetworkInner({ data: network, isPending, isError, - } = useRead("InspectDockerNetwork", { + } = useRead("InspectNetwork", { server: serverId, network: networkName, }); diff --git a/ui/src/pages/docker/volume.tsx b/ui/src/pages/docker/volume.tsx index 12b65496fc..9863cab225 100644 --- a/ui/src/pages/docker/volume.tsx +++ b/ui/src/pages/docker/volume.tsx @@ -49,7 +49,7 @@ function VolumeInner({ data: volume, isPending, isError, - } = useRead("InspectDockerVolume", { + } = useRead("InspectVolume", { server: serverId, volume: volumeName, }); @@ -62,7 +62,7 @@ function VolumeInner({ ); const containers = useRead( - "ListDockerContainers", + "ListContainers", { server: serverId, }, diff --git a/ui/src/pages/resource.tsx b/ui/src/pages/resource.tsx index 4a48f1993b..ef77e51d01 100644 --- a/ui/src/pages/resource.tsx +++ b/ui/src/pages/resource.tsx @@ -1,9 +1,9 @@ import ExportToml from "@/components/export-toml"; import ResourceUpdates from "@/components/updates/resource"; import { + useListItemQuery, usePermissions, usePushRecentlyViewed, - useRead, useResourceParamType, useSetTitle, } from "@/lib/hooks"; @@ -36,7 +36,9 @@ export default function Resource() { function ResourceInner({ type, id }: { type: UsableResource; id: string }) { const RC = ResourceComponents[type]; - const resources = useRead(`List${type}s`, {}).data; + // Same query as `useListItem` under the hood, so they share the cache entry. + // Used to distinguish "still loading" from "resource doesn't exist". + const resources = useListItemQuery(type, id).data; const resource = RC.useListItem(id); const { canCreate, canExecute } = usePermissions({ type, id }); diff --git a/ui/src/pages/resources.tsx b/ui/src/pages/resources.tsx index e3652fcc9b..5c9e5144e0 100644 --- a/ui/src/pages/resources.tsx +++ b/ui/src/pages/resources.tsx @@ -1,56 +1,22 @@ import { useState } from "react"; -import { - useFilterByUpdateAvailable, - useFilterResources, - useRead, - useResourceParamType, - useSetTitle, - useTemplatesQueryBehavior, - useUser, -} from "@/lib/hooks"; +import { useResourceParamType, useSetTitle } from "@/lib/hooks"; import { ResourceComponents, UsableResource } from "@/resources"; import { Types } from "komodo_client"; import { Page } from "mogh_ui"; -import { Group, Stack } from "@mantine/core"; -import { TableSkeleton } from "mogh_ui"; -import TemplateQuerySelector from "@/components/template-query-selector"; -import TagsFilter from "@/components/tags/filter"; +import { Group } from "@mantine/core"; import ResourceNotFound from "@/resources/not-found"; import ExportToml from "@/components/export-toml"; import ServerShowStats from "@/resources/server/show-stats"; -import { SearchInput } from "mogh_ui"; -import { LabelledSwitch } from "mogh_ui"; +import ResourceTable from "@/resources/table"; export default function Resources({ _type }: { _type?: UsableResource }) { - const is_admin = useUser().data?.admin ?? false; - const disable_non_admin_create = - useRead("GetCoreInfo", {}).data?.disable_non_admin_create ?? true; const __type = useResourceParamType()!; const type = _type ? _type : __type; + const name = type === "ResourceSync" ? "Resource Sync" : type; useSetTitle(name + "s"); - const [search, setSearch] = useState(""); - const [filterUpdateAvailable, toggleFilterUpdateAvailable] = - useFilterByUpdateAvailable(); - const query = - type === "Stack" || type === "Deployment" - ? { - query: { - specific: { update_available: filterUpdateAvailable }, - }, - } - : {}; - const [templatesQueryBehavior] = useTemplatesQueryBehavior(); - const resources = useRead(`List${type}s`, query).data; - const templatesFilterFn = - templatesQueryBehavior === Types.TemplatesQueryBehavior.Exclude - ? (resource: Types.ResourceListItem) => !resource.template - : templatesQueryBehavior === Types.TemplatesQueryBehavior.Only - ? (resource: Types.ResourceListItem) => resource.template - : () => true; - const filtered = useFilterResources(resources as any, search).filter( - templatesFilterFn, - ); + + const [query, setQuery] = useState>({}); const RC = ResourceComponents[type]; @@ -58,8 +24,6 @@ export default function Resources({ _type }: { _type?: UsableResource }) { return ; } - const targets = filtered?.map((resource) => ({ type, id: resource.id })); - return ( {type === "Server" && } - +
} > - - - - {(is_admin || !disable_non_admin_create) && } - - - - - {(type === "Stack" || type === "Deployment") && ( - - )} - - - - - - - {filtered ? : } - + ); } diff --git a/ui/src/pages/schedules.tsx b/ui/src/pages/schedules.tsx index 63572b872f..abc05fe87f 100644 --- a/ui/src/pages/schedules.tsx +++ b/ui/src/pages/schedules.tsx @@ -1,13 +1,14 @@ import TagsFilter from "@/components/tags/filter"; import TableTags from "@/components/tags/table"; +import ListPagination from "@/components/list-pagination"; import { + useDebouncedTermSearch, usePermissions, useRead, useSetTitle, - useTags, + useTagsFilter, useWrite, } from "@/lib/hooks"; -import { filterBySplit } from "mogh_ui"; import { UsableResource } from "@/resources"; import ResourceLink from "@/resources/link"; import { ICONS } from "@/lib/icons"; @@ -16,14 +17,52 @@ import { Page } from "mogh_ui"; import { SearchInput } from "mogh_ui"; import { Group, Stack, Switch } from "@mantine/core"; import { notifications } from "@mantine/notifications"; -import { useState } from "react"; +import { keepPreviousData } from "@tanstack/react-query"; +import { Types } from "komodo_client"; +import { useEffect, useState } from "react"; + +const SCHEDULE_SORT_KEYS = Object.values(Types.ScheduleSortBy); export default function Schedules() { useSetTitle("Schedules"); - const [search, setSearch] = useState(""); - const { tags } = useTags(); - const schedules = useRead("ListSchedules", { tags }).data; - const filtered = filterBySplit(schedules ?? [], search, (item) => item.name); + + const [page, setPage] = useState(0); + + const { search, setSearch, terms } = useDebouncedTermSearch({ + onUpdate: () => setPage(0), + }); + + const tags = useTagsFilter(); + + // Server side sort, passed up from the table. + const [sort, setSort] = useState<{ + sort_by?: Types.ScheduleSortBy; + sort_desc?: boolean; + }>({}); + + // Set to page 0 whenever any filter or the sort changes, + // otherwise the query can point past the last page and come back empty. + useEffect(() => { + setPage(0); + }, [terms, tags, sort.sort_by, sort.sort_desc]); + + const schedules = + useRead( + "ListSchedules", + { + tags, + terms, + page, + sort_by: sort.sort_by, + sort_desc: sort.sort_desc, + }, + { + refetchInterval: 15_000, + // Keep the previous rows visible while fetching after a query key + // change (page / sort / search / filters) to prevent table flashing. + placeholderData: keepPreviousData, + }, + ).data ?? []; return ( + { + const sort = sorting.find((s) => + SCHEDULE_SORT_KEYS.includes(s.id as Types.ScheduleSortBy), + ); + setSort( + sort + ? { + sort_by: sort.id as Types.ScheduleSortBy, + sort_desc: sort.desc, + } + : {}, + ); + }} columns={[ { size: 200, + id: "Name", accessorKey: "name", header: ({ column }) => ( @@ -56,6 +115,7 @@ export default function Schedules() { }, { size: 200, + id: "Schedule", accessorKey: "schedule", header: ({ column }) => ( @@ -63,22 +123,11 @@ export default function Schedules() { }, { size: 200, + id: "NextRun", accessorKey: "next_scheduled_run", header: ({ column }) => ( ), - sortingFn: (a, b) => { - const sa = a.original.next_scheduled_run; - const sb = b.original.next_scheduled_run; - - if (!sa && !sb) return 0; - if (!sa) return 1; - if (!sb) return -1; - - if (sa > sb) return 1; - else if (sa < sb) return -1; - else return 0; - }, cell: ({ row }) => row.original.next_scheduled_run ? new Date(row.original.next_scheduled_run).toLocaleString() @@ -86,6 +135,7 @@ export default function Schedules() { }, { size: 100, + id: "Enabled", accessorKey: "enabled", header: ({ column }) => ( diff --git a/ui/src/pages/settings/providers/delete.tsx b/ui/src/pages/settings/providers/delete.tsx index aadab9aa85..0e7b4f6ae2 100644 --- a/ui/src/pages/settings/providers/delete.tsx +++ b/ui/src/pages/settings/providers/delete.tsx @@ -7,7 +7,7 @@ export default function DeleteProviderAccount({ type, id, }: { - type: "GitProvider" | "DockerRegistry"; + type: "GitProvider" | "ImageRegistry"; id: string; }) { const invalidate = useInvalidate(); diff --git a/ui/src/pages/settings/providers/from-config.tsx b/ui/src/pages/settings/providers/from-config.tsx index 0b95ba244c..55a4fd26d5 100644 --- a/ui/src/pages/settings/providers/from-config.tsx +++ b/ui/src/pages/settings/providers/from-config.tsx @@ -4,12 +4,12 @@ import { Badge, Center, Group, Loader, Stack, Text } from "@mantine/core"; export default function ProvidersFromConfig({ type, }: { - type: "GitProvider" | "DockerRegistry"; + type: "GitProvider" | "ImageRegistry"; }) { const accounts = useRead( type === "GitProvider" ? "ListGitProvidersFromConfig" - : "ListDockerRegistriesFromConfig", + : "ListImageRegistriesFromConfig", {}, ) .data?.map((provider) => diff --git a/ui/src/pages/settings/providers/index.tsx b/ui/src/pages/settings/providers/index.tsx index ec28d1f60d..f9ab70ccf1 100644 --- a/ui/src/pages/settings/providers/index.tsx +++ b/ui/src/pages/settings/providers/index.tsx @@ -23,12 +23,12 @@ export default function SettingsProviders() { return ( - + ); } -function Providers({ type }: { type: "GitProvider" | "DockerRegistry" }) { +function Providers({ type }: { type: "GitProvider" | "ImageRegistry" }) { const user = useUser().data; const disabled = !user?.admin; useSetTitle("Providers"); @@ -62,9 +62,9 @@ function Providers({ type }: { type: "GitProvider" | "DockerRegistry" }) { return ( <>
) : ( diff --git a/ui/src/pages/settings/providers/new.tsx b/ui/src/pages/settings/providers/new.tsx index d1ee096199..92b0584906 100644 --- a/ui/src/pages/settings/providers/new.tsx +++ b/ui/src/pages/settings/providers/new.tsx @@ -17,7 +17,7 @@ import { ChangeEvent, useState } from "react"; export default function NewProviderAccount({ type, }: { - type: "GitProvider" | "DockerRegistry"; + type: "GitProvider" | "ImageRegistry"; }) { const [opened, { open, close }] = useDisclosure(); const [domain, setDomain] = useState(""); @@ -62,7 +62,7 @@ export default function NewProviderAccount({ ], ]; const accountType = - type === "DockerRegistry" ? "Registry Account" : "Git Account"; + type === "ImageRegistry" ? "Registry Account" : "Git Account"; return ( <> diff --git a/ui/src/pages/stack-service/index.tsx b/ui/src/pages/stack-service/index.tsx index 25a47b8b63..74d26a7451 100644 --- a/ui/src/pages/stack-service/index.tsx +++ b/ui/src/pages/stack-service/index.tsx @@ -60,7 +60,11 @@ function StackServiceInner({ }) { const stack = useStack(stackId); useSetTitle(`${stack?.name} | ${serviceName}`); - const services = useRead("ListStackServices", { stack: stackId }).data; + const services = useRead( + "ListStackServices", + { stack: stackId }, + { refetchInterval: 10_000 }, + ).data; const service = services?.find((s) => s.service === serviceName); const container = service?.container; diff --git a/ui/src/pages/stats.tsx b/ui/src/pages/stats.tsx new file mode 100644 index 0000000000..7a8b23a19a --- /dev/null +++ b/ui/src/pages/stats.tsx @@ -0,0 +1,547 @@ +import TagsFilter from "@/components/tags/filter"; +import { + komodo_client, + useDebouncedTermSearch, + useRead, + useSetTitle, + useTagsFilter, +} from "@/lib/hooks"; +import { ICONS } from "@/lib/icons"; +import ResourceMultiSelector from "@/resources/multi-selector"; +import { useStatsGranularity } from "@/resources/server/hooks"; +import StatsServerTable from "@/resources/server/table/stats"; +import { + Center, + Group, + Loader, + Select, + SimpleGrid, + Stack, + Text, +} from "@mantine/core"; +import { useLocalStorage } from "@mantine/hooks"; +import { keepPreviousData, useQueries } from "@tanstack/react-query"; +import { Types } from "komodo_client"; +import { ChartLine, Download, Upload } from "lucide-react"; +import { + fmtSizeBytes, + fmtUpperCamelcase, + InfoCard, + Page, + SearchInput, + Section, + StatBar, +} from "mogh_ui"; +import { ReactNode, useMemo } from "react"; +import { + CartesianGrid, + Legend, + Line, + LineChart, + ResponsiveContainer, + Tooltip, + XAxis, + YAxis, +} from "recharts"; + +/** + * Fixed categorical palette for per-server chart series (Mantine hues, + * CVD-separation and contrast validated on light and dark surfaces). + * Colors are assigned by position in the full server list, so filtering + * never repaints the remaining servers. + */ +const SERVER_COLORS = [ + "#1c7ed6", // blue.7 + "#e8590c", // orange.8 + "#0ca678", // teal.7 + "#be4bdb", // grape.6 + "#f03e3e", // red.7 + "#1098ad", // cyan.7 + "#7950f2", // violet.6 + "#d6336c", // pink.7 +]; + +/** Beyond this many series, lines become unreadable and colors would repeat. */ +const MAX_CHART_SERVERS = SERVER_COLORS.length; + +type StatType = + | "Cpu" + | "Memory" + | "Disk" + | "Load Average" + | "Network Ingress" + | "Network Egress"; + +const STAT_TYPES: [StatType, ReactNode][] = [ + ["Load Average", ], + ["Cpu", ], + ["Memory", ], + ["Disk", ], + ["Network Ingress", ], + ["Network Egress", ], +]; + +export default function Stats() { + useSetTitle("Stats"); + + // Server ids + const [selectedServers, setSelectedServers] = useLocalStorage({ + key: "stats.selected-servers.v1", + defaultValue: [], + }); + + const { search, setSearch, terms } = useDebouncedTermSearch({ + localStorageKey: "stats.search.v1", + }); + const tags = useTagsFilter(); + + // Server side sort, passed up from the table. + const [sortBy, setSortBy] = useLocalStorage({ + key: "stats.sort.v1", + defaultValue: Types.ServerSortBy.LoadAverage, + }); + + const servers = + useRead( + "ListServers", + { + query: { terms, tags }, + limit: 0, + sort_by: sortBy, + sort_desc: true, + }, + { refetchInterval: 30_000, placeholderData: keepPreviousData }, + ).data ?? []; + + // Stable color per server, keyed on the full (unfiltered) list + // so the assignment survives filtering. + const serverColors = useMemo(() => { + const colors = new Map(); + servers.forEach((server, i) => + colors.set(server.id, SERVER_COLORS[i % SERVER_COLORS.length]), + ); + return colors; + }, [servers]); + + const filteredServers = useMemo( + () => + selectedServers.length + ? servers.filter((server) => selectedServers.includes(server.name)) + : servers, + [servers, selectedServers], + ); + + const Content = useMemo( + () => + servers.length === 0 ? ( + No Servers found. + ) : ( + <> + + +
}> + +
+ + ), + [servers, filteredServers], + ); + + return ( + + + + + + + granularity && setGranularity(granularity as Types.Timelength) + } + data={[ + Types.Timelength.FiveSeconds, + Types.Timelength.FifteenSeconds, + Types.Timelength.ThirtySeconds, + Types.Timelength.OneMinute, + Types.Timelength.FiveMinutes, + Types.Timelength.FifteenMinutes, + Types.Timelength.ThirtyMinutes, + Types.Timelength.OneHour, + Types.Timelength.SixHours, + Types.Timelength.OneDay, + ]} + w={120} + /> + + } + > + + {servers.length > MAX_CHART_SERVERS && ( + + Charting the first {MAX_CHART_SERVERS} of {servers.length} servers. + Filter to specific Servers to compare the others. + + )} + + {STAT_TYPES.map(([type, icon]) => ( + + ))} + + +
+ ); +} + +function MultiServerStatChart({ + type, + icon, + series, + isPending, +}: { + type: StatType; + icon: ReactNode; + series: { name: string; color: string; stats: Types.SystemStatsRecord[] }[]; + isPending: boolean; +}) { + // Timestamps are bucket-aligned to the granularity server side, + // so records merge across servers by exact ts. + const chartData = useMemo(() => { + const byTs = new Map>(); + for (const { name, stats } of series) { + for (const record of stats) { + const entry = byTs.get(record.ts) ?? { date: record.ts }; + entry[name] = getStat(record, type); + byTs.set(record.ts, entry); + } + } + return [...byTs.values()].sort((a, b) => a.date - b.date); + }, [series, type]); + + const minTime = chartData[0]?.date ?? 0; + const maxTime = chartData[chartData.length - 1]?.date ?? 0; + const timeDiff = maxTime - minTime; + + const isNetwork = type === "Network Ingress" || type === "Network Egress"; + const isLoadAvg = type === "Load Average"; + const isPercent = !isNetwork && !isLoadAvg; + + const yTickFormatter = (value: number) => { + if (isNetwork) return fmtSizeBytes(value); + if (isLoadAvg) return value.toFixed(1); + return `${value.toFixed(0)}%`; + }; + + const xTickFormatter = (ts: number) => { + const date = new Date(ts); + if (timeDiff < 2 * 60 * 60 * 1000) { + return date.toLocaleTimeString([], { + hour: "2-digit", + minute: "2-digit", + }); + } + return date.toLocaleString([], { + month: "short", + day: "numeric", + hour: "2-digit", + minute: "2-digit", + }); + }; + + return ( + + + {icon} + + {isLoadAvg ? "Load Average (1m)" : type} + + + {isPending ? ( +
+ +
+ ) : ( + + + + + + + new Date(label).toLocaleString()} + // List the busiest server first. + itemSorter={(item) => -Number(item.value)} + formatter={(value, name) => { + const v = Number(value); + const formatted = isNetwork + ? fmtSizeBytes(v) + : isLoadAvg + ? v.toFixed(2) + : `${v.toFixed(1)}%`; + return [formatted, name]; + }} + /> + {series.map(({ name, color }) => ( + + ))} + + + )} +
+ ); +} + +function getStat(stat: Types.SystemStatsRecord, type: StatType) { + switch (type) { + case "Cpu": + return stat.cpu_perc || 0; + case "Memory": + return stat.mem_total_gb > 0 + ? (100 * stat.mem_used_gb) / stat.mem_total_gb + : 0; + case "Disk": + return stat.disk_total_gb > 0 + ? (100 * stat.disk_used_gb) / stat.disk_total_gb + : 0; + case "Network Ingress": + return stat.network_ingress_bytes || 0; + case "Network Egress": + return stat.network_egress_bytes || 0; + case "Load Average": + return stat.load_average?.one ?? 0; + } +} diff --git a/ui/src/pages/terminals/index.tsx b/ui/src/pages/terminals/index.tsx index fb7a0c5195..b8995d3589 100644 --- a/ui/src/pages/terminals/index.tsx +++ b/ui/src/pages/terminals/index.tsx @@ -1,32 +1,68 @@ import TerminalTargetLink from "@/pages/terminals/target-link"; -import { useRead, useSetTitle } from "@/lib/hooks"; +import ListPagination from "@/components/list-pagination"; +import { useDebouncedTermSearch, useRead, useSetTitle } from "@/lib/hooks"; import { ICONS } from "@/lib/icons"; import { terminalLink } from "@/lib/utils"; -import { - DataTable, - fmtDateWithMinutes, - SortableHeader, - filterBySplit, -} from "mogh_ui"; +import { DataTable, fmtDateWithMinutes, SortableHeader } from "mogh_ui"; import { Page } from "mogh_ui"; import { Group, Stack, Text } from "@mantine/core"; -import { useState } from "react"; +import { keepPreviousData } from "@tanstack/react-query"; +import { Types } from "komodo_client"; +import { useEffect, useState } from "react"; import { Link } from "react-router-dom"; import DeleteTerminal from "./delete"; import BatchDeleteAllTerminals from "./batch-delete"; import NewTerminal from "./new"; import { SearchInput } from "mogh_ui"; +const TERMINAL_SORT_KEYS = Object.values(Types.TerminalSortBy); + export default function Terminals() { useSetTitle("Terminals"); - const [search, setSearch] = useState(""); - const { data: terminals, refetch, isPending } = useRead("ListTerminals", {}); - const filtered = filterBySplit(terminals ?? [], search, (item) => item.name); + + const [page, setPage] = useState(0); + + const { search, setSearch, terms } = useDebouncedTermSearch({ + onUpdate: () => setPage(0), + }); + + // Server side sort, passed up from the table. + const [sort, setSort] = useState<{ + sort_by?: Types.TerminalSortBy; + sort_desc?: boolean; + }>({}); + + // Set to page 0 whenever the search or the sort changes, + // otherwise the query can point past the last page and come back empty. + useEffect(() => { + setPage(0); + }, [terms, sort.sort_by, sort.sort_desc]); + + const { + data: terminals, + refetch, + isPending, + } = useRead( + "ListTerminals", + { + terms, + page, + sort_by: sort.sort_by, + sort_desc: sort.sort_desc, + }, + { + refetchInterval: 15_000, + // Keep the previous rows visible while fetching after a query key + // change (page / sort / search) to prevent table flashing. + placeholderData: keepPreviousData, + }, + ); + return ( @@ -37,15 +73,37 @@ export default function Terminals() { noTerminals={!terminals?.length} /> - + + + + { + const sort = sorting.find((s) => + TERMINAL_SORT_KEYS.includes(s.id as Types.TerminalSortBy), + ); + setSort( + sort + ? { + sort_by: sort.id as Types.TerminalSortBy, + sort_desc: sort.desc, + } + : {}, + ); + }} columns={[ { + id: "Name", accessorKey: "name", header: ({ column }) => ( @@ -66,6 +124,7 @@ export default function Terminals() { }, { size: 200, + id: "Target", accessorKey: "target", header: ({ column }) => ( @@ -75,6 +134,7 @@ export default function Terminals() { ), }, { + id: "Command", accessorKey: "command", header: ({ column }) => ( @@ -87,7 +147,8 @@ export default function Terminals() { }, { size: 100, - accessorKey: "size", + id: "Size", + accessorKey: "stored_size_kb", header: ({ column }) => ( ), @@ -102,6 +163,7 @@ export default function Terminals() { ), }, { + id: "Created", accessorKey: "created_at", header: ({ column }) => ( diff --git a/ui/src/pages/terminals/new.tsx b/ui/src/pages/terminals/new.tsx index 65e79b3464..db2276e7fb 100644 --- a/ui/src/pages/terminals/new.tsx +++ b/ui/src/pages/terminals/new.tsx @@ -236,7 +236,11 @@ function CreateServerTerminal({ close: () => void; }) { const nav = useNavigate(); - const firstServer = (useRead("ListServers", {}).data ?? [])[0]?.id ?? ""; + const firstServer = + (useRead("ListServers", { + query: { specific: { states: [Types.ServerState.Ok] } }, + limit: 1, + }).data ?? [])[0]?.id ?? ""; const [server, _setServer] = useState(firstServer); const [changed, setChanged] = useState(false); const setServer = (server: string) => { @@ -292,7 +296,11 @@ function CreateContainerTerminal({ close: () => void; }) { const nav = useNavigate(); - const firstServer = (useRead("ListServers", {}).data ?? [])[0]?.id ?? ""; + const firstServer = + (useRead("ListServers", { + query: { specific: { states: [Types.ServerState.Ok] } }, + limit: 1, + }).data ?? [])[0]?.id ?? ""; const [params, _setParams] = useState({ server: firstServer, container: "" }); const [changed, setChanged] = useState(false); const setParams = (params: { server: string; container: string }) => { @@ -368,7 +376,15 @@ function CreateStackServiceTerminal({ close: () => void; }) { const nav = useNavigate(); - const firstStack = (useRead("ListStacks", {}).data ?? [])[0]?.id ?? ""; + const firstStack = + (useRead("ListStacks", { + query: { + specific: { + states: [Types.StackState.Running, Types.StackState.Unhealthy], + }, + }, + limit: 1, + }).data ?? [])[0]?.id ?? ""; const [params, _setParams] = useState({ stack: firstStack, service: "" }); const [changed, setChanged] = useState(false); const setParams = (params: { stack: string; service: string }) => { @@ -445,7 +461,10 @@ function CreateDeploymentTerminal({ }) { const nav = useNavigate(); const firstDeployment = - (useRead("ListDeployments", {}).data ?? [])[0]?.id ?? ""; + (useRead("ListDeployments", { + query: { specific: { states: [Types.DeploymentState.Running] } }, + limit: 1, + }).data ?? [])[0]?.id ?? ""; const [deployment, _setDeployment] = useState(firstDeployment); const [changed, setChanged] = useState(false); const setDeployment = (deployment: string) => { diff --git a/ui/src/pages/updates.tsx b/ui/src/pages/updates.tsx index 779488715b..603b34665f 100644 --- a/ui/src/pages/updates.tsx +++ b/ui/src/pages/updates.tsx @@ -22,6 +22,7 @@ import { Text, } from "@mantine/core"; import { Types } from "komodo_client"; +import { keepPreviousData } from "@tanstack/react-query"; import { useState } from "react"; import { useSearchParams } from "react-router-dom"; @@ -37,14 +38,20 @@ export default function Updates() { operation: params.get("operation") as Types.Operation | null, }; - const { data: updates } = useRead("ListUpdates", { - query: { - "target.type": type ?? undefined, - "target.id": id ?? undefined, - operation: operation ?? undefined, + const { data: updates } = useRead( + "ListUpdates", + { + query: { + "target.type": type ?? undefined, + "target.id": id ?? undefined, + operation: operation ?? undefined, + }, + page, }, - page, - }); + // Keep the previous rows visible while fetching after a + // query key change (page / filters) to prevent table flashing. + { placeholderData: keepPreviousData }, + ); return ( 0; + const { mutateAsync: run, isPending: runPending } = useExecute("RunAction"); + const { mutateAsync: cancel, isPending: cancelPending } = + useExecute("CancelAction"); + const action = useAction(id); + const cancelling = useIsCancelling( + { type: "Action", id }, + Types.Operation.RunAction, + Types.Operation.CancelAction, + ); + + if (!action) { + return null; + } + + if (running) { + return ( + } + onClick={() => cancel({ action: id })} + loading={cancelPending || cancelling} + > + Cancel Run + + ); + } else { + return ( + } + confirmText={action.name} + onConfirm={() => run({ action: id, args: {} })} + loading={runPending} + > + Run Action + + ); + } +} diff --git a/ui/src/resources/action/index.tsx b/ui/src/resources/action/index.tsx index 584725277b..3a85b79c43 100644 --- a/ui/src/resources/action/index.tsx +++ b/ui/src/resources/action/index.tsx @@ -1,5 +1,5 @@ import { actionStateIntention } from "@/lib/color"; -import { useExecute, useRead } from "@/lib/hooks"; +import { useListItem, useRead } from "@/lib/hooks"; import { ICONS } from "@/lib/icons"; import { RequiredResourceComponents } from ".."; import { Types } from "komodo_client"; @@ -8,17 +8,19 @@ import { Badge, Group, Popover, Text } from "@mantine/core"; import { Clock } from "lucide-react"; import { useDisclosure } from "@mantine/hooks"; import { updateLogToHtml } from "@/lib/utils"; -import ConfirmModalWithDisable from "@/components/confirm-modal-with-disable"; import NewResource from "@/resources/new"; import ActionConfig from "./config"; import ActionTable from "./table"; import ResourceHeader from "../header"; -import BatchExecutions from "@/components/batch-executions"; - -export function useAction(id: string | undefined, useName?: boolean) { - return useRead("ListActions", {}).data?.find((r) => - useName ? r.name === id : r.id === id, - ); +import BatchExecutions from "@/resources/batch-executions"; +import { RunAction } from "./executions"; + +export function useAction( + id: string | undefined, + useName?: boolean, + refetchInterval?: number | false, +) { + return useListItem("Action", id, useName, refetchInterval); } export function useFullAction(id: string) { @@ -28,9 +30,11 @@ export function useFullAction(id: string) { export const ActionComponents: RequiredResourceComponents< Types.ActionConfig, {}, - Types.ActionListItemInfo + Types.ActionListItemInfo, + Types.ActionQuerySpecifics > = { - useList: () => useRead("ListActions", {}).data, + useList: (query, limit, page) => + useRead("ListActions", { query, limit, page }).data, useListItem: useAction, useFull: useFullAction, @@ -73,8 +77,7 @@ export const ActionComponents: RequiredResourceComponents< Table: ActionTable, Icon: ({ id, size = "1rem", noColor }) => { - const state = useRead("ListActions", {}).data?.find((r) => r.id === id) - ?.info.state; + const state = useAction(id)?.info.state; const color = noColor ? undefined : state && hexColorByIntention(actionStateIntention(state)); @@ -148,33 +151,7 @@ export const ActionComponents: RequiredResourceComponents< }, Executions: { - RunAction: ({ id }) => { - const running = - (useRead( - "GetActionActionState", - { action: id }, - { refetchInterval: 5000 }, - ).data?.running ?? 0) > 0; - const { mutateAsync, isPending } = useExecute("RunAction"); - const action = useAction(id); - - if (!action) { - return null; - } - - return ( - } - confirmText={action.name} - onConfirm={async () => { - await mutateAsync({ action: id, args: {} }); - }} - loading={running || isPending} - > - {running ? "Running" : "Run Action"} - - ); - }, + RunAction, }, Config: ActionConfig, diff --git a/ui/src/resources/action/table.tsx b/ui/src/resources/action/table.tsx index 91fa2d9193..18288201b6 100644 --- a/ui/src/resources/action/table.tsx +++ b/ui/src/resources/action/table.tsx @@ -1,29 +1,44 @@ import { Types } from "komodo_client"; import { DataTable, SortableHeader } from "mogh_ui"; -import { useSelectedResources } from "@/lib/hooks"; +import { useResourceSelectionState } from "@/lib/hooks"; import { ActionComponents } from "."; import TableTags from "@/components/tags/table"; import { BoxProps } from "@mantine/core"; import ResourceLink from "@/resources/link"; +const SORT_KEYS = ["Name", "State", "NextRun"]; + export default function ActionTable({ resources, + onServerSort, ...boxProps }: { resources: Types.ActionListItem[]; + /** When provided, sorting is handled server side, + * and sort updates are passed to this callback. */ + onServerSort?: (sort: { sort_by?: string; sort_desc?: boolean }) => void; } & BoxProps) { - const [_, setSelectedResources] = useSelectedResources("Action"); + const selectionState = useResourceSelectionState("Action"); return ( { + const sort = sorting.find((s) => SORT_KEYS.includes(s.id)); + onServerSort(sort ? { sort_by: sort.id, sort_desc: sort.desc } : {}); + }) + } tableKey="actions-table" data={resources} selectOptions={{ selectKey: ({ name }) => name, - onSelect: setSelectedResources, + state: selectionState, }} columns={[ { + id: "Name", accessorKey: "name", header: ({ column }) => ( @@ -33,6 +48,7 @@ export default function ActionTable({ ), }, { + id: "State", accessorKey: "info.state", header: ({ column }) => ( @@ -40,6 +56,7 @@ export default function ActionTable({ cell: ({ row }) => , }, { + id: "NextRun", accessorKey: "info.next_scheduled_run", header: ({ column }) => ( diff --git a/ui/src/resources/alerter/config/resources.tsx b/ui/src/resources/alerter/config/resources.tsx index 2002967bbf..0384ed313c 100644 --- a/ui/src/resources/alerter/config/resources.tsx +++ b/ui/src/resources/alerter/config/resources.tsx @@ -22,7 +22,7 @@ export default function AlerterConfigResources({ disabled: boolean; blacklist: boolean; }) { - const resourcesMap = useAllResources(); + const resourcesMap = useAllResources(undefined, 0); const allResources = [ ...(resourcesMap.Server?.map((server) => { return { diff --git a/ui/src/resources/alerter/index.tsx b/ui/src/resources/alerter/index.tsx index c56c633a71..f1dc9dbf63 100644 --- a/ui/src/resources/alerter/index.tsx +++ b/ui/src/resources/alerter/index.tsx @@ -1,4 +1,4 @@ -import { useExecute, useRead } from "@/lib/hooks"; +import { useExecute, useListItem, useRead } from "@/lib/hooks"; import { ICONS } from "@/lib/icons"; import { RequiredResourceComponents } from ".."; import { Types } from "komodo_client"; @@ -7,13 +7,15 @@ import AlerterTable from "./table"; import ResourceHeader from "../header"; import AlerterConfig from "./config"; import { hexColorByIntention } from "mogh_ui"; -import BatchExecutions from "@/components/batch-executions"; +import BatchExecutions from "@/resources/batch-executions"; import { ConfirmButton } from "mogh_ui"; -export function useAlerter(id: string | undefined, useName?: boolean) { - return useRead("ListAlerters", {}).data?.find((r) => - useName ? r.name === id : r.id === id, - ); +export function useAlerter( + id: string | undefined, + useName?: boolean, + refetchInterval?: number | false, +) { + return useListItem("Alerter", id, useName, refetchInterval); } export function useFullAlerter(id: string) { @@ -24,9 +26,11 @@ export function useFullAlerter(id: string) { export const AlerterComponents: RequiredResourceComponents< Types.AlerterConfig, undefined, - Types.AlerterListItemInfo + Types.AlerterListItemInfo, + Types.AlerterQuerySpecifics > = { - useList: () => useRead("ListAlerters", {}).data, + useList: (query, limit, page) => + useRead("ListAlerters", { query, limit, page }).data, useListItem: useAlerter, useFull: useFullAlerter, @@ -55,8 +59,7 @@ export const AlerterComponents: RequiredResourceComponents< Table: AlerterTable, Icon: ({ id, size = "1rem", noColor }) => { - const enabled = useRead("ListAlerters", {}).data?.find((r) => r.id === id) - ?.info.enabled; + const enabled = useAlerter(id)?.info.enabled; const color = enabled === undefined || noColor ? undefined diff --git a/ui/src/resources/alerter/table.tsx b/ui/src/resources/alerter/table.tsx index 392ed43d01..9d334e7f66 100644 --- a/ui/src/resources/alerter/table.tsx +++ b/ui/src/resources/alerter/table.tsx @@ -1,4 +1,4 @@ -import { useSelectedResources } from "@/lib/hooks"; +import { useResourceSelectionState } from "@/lib/hooks"; import { DataTable, SortableHeader } from "mogh_ui"; import { Badge, BoxProps } from "@mantine/core"; import { Types } from "komodo_client"; @@ -6,22 +6,44 @@ import ResourceLink from "@/resources/link"; import TableTags from "@/components/tags/table"; import { StatusBadge } from "mogh_ui"; +const SORT_KEYS = ["Name", "Type", "Enabled"]; + export default function AlerterTable({ resources, + onServerSort, ...boxProps -}: { resources: Types.AlerterListItem[] } & BoxProps) { - const [_, setSelectedResources] = useSelectedResources("Alerter"); +}: { + resources: Types.AlerterListItem[]; + /** When provided, sorting is handled server side, + * and sort updates are passed to this callback. */ + onServerSort?: (sort: { + sort_by?: string; + sort_desc?: boolean; + }) => void; +} & BoxProps) { + const selectionState = useResourceSelectionState("Alerter"); return ( { + const sort = sorting.find((s) => SORT_KEYS.includes(s.id)); + onServerSort( + sort ? { sort_by: sort.id, sort_desc: sort.desc } : {}, + ); + }) + } tableKey="alerters" data={resources} selectOptions={{ selectKey: ({ name }) => name, - onSelect: setSelectedResources, + state: selectionState, }} columns={[ { + id: "Name", accessorKey: "name", header: ({ column }) => ( @@ -31,6 +53,7 @@ export default function AlerterTable({ ), }, { + id: "Type", accessorKey: "info.endpoint_type", header: ({ column }) => ( @@ -40,6 +63,7 @@ export default function AlerterTable({ ), }, { + id: "Enabled", accessorKey: "info.enabled", header: ({ column }) => ( diff --git a/ui/src/components/batch-executions.tsx b/ui/src/resources/batch-executions.tsx similarity index 100% rename from ui/src/components/batch-executions.tsx rename to ui/src/resources/batch-executions.tsx diff --git a/ui/src/resources/build/executions.tsx b/ui/src/resources/build/executions.tsx index 6894b0c714..ae62513937 100644 --- a/ui/src/resources/build/executions.tsx +++ b/ui/src/resources/build/executions.tsx @@ -1,7 +1,10 @@ import { Types } from "komodo_client"; -import { useExecute, usePermissions, useRead } from "@/lib/hooks"; -import { useBuilder } from "../builder"; -import { useBuild } from "."; +import { + useExecute, + useIsCancelling, + usePermissions, + useRead, +} from "@/lib/hooks"; import { ConfirmButton } from "mogh_ui"; import { ICONS } from "@/lib/icons"; @@ -12,45 +15,28 @@ export function RunBuild({ id }: { id: string }) { { build: id }, { refetchInterval: 5_000 }, ).data?.building; - const updates = useRead("ListUpdates", { - query: { - "target.type": "Build", - "target.id": id, - }, - }).data; - const { mutate: runBuild, isPending: runPending } = useExecute("RunBuild"); - const { mutate: cancelBuild, isPending: cancelPending } = + const { mutate: run, isPending: runPending } = useExecute("RunBuild"); + const { mutate: cancel, isPending: cancelPending } = useExecute("CancelBuild"); - const build = useBuild(id); - const builder = useBuilder(build?.info.builder_id); - const canCancel = builder?.info.builder_type === "Aws"; + const cancelling = useIsCancelling( + { type: "Build", id }, + Types.Operation.RunBuild, + Types.Operation.CancelBuild, + ); // make sure hidden without perms. - // not usually necessary, but this button also used in deployment actions. + // not usually necessary as execution area hidden without execute + // on the resource, but this button is also used in deployment executions. if (!canExecute) return null; - // updates come in in descending order, so 'find' will find latest update matching operation - const latestBuild = updates?.updates.find( - (u) => u.operation === Types.Operation.RunBuild, - ); - const latestCancel = updates?.updates.find( - (u) => u.operation === Types.Operation.CancelBuild, - ); - const cancelDisabled = - !canCancel || - cancelPending || - (latestCancel && latestBuild - ? latestCancel!.start_ts > latestBuild!.start_ts - : false); - - if (building && canCancel) { + if (building) { return ( } - onClick={() => cancelBuild({ build: id })} - disabled={cancelDisabled} + onClick={() => cancel({ build: id })} + loading={cancelPending || cancelling} > Cancel Build @@ -59,9 +45,8 @@ export function RunBuild({ id }: { id: string }) { return ( } - loading={runPending || building} - onClick={() => runBuild({ build: id })} - disabled={runPending || building} + onClick={() => run({ build: id })} + loading={runPending} > Build diff --git a/ui/src/resources/build/index.tsx b/ui/src/resources/build/index.tsx index 3909d80426..00c9810bef 100644 --- a/ui/src/resources/build/index.tsx +++ b/ui/src/resources/build/index.tsx @@ -1,5 +1,11 @@ import { buildStateIntention } from "@/lib/color"; -import { useInvalidate, usePermissions, useRead, useWrite } from "@/lib/hooks"; +import { + useInvalidate, + useListItem, + usePermissions, + useRead, + useWrite, +} from "@/lib/hooks"; import { ICONS } from "@/lib/icons"; import { RequiredResourceComponents } from ".."; import { Types } from "komodo_client"; @@ -15,15 +21,17 @@ import ResourceLink from "../link"; import FileSource from "@/components/file-source"; import HashCompare from "@/components/hash-compare"; import ResourceHeader from "../header"; -import BatchExecutions from "@/components/batch-executions"; +import BatchExecutions from "@/resources/batch-executions"; import { useState } from "react"; import ResourceSelector from "../selector"; import { hexColorByIntention } from "mogh_ui"; -export function useBuild(id: string | undefined, useName?: boolean) { - return useRead("ListBuilds", {}).data?.find((r) => - useName ? r.name === id : r.id === id, - ); +export function useBuild( + id: string | undefined, + useName?: boolean, + refetchInterval?: number | false, +) { + return useListItem("Build", id, useName, refetchInterval); } export function useFullBuild(id: string) { @@ -33,9 +41,11 @@ export function useFullBuild(id: string) { export const BuildComponents: RequiredResourceComponents< Types.BuildConfig, Types.BuildInfo, - Types.BuildListItemInfo + Types.BuildListItemInfo, + Types.BuildQuerySpecifics > = { - useList: () => useRead("ListBuilds", {}).data, + useList: (query, limit, page) => + useRead("ListBuilds", { query, limit, page }).data, useListItem: useBuild, useFull: useFullBuild, @@ -69,12 +79,15 @@ export const BuildComponents: RequiredResourceComponents< Description: () => <>Build container images., - New: ({ builderId: _builderId }) => { + New: ({ builderId: _builderId, repoId }) => { const [builderId, setBuilderId] = useState(""); return ( type="Build" - config={() => ({ builder_id: _builderId ?? builderId })} + config={() => ({ + builder_id: _builderId ?? builderId, + linked_repo: repoId, + })} extraInputs={ !_builderId && ( { - const state = useRead("ListBuilds", {}).data?.find((r) => r.id === id)?.info - .state; + const state = useBuild(id)?.info.state; const color = noColor ? undefined : state && hexColorByIntention(buildStateIntention(state)); diff --git a/ui/src/resources/build/table.tsx b/ui/src/resources/build/table.tsx index 106bbb8213..a082a2367d 100644 --- a/ui/src/resources/build/table.tsx +++ b/ui/src/resources/build/table.tsx @@ -1,4 +1,4 @@ -import { useSelectedResources } from "@/lib/hooks"; +import { useResourceSelectionState } from "@/lib/hooks"; import { DataTable, SortableHeader } from "mogh_ui"; import { Types } from "komodo_client"; import { fmtVersion } from "@/lib/formatting"; @@ -8,28 +8,48 @@ import { BoxProps } from "@mantine/core"; import ResourceLink from "@/resources/link"; import FileSource from "@/components/file-source"; +const SORT_KEYS = ["Name", "Source", "State"]; + export default function BuildTable({ resources, + onServerSort, ...boxProps }: { resources: Types.BuildListItem[]; + /** When provided, sorting is handled server side, + * and sort updates are passed to this callback. */ + onServerSort?: (sort: { + sort_by?: string; + sort_desc?: boolean; + }) => void; } & BoxProps) { - const [_, setSelectedResources] = useSelectedResources("Build"); + const selectionState = useResourceSelectionState("Build"); return ( { + const sort = sorting.find((s) => SORT_KEYS.includes(s.id)); + onServerSort( + sort ? { sort_by: sort.id, sort_desc: sort.desc } : {}, + ); + }) + } tableKey="builds" data={resources} selectOptions={{ selectKey: ({ name }) => name, - onSelect: setSelectedResources, + state: selectionState, }} columns={[ { header: ({ column }) => ( ), + id: "Name", accessorKey: "name", cell: ({ row }) => , size: 200, @@ -38,6 +58,7 @@ export default function BuildTable({ header: ({ column }) => ( ), + id: "Source", accessorKey: "info.repo", cell: ({ row }) => , size: 200, @@ -48,6 +69,7 @@ export default function BuildTable({ size: 120, }, { + id: "State", accessorKey: "info.state", header: ({ column }) => ( diff --git a/ui/src/resources/build/tabs.tsx b/ui/src/resources/build/tabs.tsx index ea1265c183..9eb70015c2 100644 --- a/ui/src/resources/build/tabs.tsx +++ b/ui/src/resources/build/tabs.tsx @@ -1,19 +1,15 @@ import { buildStateIntention } from "@/lib/color"; import { useRead } from "@/lib/hooks"; -import { - MobileFriendlyTabsSelector, - TabNoContent, -} from "mogh_ui"; +import { MobileFriendlyTabsSelector, TabNoContent } from "mogh_ui"; import { Tabs } from "@mantine/core"; import { useLocalStorage } from "@mantine/hooks"; import { useMemo } from "react"; import { useBuild } from "."; import BuildConfig from "./config"; import { Section } from "mogh_ui"; -import DeploymentTable from "../deployment/table"; import BuildInfo from "./info"; import { ICONS } from "@/lib/icons"; -import { ResourceComponents } from ".."; +import ResourceTable from "../table"; type BuildTabsView = "Config" | "Info" | "Resources"; @@ -25,10 +21,11 @@ export default function BuildTabs({ id }: { id: string }) { const info = useBuild(id)?.info; - const deployments = useRead("ListDeployments", {}).data?.filter( - (deployment) => deployment.info.build_id === id, - ); - const deploymentsDisabled = (deployments?.length || 0) === 0; + const deploymentsDisabled = + (useRead("ListDeployments", { + query: { specific: { build_ids: [id] } }, + limit: 1, + }).data?.length || 0) === 0; const hasBeenBuilt = !!useRead("ListUpdates", { query: { "target.type": "Build", "target.id": id, operation: "RunBuild" }, @@ -75,11 +72,13 @@ export default function BuildTabs({ id }: { id: string }) { break; case "Resources": View = ( -
} - > - +
+
); break; diff --git a/ui/src/resources/builder/config/server.tsx b/ui/src/resources/builder/config/server.tsx index d033190e61..4e1cbad1b7 100644 --- a/ui/src/resources/builder/config/server.tsx +++ b/ui/src/resources/builder/config/server.tsx @@ -1,11 +1,11 @@ import { usePermissions, useRead, useWrite } from "@/lib/hooks"; -import ResourceLink from "@/resources/link"; import ResourceSelector from "@/resources/selector"; import { Config } from "mogh_ui"; import { ConfigItem } from "mogh_ui"; -import { Group } from "@mantine/core"; +import { ActionIcon, Button, Group } from "@mantine/core"; import { useLocalStorage } from "@mantine/hooks"; import { Types } from "komodo_client"; +import { ICONS } from "@/lib/icons"; export default function ServerBuilderConfig({ id }: { id: string }) { const { canWrite } = usePermissions({ type: "Builder", id }); @@ -36,33 +36,71 @@ export default function ServerBuilderConfig({ id }: { id: string }) { label: "Server", labelHidden: true, fields: { - server_id: (serverId, set) => { + server_ids: (serverIds, set) => { return ( - Server: - + {serverIds?.map((serverId, index) => { + return ( + + + set({ + server_ids: [ + ...serverIds.map((id, i) => + i === index ? server_id : id, + ), + ], + }) + } + disabled={disabled} + targetProps={{ w: "90%", maw: "" }} + clearable={false} /> + {!disabled && ( + + + set({ + server_ids: [ + ...serverIds?.filter( + (_, i) => i !== index, + ), + ], + }) + } + /> + + )} - ) : ( - "Select Server" - ) - } - description="Select the Server to build on." - > - set({ server_id })} - disabled={disabled} - clearable - /> + ); + })} + {!disabled && ( + + )} ); }, diff --git a/ui/src/resources/builder/index.tsx b/ui/src/resources/builder/index.tsx index 9ca55a67d9..584f0ad99e 100644 --- a/ui/src/resources/builder/index.tsx +++ b/ui/src/resources/builder/index.tsx @@ -1,22 +1,23 @@ -import { useRead } from "@/lib/hooks"; +import { useListItem, useRead } from "@/lib/hooks"; import { ICONS } from "@/lib/icons"; import { RequiredResourceComponents } from ".."; import { Types } from "komodo_client"; import ResourceLink from "@/resources/link"; import NewResource from "@/resources/new"; import BuilderTable from "./table"; -import { useServer } from "../server"; import { serverStateIntention } from "@/lib/color"; import ResourceHeader from "../header"; import BuilderConfig from "./config"; -import BatchExecutions from "@/components/batch-executions"; +import BatchExecutions from "@/resources/batch-executions"; import { useState } from "react"; -import { Select } from "@mantine/core"; +import { Group, Select } from "@mantine/core"; -export function useBuilder(id: string | undefined, useName?: boolean) { - return useRead("ListBuilders", {}).data?.find((r) => - useName ? r.name === id : r.id === id, - ); +export function useBuilder( + id: string | undefined, + useName?: boolean, + refetchInterval?: number | false, +) { + return useListItem("Builder", id, useName, refetchInterval); } export function useFullBuilder(id: string) { @@ -27,9 +28,11 @@ export function useFullBuilder(id: string) { export const BuilderComponents: RequiredResourceComponents< Types.BuilderConfig, undefined, - Types.BuilderListItemInfo + Types.BuilderListItemInfo, + Types.BuilderQuerySpecifics > = { - useList: () => useRead("ListBuilders", {}).data, + useList: (query, limit, page) => + useRead("ListBuilders", { query, limit, page }).data, useListItem: useBuilder, useFull: useFullBuilder, @@ -77,20 +80,35 @@ export const BuilderComponents: RequiredResourceComponents< ResourcePageHeader: ({ id }) => { const builder = useBuilder(id); - const server = useServer( - builder?.info.builder_type === "Server" - ? builder.info.instance_type - : undefined, - ); + const configured = + (builder?.info.builder_type === "Server" && + builder.info.instance_type?.split(",").map((id) => id.trim())) || + []; + const servers = useRead( + "ListServers", + { + query: { names: configured }, + }, + { refetchInterval: 15_000 }, + ).data?.filter((s) => configured.includes(s.id)); const coreVersion = useRead("GetVersion", {}).data?.version; - const intent = server?.info.state - ? serverStateIntention( - server.info.state, - !!coreVersion && - !!server.info.version && - coreVersion !== server.info.version, - ) - : "Neutral"; + const intent = !servers?.length + ? "Neutral" + : servers.length === 1 + ? serverStateIntention( + servers[0].info.state, + !!coreVersion && + !!servers[0].info.version && + coreVersion !== servers[0].info.version, + ) + : // All servers connected + servers.every((s) => s.info.state === Types.ServerState.Ok) + ? "Good" + : // No servers connected + servers.every((s) => s.info.state !== Types.ServerState.Ok) + ? "Critical" + : // Some servers connected + "Warning"; return ( + ) : builder?.info.builder_type === "Server" ? ( + + {builder?.info.instance_type + ?.split(",") + .slice(0, 3) + .map((id) => ( + + ))} + ) : undefined } /> diff --git a/ui/src/resources/builder/instance-type.tsx b/ui/src/resources/builder/instance-type.tsx index ee02cabeff..a1dac466a5 100644 --- a/ui/src/resources/builder/instance-type.tsx +++ b/ui/src/resources/builder/instance-type.tsx @@ -2,14 +2,17 @@ import { Bot } from "lucide-react"; import { useBuilder } from "."; import ResourceLink from "../link"; import { Group } from "@mantine/core"; +import { DividedChildren } from "mogh_ui"; export default function BuilderInstanceType({ id }: { id: string }) { let info = useBuilder(id)?.info; if (info?.builder_type === "Server") { return ( - info.instance_type && ( - - ) + + {info.instance_type?.split(",").map((id) => ( + + ))} + ); } else { return ( diff --git a/ui/src/resources/builder/table.tsx b/ui/src/resources/builder/table.tsx index 641cd4cf81..52dfd5dfe3 100644 --- a/ui/src/resources/builder/table.tsx +++ b/ui/src/resources/builder/table.tsx @@ -1,4 +1,4 @@ -import { useSelectedResources } from "@/lib/hooks"; +import { useResourceSelectionState } from "@/lib/hooks"; import { DataTable, SortableHeader } from "mogh_ui"; import { BoxProps } from "@mantine/core"; import { Types } from "komodo_client"; @@ -6,24 +6,44 @@ import ResourceLink from "@/resources/link"; import TableTags from "@/components/tags/table"; import BuilderInstanceType from "./instance-type"; +const SORT_KEYS = ["Name", "Provider", "InstanceType"]; + export default function BuilderTable({ resources, + onServerSort, ...boxProps }: { resources: Types.BuilderListItem[]; + /** When provided, sorting is handled server side, + * and sort updates are passed to this callback. */ + onServerSort?: (sort: { + sort_by?: string; + sort_desc?: boolean; + }) => void; } & BoxProps) { - const [_, setSelectedResources] = useSelectedResources("Builder"); + const selectionState = useResourceSelectionState("Builder"); return ( { + const sort = sorting.find((s) => SORT_KEYS.includes(s.id)); + onServerSort( + sort ? { sort_by: sort.id, sort_desc: sort.desc } : {}, + ); + }) + } tableKey="builders" data={resources} selectOptions={{ selectKey: ({ name }) => name, - onSelect: setSelectedResources, + state: selectionState, }} columns={[ { + id: "Name", accessorKey: "name", header: ({ column }) => ( @@ -33,12 +53,14 @@ export default function BuilderTable({ ), }, { + id: "Provider", accessorKey: "info.builder_type", header: ({ column }) => ( ), }, { + id: "InstanceType", accessorKey: "info.instance_type", header: ({ column }) => ( diff --git a/ui/src/resources/deployment/config/image.tsx b/ui/src/resources/deployment/config/image.tsx index 643a1df8f4..6f2a2f5db6 100644 --- a/ui/src/resources/deployment/config/image.tsx +++ b/ui/src/resources/deployment/config/image.tsx @@ -14,6 +14,7 @@ import { import { Types } from "komodo_client"; import { ChevronsUpDown } from "lucide-react"; import { fmtDate, useSearchCombobox } from "mogh_ui"; +import { parseVersion } from "@/lib/utils"; export interface DeploymentImageConfigProps { image: Types.DeploymentImage | undefined; @@ -64,7 +65,7 @@ export default function DeploymentImageConfig({ + onSelect={(version) => { setUpdate({ image: { ...image, @@ -73,8 +74,8 @@ export default function DeploymentImageConfig({ version, }, }, - }) - } + }); + }} disabled={disabled} /> @@ -134,7 +135,7 @@ function BuildVersionSelector({ }) { const versions = useRead( "ListBuildVersions", - { build: buildId! }, + { build: buildId!, limit: 50 }, { enabled: !!buildId }, ).data; @@ -148,7 +149,12 @@ function BuildVersionSelector({ { + onOptionSubmit={(value) => { + if (value === "Latest") { + onSelect({ major: 0, minor: 0, patch: 0 }); + } else { + onSelect(parseVersion(value)); + } combobox.closeDropdown(); }} width={250} @@ -177,16 +183,11 @@ function BuildVersionSelector({ }} /> - onSelect({ major: 0, minor: 0, patch: 0 })} - > - Latest - + Latest {filtered.map((v) => { const version = fmtVersion(v.version); return ( - + {version} {fmtDate(new Date(v.ts))} diff --git a/ui/src/resources/deployment/config/index.tsx b/ui/src/resources/deployment/config/index.tsx index 4aa24a0c32..ef3c49e8c8 100644 --- a/ui/src/resources/deployment/config/index.tsx +++ b/ui/src/resources/deployment/config/index.tsx @@ -28,7 +28,7 @@ export default function DeploymentConfig({ }) { const { canWrite } = usePermissions({ type: "Deployment", id }); const config = useFullDeployment(id)?.config; - const builds = useRead("ListBuilds", {}).data; + const builds = useRead("ListBuilds", { limit: 0 }).data; const globalDisabled = useRead("GetCoreInfo", {}).data?.ui_write_disabled ?? false; const swarmsExist = useRead("ListSwarms", {}).data?.length ? true : false; @@ -99,7 +99,7 @@ export default function DeploymentConfig({ { label: "Server", labelHidden: true, - hidden: !!currSwarmId, + hidden: swarmsExist && !!currSwarmId, fields: { server_id: (server_id, set) => { return ( @@ -298,6 +298,18 @@ export default function DeploymentConfig({ }, ], advanced: [ + { + label: "Custom Name", + labelHidden: true, + fields: { + custom_name: { + placeholder: `Custom ${currServerId ? "service" : "container"} name`, + description: currSwarmId + ? "Optionally set a custom service name, if different from the Deployment name." + : "Optionally set a custom container name, if different from the Deployment name.", + }, + }, + }, { label: "Command", labelHidden: true, diff --git a/ui/src/resources/deployment/config/network.tsx b/ui/src/resources/deployment/config/network.tsx index e7aed6e785..db5d40a7d2 100644 --- a/ui/src/resources/deployment/config/network.tsx +++ b/ui/src/resources/deployment/config/network.tsx @@ -20,7 +20,7 @@ export default function DeploymentNetworkSelector({ }: DeploymentNetworkSelectorProps) { const _networks = useRead( - swarmId ? "ListSwarmNetworks" : "ListDockerNetworks", + swarmId ? "ListSwarmNetworks" : "ListNetworks", { swarm: swarmId, server: serverId! }, { enabled: !!swarmId || !!serverId }, ) diff --git a/ui/src/resources/deployment/executions.tsx b/ui/src/resources/deployment/executions.tsx index 043c53cc36..ff7495eccf 100644 --- a/ui/src/resources/deployment/executions.tsx +++ b/ui/src/resources/deployment/executions.tsx @@ -1,6 +1,6 @@ import { useEffect, useState } from "react"; import { Group, Select, Stack, Text } from "@mantine/core"; -import { useExecute, useRead } from "@/lib/hooks"; +import { useExecute, useListItem, useRead } from "@/lib/hooks"; import { Types } from "komodo_client"; import { parseKeyValue } from "@/lib/utils"; import { useDeployment } from "."; @@ -23,8 +23,7 @@ export function DeployDeployment({ id }: DeploymentId) { const { mutateAsync: deploy, isPending } = useExecute("Deploy"); - const deployments = useRead("ListDeployments", {}).data; - const deployment_item = deployments?.find((d) => d.id === id); + const deployment_item = useListItem("Deployment", id); const deploying = useRead( "GetDeploymentActionState", @@ -93,15 +92,14 @@ export function DestroyDeployment({ id }: DeploymentId) { const { mutateAsync: destroy, isPending } = useExecute("DestroyDeployment"); - const deployments = useRead("ListDeployments", {}).data; - const state = deployments?.find((d) => d.id === id)?.info.state; + const state = useListItem("Deployment", id)?.info.state; const destroying = useRead( "GetDeploymentActionState", { deployment: id, }, - { refetchInterval: 5000 }, + { refetchInterval: 5_000 }, ).data?.destroying; const pending = isPending || destroying; @@ -145,7 +143,7 @@ export function PullDeployment({ id }: DeploymentId) { { deployment: id, }, - { refetchInterval: 5000 }, + { refetchInterval: 5_000 }, ).data; if (!deployment || deployment.info.swarm_id) return null; @@ -172,7 +170,7 @@ export function RestartDeployment({ id }: DeploymentId) { { deployment: id, }, - { refetchInterval: 5000 }, + { refetchInterval: 5_000 }, ).data; if (!deployment || deployment.info.swarm_id) return null; @@ -204,7 +202,7 @@ export function StartStopDeployment({ id }: DeploymentId) { { deployment: id, }, - { refetchInterval: 5000 }, + { refetchInterval: 5_000 }, ).data; if (!deployment || deployment.info.swarm_id) return null; @@ -241,7 +239,7 @@ function StopDeployment({ id }: DeploymentId) { { deployment: id, }, - { refetchInterval: 5000 }, + { refetchInterval: 5_000 }, ).data?.stopping; const pending = isPending || stopping; @@ -317,7 +315,7 @@ export function PauseUnpauseDeployment({ id }: DeploymentId) { { deployment: id, }, - { refetchInterval: 5000 }, + { refetchInterval: 5_000 }, ).data; if (!deployment || deployment.info.swarm_id) return null; diff --git a/ui/src/resources/deployment/index.tsx b/ui/src/resources/deployment/index.tsx index a0c7dedc4e..7cf36031d7 100644 --- a/ui/src/resources/deployment/index.tsx +++ b/ui/src/resources/deployment/index.tsx @@ -1,5 +1,5 @@ import { deploymentStateIntention } from "@/lib/color"; -import { useRead } from "@/lib/hooks"; +import { useListItem, useRead } from "@/lib/hooks"; import { ICONS } from "@/lib/icons"; import { RequiredResourceComponents } from ".."; import { Types } from "komodo_client"; @@ -24,14 +24,16 @@ import SwarmResourceLink from "@/components/swarm/link"; import ContainerPorts from "@/components/docker/container-ports"; import DeploymentUpdateAvailable from "./update-available"; import ResourceHeader from "../header"; -import BatchExecutions from "@/components/batch-executions"; +import BatchExecutions from "@/resources/batch-executions"; import NewResourceWithDeployTarget from "../new-with-deploy-target"; import { hexColorByIntention } from "mogh_ui"; -export function useDeployment(id: string | undefined, useName?: boolean) { - return useRead("ListDeployments", {}).data?.find((r) => - useName ? r.name === id : r.id === id, - ); +export function useDeployment( + id: string | undefined, + useName?: boolean, + refetchInterval?: number | false, +) { + return useListItem("Deployment", id, useName, refetchInterval); } export function useFullDeployment(id: string) { @@ -42,12 +44,23 @@ export function useFullDeployment(id: string) { ).data; } +/** The container / service name for the deployment, + * ie the configured custom name if set, otherwise the deployment name. */ +export function deploymentContainerName( + deployment: Types.DeploymentListItem | undefined, +) { + if (!deployment) return undefined; + return deployment.info.custom_name || deployment.name; +} + export const DeploymentComponents: RequiredResourceComponents< Types.DeploymentConfig, Types.DeploymentInfo, - Types.DeploymentListItemInfo + Types.DeploymentListItemInfo, + Types.DeploymentQuerySpecifics > = { - useList: () => useRead("ListDeployments", {}).data, + useList: (query, limit, page) => + useRead("ListDeployments", { query, limit, page }).data, useListItem: useDeployment, useFull: useFullDeployment, @@ -112,9 +125,7 @@ export const DeploymentComponents: RequiredResourceComponents< Table: DeploymentTable, Icon: ({ id, size = "1rem", noColor }) => { - const info = useRead("ListDeployments", {}).data?.find( - (r) => r.id === id, - )?.info; + const info = useDeployment(id)?.info; const color = noColor ? undefined : info && @@ -191,11 +202,12 @@ export const DeploymentComponents: RequiredResourceComponents< }, DockerResource: ({ id }) => { const deployment = useDeployment(id); + const containerName = deploymentContainerName(deployment); const service = useRead( "ListSwarmServices", { swarm: deployment?.info.swarm_id! }, { enabled: !!deployment?.info.swarm_id }, - ).data?.find((service) => service.Name === deployment?.name); + ).data?.find((service) => service.Name === containerName); if ( !deployment || [ @@ -211,8 +223,8 @@ export const DeploymentComponents: RequiredResourceComponents< {service?.Configs.map((config) => ( ); @@ -246,13 +258,14 @@ export const DeploymentComponents: RequiredResourceComponents< }, Ports: ({ id }) => { const deployment = useDeployment(id); + const containerName = deploymentContainerName(deployment); const container = useRead( - "ListDockerContainers", + "ListContainers", { server: deployment?.info.server_id!, }, { refetchInterval: 10_000, enabled: !!deployment?.info.server_id }, - ).data?.find((container) => container.name === deployment?.name); + ).data?.find((container) => container.name === containerName); if (!container) return null; return ( void; } & BoxProps) { - const swarmName = useResourceName("Swarm"); - const serverName = useResourceName("Server"); - - const [_, setSelectedResources] = useSelectedResources("Deployment"); + const selectionState = useResourceSelectionState("Deployment"); return ( { + const sort = sorting.find((s) => SORT_KEYS.includes(s.id)); + onServerSort( + sort ? { sort_by: sort.id, sort_desc: sort.desc } : {}, + ); + }) + } tableKey="deployments" data={resources} selectOptions={{ selectKey: ({ name }) => name, - onSelect: setSelectedResources, + state: selectionState, }} columns={[ { + id: "Name", accessorKey: "name", header: ({ column }) => ( @@ -43,6 +60,7 @@ export default function DeploymentTable({ size: 200, }, { + id: "Image", accessorKey: "info.image", header: ({ column }) => ( @@ -60,14 +78,15 @@ export default function DeploymentTable({ header: ({ column }) => ( ), + id: "Host", accessorKey: "info.server_id", sortingFn: (a, b) => { const name_a = a.original.info.swarm_id - ? swarmName(a.original.info.swarm_id) - : serverName(a.original.info.server_id); + ? a.original.info.swarm_name + : a.original.info.server_name; const name_b = b.original.info.swarm_id - ? swarmName(b.original.info.swarm_id) - : serverName(b.original.info.server_id); + ? b.original.info.swarm_name + : b.original.info.server_name; if (!name_a && !name_b) return 0; if (!name_a) return 1; @@ -86,6 +105,7 @@ export default function DeploymentTable({ size: 200, }, { + id: "State", accessorKey: "info.state", header: ({ column }) => ( @@ -111,21 +131,17 @@ const Image = ({ buildId: string | undefined; image: string; }) => { - const builds = useRead("ListBuilds", {}).data; if (buildId) { - const build = builds?.find((build) => build.id === buildId); - if (build) { - return ; - } else { - return undefined; - } + return ; } else { - const [img] = image.split(":"); - return ( - - - {img} - - ); + const img = image?.split(":")?.[0]; + if (img) { + return ( + + + {img} + + ); + } } }; diff --git a/ui/src/resources/deployment/tabs.tsx b/ui/src/resources/deployment/tabs.tsx index ad136316a0..37d5e0bdc8 100644 --- a/ui/src/resources/deployment/tabs.tsx +++ b/ui/src/resources/deployment/tabs.tsx @@ -19,7 +19,7 @@ type DeploymentTabsView = "Config" | "Tasks" | "Log" | "Inspect" | "Terminals"; export default function DeploymentTabs({ id }: { id: string }) { const [_view, setView] = useLocalStorage({ - key: "deployment-tabs-v1", + key: `deployment-${id}-tab-v2`, defaultValue: "Config", }); const info = useDeployment(id)?.info; @@ -42,11 +42,12 @@ export default function DeploymentTabs({ id }: { id: string }) { const terminalDisabled = !specificTerminal || containerTerminalsDisabled || - state !== Types.DeploymentState.Running; + state !== Types.DeploymentState.Running || + !!info?.swarm_id; const view = (logsDisabled && _view === "Log") || - (downOrUnknown && _view === "Tasks") || + ((downOrUnknown || !info?.swarm_id) && _view === "Tasks") || (inspectDisabled && _view === "Inspect") || (terminalDisabled && _view === "Terminals") ? "Config" @@ -78,7 +79,6 @@ export default function DeploymentTabs({ id }: { id: string }) { value: "Terminals", icon: ICONS.Terminal, disabled: terminalDisabled, - hidden: !!info?.swarm_id, }, ], [logsDisabled, inspectDisabled, terminalDisabled, info?.swarm_id], diff --git a/ui/src/resources/deployment/tasks.tsx b/ui/src/resources/deployment/tasks.tsx index dc076b6faf..ec6959dd3d 100644 --- a/ui/src/resources/deployment/tasks.tsx +++ b/ui/src/resources/deployment/tasks.tsx @@ -1,9 +1,11 @@ import { useRead } from "@/lib/hooks"; -import { useDeployment } from "."; +import { deploymentContainerName, useDeployment } from "."; import SwarmServiceTasksSection, { SwarmServiceTasksSectionProps, } from "@/pages/swarm/service/tasks"; import { useState } from "react"; +import { Section } from "mogh_ui"; +import { Text } from "@mantine/core"; export interface DeploymentTasksSectionProps extends Omit< Omit, @@ -22,10 +24,18 @@ export default function DeploymentTasksSection({ "ListSwarmServices", { swarm: swarmId! }, { enabled: !!swarmId }, - ).data?.find((service) => service.Name === deployment?.name); + ).data?.find( + (service) => service.Name === deploymentContainerName(deployment), + ); const _search = useState(""); - if (!swarmId || !service) return; + if (!swarmId || !service) { + return ( +
+ Did not find {!swarmId ? "Swarm" : "Swarm Service"} +
+ ); + } return ( - {resource?.description || "Set Description"} + {resource?.description?.split("\n")[0] || "Set Description"} )} diff --git a/ui/src/resources/index.ts b/ui/src/resources/index.ts index 4be6caf769..6cba7577c4 100644 --- a/ui/src/resources/index.ts +++ b/ui/src/resources/index.ts @@ -62,8 +62,13 @@ export interface RequiredResourceComponents< Config = any, Info = any, ListItemInfo = any, + ResourceQuerySpecifics = any, > { - useList: () => Types.ResourceListItem[] | undefined; + useList: ( + query?: Types.ResourceQuery, + limit?: number, + page?: number, + ) => Types.ResourceListItem[] | undefined; useListItem: ( id: string | undefined, useName?: boolean, @@ -88,12 +93,16 @@ export interface RequiredResourceComponents< serverId?: string; builderId?: string; buildId?: string; + repoId?: string; }>; /** A table component to view resource list */ Table: React.FC< { resources: Types.ResourceListItem[]; + /** When provided, sorting is handled server side, + * and sort updates are passed to this callback. */ + onServerSort?: (sort: { sort_by?: string; sort_desc?: boolean }) => void; tableProps?: TableProps; } & BoxProps >; diff --git a/ui/src/resources/multi-selector.tsx b/ui/src/resources/multi-selector.tsx new file mode 100644 index 0000000000..7e815be63b --- /dev/null +++ b/ui/src/resources/multi-selector.tsx @@ -0,0 +1,65 @@ +import { useDebouncedTermSearch, useRead } from "@/lib/hooks"; +import { keepPreviousData } from "@tanstack/react-query"; +import { MultiSelect } from "@mantine/core"; +import { Types } from "komodo_client"; +import { useMemo } from "react"; +import { UsableResource } from "."; +import { fmtResourceType } from "@/lib/formatting"; + +/** + * Multi select over resource names, using server side term search + * with `limit: 10` instead of pulling the full resource list. + */ +export default function ResourceMultiSelector({ + type, + value, + onChange, + templates = Types.TemplatesQueryBehavior.Exclude, + placeholder, +}: { + type: UsableResource; + /** The selected resource names. */ + value: string[]; + onChange: (names: string[]) => void; + templates?: Types.TemplatesQueryBehavior; + placeholder?: string; +}) { + const { search, setSearch, terms } = useDebouncedTermSearch(); + + const resources = + useRead( + `List${type}s`, + { query: { templates, terms }, limit: 10 }, + // Keep the previous options visible while the + // next search fetches, to prevent flashing. + { placeholderData: keepPreviousData }, + ).data ?? []; + + // Keep the selected names in the options, + // even when they don't match the current search. + const data = useMemo(() => { + const names = resources.map((resource) => resource.name); + for (const name of value) { + if (!names.includes(name)) { + names.push(name); + } + } + return names; + }, [resources, value]); + + return ( + options} + /> + ); +} diff --git a/ui/src/resources/new-with-deploy-target.tsx b/ui/src/resources/new-with-deploy-target.tsx index dcb9e0dc5c..298264c436 100644 --- a/ui/src/resources/new-with-deploy-target.tsx +++ b/ui/src/resources/new-with-deploy-target.tsx @@ -10,20 +10,30 @@ export default function NewResourceWithDeployTarget({ type, serverId: _serverId, swarmId: _swarmId, + buildId, + repoId, }: { type: "Stack" | "Deployment"; serverId?: string; swarmId?: string; + /** Deployment only */ + buildId?: string; + /** Stack only */ + repoId?: string; }) { const [serverId, setServerId] = useState(""); const [swarmId, setSwarmId] = useState(""); const showSwarms = !!useRead("ListSwarms", {}).data?.length; return ( - + type={type} config={() => ({ server_id: _serverId ?? serverId, swarm_id: _swarmId ?? swarmId, + image: buildId + ? { type: "Build", params: { build_id: buildId } } + : undefined, + linked_repo: repoId, })} extraInputs={ !(_serverId ?? _swarmId) ? ( diff --git a/ui/src/resources/new.tsx b/ui/src/resources/new.tsx index 5aa33d716d..ba4b949cf3 100644 --- a/ui/src/resources/new.tsx +++ b/ui/src/resources/new.tsx @@ -31,8 +31,9 @@ export default function NewResource({ }: NewResourceProps) { const nav = useNavigate(); const showTemplateSelector = - (useRead(`List${type}s`, {}).data?.filter((r) => r.template).length ?? 0) > - 0; + (useRead(`List${type}s`, { + query: { templates: Types.TemplatesQueryBehavior.Only }, + }).data?.length ?? 0) > 0; const { mutateAsync: create, isPending: createPending } = useWrite( `Create${type}`, diff --git a/ui/src/resources/procedure/config/executions.tsx b/ui/src/resources/procedure/config/executions.tsx index 4161be4810..7f50216431 100644 --- a/ui/src/resources/procedure/config/executions.tsx +++ b/ui/src/resources/procedure/config/executions.tsx @@ -24,6 +24,7 @@ import { Types } from "komodo_client"; import { CheckCircle } from "lucide-react"; import { useEffect, useState } from "react"; import { quote as shellQuote, parse as shellParse } from "shell-quote"; +import TagMultiSelector from "@/components/tags/multi-selector"; export type ExecutionType = Types.Execution["type"]; @@ -52,6 +53,8 @@ export type ProcedureMinExecutionType = Exclude< | "CreateSwarmSecret" | "RotateSwarmSecret" | "RemoveSwarmSecrets" + | "CancelProcedure" + | "CancelAction" >; export type ProcedureExecutionParams = @@ -82,19 +85,30 @@ export const PROCEDURE_EXECUTIONS: ProcedureExecutions = { ), }, BatchRunProcedure: { - params: { pattern: "" }, + params: { pattern: "", tags: [] }, Component: ({ params, setParams, disabled }) => ( - setParams({ pattern })} - disabled={disabled} - useMonaco - monacoLanguage="string_list" - /> + + + setParams({ ...params, pattern }) + } + disabled={disabled} + useMonaco + monacoLanguage="string_list" + /> + setParams({ ...params, tags })} + disabled={disabled} + useName + /> + ), }, // Action @@ -122,19 +136,30 @@ export const PROCEDURE_EXECUTIONS: ProcedureExecutions = { ), }, BatchRunAction: { - params: { pattern: "" }, + params: { pattern: "", tags: [] }, Component: ({ params, setParams, disabled }) => ( - setParams({ pattern })} - disabled={disabled} - useMonaco - monacoLanguage="string_list" - /> + + + setParams({ ...params, pattern }) + } + disabled={disabled} + useMonaco + monacoLanguage="string_list" + /> + setParams({ ...params, tags })} + disabled={disabled} + useName + /> + ), }, // Build @@ -150,19 +175,30 @@ export const PROCEDURE_EXECUTIONS: ProcedureExecutions = { ), }, BatchRunBuild: { - params: { pattern: "" }, + params: { pattern: "", tags: [] }, Component: ({ params, setParams, disabled }) => ( - setParams({ pattern })} - disabled={disabled} - useMonaco - monacoLanguage="string_list" - /> + + + setParams({ ...params, pattern }) + } + disabled={disabled} + useMonaco + monacoLanguage="string_list" + /> + setParams({ ...params, tags })} + disabled={disabled} + useName + /> + ), }, CancelBuild: { @@ -191,19 +227,30 @@ export const PROCEDURE_EXECUTIONS: ProcedureExecutions = { }, }, BatchDeploy: { - params: { pattern: "" }, + params: { pattern: "", tags: [] }, Component: ({ params, setParams, disabled }) => ( - setParams({ pattern })} - disabled={disabled} - useMonaco - monacoLanguage="string_list" - /> + + + setParams({ ...params, pattern }) + } + disabled={disabled} + useMonaco + monacoLanguage="string_list" + /> + setParams({ ...params, tags })} + disabled={disabled} + useName + /> + ), }, PullDeployment: { @@ -284,19 +331,30 @@ export const PROCEDURE_EXECUTIONS: ProcedureExecutions = { ), }, BatchDestroyDeployment: { - params: { pattern: "" }, + params: { pattern: "", tags: [] }, Component: ({ params, setParams, disabled }) => ( - setParams({ pattern })} - disabled={disabled} - useMonaco - monacoLanguage="string_list" - /> + + + setParams({ ...params, pattern }) + } + disabled={disabled} + useMonaco + monacoLanguage="string_list" + /> + setParams({ ...params, tags })} + disabled={disabled} + useName + /> + ), }, // Stack @@ -333,19 +391,30 @@ export const PROCEDURE_EXECUTIONS: ProcedureExecutions = { }, }, BatchDeployStack: { - params: { pattern: "" }, + params: { pattern: "", tags: [] }, Component: ({ params, setParams, disabled }) => ( - setParams({ pattern })} - disabled={disabled} - useMonaco - monacoLanguage="string_list" - /> + + + setParams({ ...params, pattern }) + } + disabled={disabled} + useMonaco + monacoLanguage="string_list" + /> + setParams({ ...params, tags })} + disabled={disabled} + useName + /> + ), }, DeployStackIfChanged: { @@ -360,19 +429,30 @@ export const PROCEDURE_EXECUTIONS: ProcedureExecutions = { ), }, BatchDeployStackIfChanged: { - params: { pattern: "" }, + params: { pattern: "", tags: [] }, Component: ({ params, setParams, disabled }) => ( - setParams({ pattern })} - disabled={disabled} - useMonaco - monacoLanguage="string_list" - /> + + + setParams({ ...params, pattern }) + } + disabled={disabled} + useMonaco + monacoLanguage="string_list" + /> + setParams({ ...params, tags })} + disabled={disabled} + useName + /> + ), }, PullStack: { @@ -408,19 +488,30 @@ export const PROCEDURE_EXECUTIONS: ProcedureExecutions = { }, }, BatchPullStack: { - params: { pattern: "" }, + params: { pattern: "", tags: [] }, Component: ({ params, setParams, disabled }) => ( - setParams({ pattern })} - disabled={disabled} - useMonaco - monacoLanguage="string_list" - /> + + + setParams({ ...params, pattern }) + } + disabled={disabled} + useMonaco + monacoLanguage="string_list" + /> + setParams({ ...params, tags })} + disabled={disabled} + useName + /> + ), }, StartStack: { @@ -616,19 +707,30 @@ export const PROCEDURE_EXECUTIONS: ProcedureExecutions = { }, }, BatchDestroyStack: { - params: { pattern: "" }, + params: { pattern: "", tags: [] }, Component: ({ params, setParams, disabled }) => ( - setParams({ pattern })} - disabled={disabled} - useMonaco - monacoLanguage="string_list" - /> + + + setParams({ ...params, pattern }) + } + disabled={disabled} + useMonaco + monacoLanguage="string_list" + /> + setParams({ ...params, tags })} + disabled={disabled} + useName + /> + ), }, RunStackService: { @@ -907,19 +1009,30 @@ export const PROCEDURE_EXECUTIONS: ProcedureExecutions = { ), }, BatchCloneRepo: { - params: { pattern: "" }, + params: { pattern: "", tags: [] }, Component: ({ params, setParams, disabled }) => ( - setParams({ pattern })} - disabled={disabled} - useMonaco - monacoLanguage="string_list" - /> + + + setParams({ ...params, pattern }) + } + disabled={disabled} + useMonaco + monacoLanguage="string_list" + /> + setParams({ ...params, tags })} + disabled={disabled} + useName + /> + ), }, PullRepo: { @@ -934,19 +1047,30 @@ export const PROCEDURE_EXECUTIONS: ProcedureExecutions = { ), }, BatchPullRepo: { - params: { pattern: "" }, + params: { pattern: "", tags: [] }, Component: ({ params, setParams, disabled }) => ( - setParams({ pattern })} - disabled={disabled} - useMonaco - monacoLanguage="string_list" - /> + + + setParams({ ...params, pattern }) + } + disabled={disabled} + useMonaco + monacoLanguage="string_list" + /> + setParams({ ...params, tags })} + disabled={disabled} + useName + /> + ), }, BuildRepo: { @@ -961,19 +1085,30 @@ export const PROCEDURE_EXECUTIONS: ProcedureExecutions = { ), }, BatchBuildRepo: { - params: { pattern: "" }, + params: { pattern: "", tags: [] }, Component: ({ params, setParams, disabled }) => ( - setParams({ pattern })} - disabled={disabled} - useMonaco - monacoLanguage="string_list" - /> + + + setParams({ ...params, pattern }) + } + disabled={disabled} + useMonaco + monacoLanguage="string_list" + /> + setParams({ ...params, tags })} + disabled={disabled} + useName + /> + ), }, CancelRepoBuild: { diff --git a/ui/src/resources/procedure/executions.tsx b/ui/src/resources/procedure/executions.tsx index a5deb8212c..8d0a5b05a6 100644 --- a/ui/src/resources/procedure/executions.tsx +++ b/ui/src/resources/procedure/executions.tsx @@ -1,26 +1,53 @@ -import { useExecute, useRead } from "@/lib/hooks"; +import { useExecute, useIsCancelling, useRead } from "@/lib/hooks"; import { useProcedure } from "."; import { ICONS } from "@/lib/icons"; import ConfirmModalWithDisable from "@/components/confirm-modal-with-disable"; +import { ConfirmButton } from "mogh_ui"; +import { Types } from "komodo_client"; export function RunProcedure({ id }: { id: string }) { const running = useRead( "GetProcedureActionState", { procedure: id }, - { refetchInterval: 5000 }, + { refetchInterval: 5_000 }, ).data?.running; - const { mutateAsync: run, isPending } = useExecute("RunProcedure"); + const { mutateAsync: run, isPending: runPending } = + useExecute("RunProcedure"); + const { mutateAsync: cancel, isPending: cancelPending } = + useExecute("CancelProcedure"); const procedure = useProcedure(id); - if (!procedure) return null; - return ( - } - onConfirm={() => run({ procedure: id })} - disabled={running || isPending} - loading={running || isPending} - > - {running ? "Running" : "Run Procedure"} - + const cancelling = useIsCancelling( + { type: "Procedure", id }, + Types.Operation.RunProcedure, + Types.Operation.CancelProcedure, ); + + if (!procedure) { + return null; + } + + if (running) { + return ( + } + onClick={() => cancel({ procedure: id })} + loading={cancelPending || cancelling} + > + Cancel Run + + ); + } else { + return ( + } + confirmText={procedure.name} + onConfirm={() => run({ procedure: id })} + loading={runPending} + > + Run Procedure + + ); + } } diff --git a/ui/src/resources/procedure/index.tsx b/ui/src/resources/procedure/index.tsx index bbe75109cc..7755556c2c 100644 --- a/ui/src/resources/procedure/index.tsx +++ b/ui/src/resources/procedure/index.tsx @@ -1,5 +1,5 @@ import { procedureStateIntention } from "@/lib/color"; -import { useRead } from "@/lib/hooks"; +import { useListItem, useRead } from "@/lib/hooks"; import { ICONS } from "@/lib/icons"; import { RequiredResourceComponents } from ".."; import { Types } from "komodo_client"; @@ -9,17 +9,19 @@ import NewResource from "@/resources/new"; import ProcedureConfig from "./config"; import { RunProcedure } from "./executions"; import ResourceHeader from "../header"; -import BatchExecutions from "@/components/batch-executions"; +import BatchExecutions from "@/resources/batch-executions"; import { Badge, Group, Popover, Text } from "@mantine/core"; import { Clock } from "lucide-react"; import { useDisclosure } from "@mantine/hooks"; import { updateLogToHtml } from "@/lib/utils"; import { hexColorByIntention } from "mogh_ui"; -export function useProcedure(id: string | undefined, useName?: boolean) { - return useRead("ListProcedures", {}).data?.find((r) => - useName ? r.name === id : r.id === id, - ); +export function useProcedure( + id: string | undefined, + useName?: boolean, + refetchInterval?: number | false, +) { + return useListItem("Procedure", id, useName, refetchInterval); } export function useFullProcedure(id: string) { @@ -30,9 +32,11 @@ export function useFullProcedure(id: string) { export const ProcedureComponents: RequiredResourceComponents< Types.ProcedureConfig, undefined, - Types.ProcedureListItemInfo + Types.ProcedureListItemInfo, + Types.ProcedureQuerySpecifics > = { - useList: () => useRead("ListProcedures", {}).data, + useList: (query, limit, page) => + useRead("ListProcedures", { query, limit, page }).data, useListItem: useProcedure, useFull: useFullProcedure, @@ -78,8 +82,7 @@ export const ProcedureComponents: RequiredResourceComponents< Table: ProcedureTable, Icon: ({ id, size = "1rem", noColor }) => { - const state = useRead("ListProcedures", {}).data?.find((r) => r.id === id) - ?.info.state; + const state = useProcedure(id)?.info.state; const color = noColor ? undefined : state && hexColorByIntention(procedureStateIntention(state)); diff --git a/ui/src/resources/procedure/table.tsx b/ui/src/resources/procedure/table.tsx index 4f13ac92eb..229480a074 100644 --- a/ui/src/resources/procedure/table.tsx +++ b/ui/src/resources/procedure/table.tsx @@ -1,4 +1,4 @@ -import { useSelectedResources } from "@/lib/hooks"; +import { useResourceSelectionState } from "@/lib/hooks"; import { DataTable, SortableHeader } from "mogh_ui"; import { Types } from "komodo_client"; import ResourceLink from "@/resources/link"; @@ -6,25 +6,45 @@ import { ProcedureComponents } from "."; import TableTags from "@/components/tags/table"; import { BoxProps } from "@mantine/core"; +const SORT_KEYS = ["Name", "State", "NextRun"]; + export default function ProcedureTable({ resources, + onServerSort, ...boxProps }: { resources: Types.ProcedureListItem[]; + /** When provided, sorting is handled server side, + * and sort updates are passed to this callback. */ + onServerSort?: (sort: { + sort_by?: string; + sort_desc?: boolean; + }) => void; } & BoxProps) { - const [_, setSelectedResources] = useSelectedResources("Procedure"); + const selectionState = useResourceSelectionState("Procedure"); return ( { + const sort = sorting.find((s) => SORT_KEYS.includes(s.id)); + onServerSort( + sort ? { sort_by: sort.id, sort_desc: sort.desc } : {}, + ); + }) + } tableKey="procedures" data={resources} selectOptions={{ selectKey: ({ name }) => name, - onSelect: setSelectedResources, + state: selectionState, }} columns={[ { + id: "Name", accessorKey: "name", header: ({ column }) => ( @@ -34,6 +54,7 @@ export default function ProcedureTable({ ), }, { + id: "State", accessorKey: "info.state", header: ({ column }) => ( @@ -41,6 +62,7 @@ export default function ProcedureTable({ cell: ({ row }) => , }, { + id: "NextRun", accessorKey: "info.next_scheduled_run", header: ({ column }) => ( diff --git a/ui/src/resources/repo/executions.tsx b/ui/src/resources/repo/executions.tsx index 77f68f0b3f..741df6bceb 100644 --- a/ui/src/resources/repo/executions.tsx +++ b/ui/src/resources/repo/executions.tsx @@ -10,7 +10,7 @@ export function CloneRepo({ id }: { id: string }) { const cloning = useRead( "GetRepoActionState", { repo: id }, - { refetchInterval: 5000 }, + { refetchInterval: 5_000 }, ).data?.cloning; const info = useRepo(id)?.info; if (!info?.server_id) return null; @@ -34,7 +34,7 @@ export function PullRepo({ id }: { id: string }) { const pulling = useRead( "GetRepoActionState", { repo: id }, - { refetchInterval: 5000 }, + { refetchInterval: 5_000 }, ).data?.pulling; const info = useRepo(id)?.info; if (!info?.server_id) return null; @@ -59,7 +59,7 @@ export function BuildRepo({ id }: { id: string }) { const building = useRead( "GetRepoActionState", { repo: id }, - { refetchInterval: 5000 }, + { refetchInterval: 5_000 }, ).data?.building; const updates = useRead("ListUpdates", { query: { diff --git a/ui/src/resources/repo/index.tsx b/ui/src/resources/repo/index.tsx index d60f4d8173..4da9a196e4 100644 --- a/ui/src/resources/repo/index.tsx +++ b/ui/src/resources/repo/index.tsx @@ -1,5 +1,5 @@ import { repoStateIntention } from "@/lib/color"; -import { useRead } from "@/lib/hooks"; +import { useListItem, useRead } from "@/lib/hooks"; import { ICONS } from "@/lib/icons"; import { RequiredResourceComponents } from "@/resources"; import { Types } from "komodo_client"; @@ -8,7 +8,7 @@ import RepoTable from "./table"; import NewResource from "@/resources/new"; import ResourceHeader from "@/resources/header"; import RepoTabs from "./tabs"; -import BatchExecutions from "@/components/batch-executions"; +import BatchExecutions from "@/resources/batch-executions"; import { BuildRepo, CloneRepo, PullRepo } from "./executions"; import { useServer } from "../server"; import { useBuilder } from "../builder"; @@ -17,10 +17,12 @@ import ResourceLink from "../link"; import RepoLink from "@/components/repo-link"; import { hexColorByIntention } from "mogh_ui"; -export function useRepo(id: string | undefined, useName?: boolean) { - return useRead("ListRepos", {}).data?.find((r) => - useName ? r.name === id : r.id === id, - ); +export function useRepo( + id: string | undefined, + useName?: boolean, + refetchInterval?: number | false, +) { + return useListItem("Repo", id, useName, refetchInterval); } export function useFullRepo(id: string) { @@ -30,9 +32,11 @@ export function useFullRepo(id: string) { export const RepoComponents: RequiredResourceComponents< Types.RepoConfig, Types.RepoInfo, - Types.RepoListItemInfo + Types.RepoListItemInfo, + Types.RepoQuerySpecifics > = { - useList: () => useRead("ListRepos", {}).data, + useList: (query, limit, page) => + useRead("ListRepos", { query, limit, page }).data, useListItem: useRepo, useFull: useFullRepo, @@ -82,8 +86,7 @@ export const RepoComponents: RequiredResourceComponents< Table: RepoTable, Icon: ({ id, size = "1rem", noColor }) => { - const state = useRead("ListRepos", {}).data?.find((r) => r.id === id)?.info - .state; + const state = useRepo(id)?.info.state; const color = noColor ? undefined : state && hexColorByIntention(repoStateIntention(state)); diff --git a/ui/src/resources/repo/resources.tsx b/ui/src/resources/repo/resources.tsx index ebd3b72fa6..805cb6a3e0 100644 --- a/ui/src/resources/repo/resources.tsx +++ b/ui/src/resources/repo/resources.tsx @@ -1,42 +1,41 @@ import { ICONS } from "@/lib/icons"; -import { Types } from "komodo_client"; import { Section, SectionProps } from "mogh_ui"; -import StackTable from "@/resources/stack/table"; -import BuildTable from "../build/table"; -import ResourceSyncTable from "../sync/table"; +import ResourceTable from "../table"; export interface RepoLinkedResourcesSectionProps extends Omit< SectionProps, "children" > { - stacks: Types.StackListItem[]; - builds: Types.BuildListItem[]; - syncs: Types.ResourceSyncListItem[]; + repoId: string; } export default function RepoLinkedResourcesSection({ - stacks, - builds, - syncs, + repoId, ...sectionProps }: RepoLinkedResourcesSectionProps) { return ( -
- {stacks.length ? ( -
}> - -
- ) : null} - {builds.length ? ( -
}> - -
- ) : null} - {syncs.length ? ( -
}> - -
- ) : null} +
+
}> + +
+
}> + +
+
}> + +
); } diff --git a/ui/src/resources/repo/table.tsx b/ui/src/resources/repo/table.tsx index ce2d5ee60a..f86db9ac77 100644 --- a/ui/src/resources/repo/table.tsx +++ b/ui/src/resources/repo/table.tsx @@ -1,5 +1,5 @@ import { Types } from "komodo_client"; -import { useSelectedResources } from "@/lib/hooks"; +import { useResourceSelectionState } from "@/lib/hooks"; import { DataTable, SortableHeader } from "mogh_ui"; import ResourceLink from "@/resources/link"; import { RepoComponents } from "."; @@ -7,26 +7,48 @@ import TableTags from "@/components/tags/table"; import { BoxProps } from "@mantine/core"; import RepoLink from "@/components/repo-link"; +const SORT_KEYS = ["Name", "Repo", "Branch", "State"]; + export default function RepoTable({ resources, + onServerSort, ...boxProps -}: { resources: Types.RepoListItem[] } & BoxProps) { - const [_, setSelectedResources] = useSelectedResources("Repo"); +}: { + resources: Types.RepoListItem[]; + /** When provided, sorting is handled server side, + * and sort updates are passed to this callback. */ + onServerSort?: (sort: { + sort_by?: string; + sort_desc?: boolean; + }) => void; +} & BoxProps) { + const selectionState = useResourceSelectionState("Repo"); return ( { + const sort = sorting.find((s) => SORT_KEYS.includes(s.id)); + onServerSort( + sort ? { sort_by: sort.id, sort_desc: sort.desc } : {}, + ); + }) + } tableKey="repo-table" data={resources} selectOptions={{ selectKey: ({ name }) => name, - onSelect: setSelectedResources, + state: selectionState, }} columns={[ { header: ({ column }) => ( ), + id: "Name", accessorKey: "name", cell: ({ row }) => , size: 200, @@ -35,6 +57,7 @@ export default function RepoTable({ header: ({ column }) => ( ), + id: "Repo", accessorKey: "info.repo", cell: ({ row }) => ( ( ), + id: "Branch", accessorKey: "info.branch", size: 200, }, @@ -55,6 +79,7 @@ export default function RepoTable({ header: ({ column }) => ( ), + id: "State", accessorKey: "info.state", cell: ({ row }) => , size: 120, diff --git a/ui/src/resources/repo/tabs.tsx b/ui/src/resources/repo/tabs.tsx index ff31e3fe45..b9ab925259 100644 --- a/ui/src/resources/repo/tabs.tsx +++ b/ui/src/resources/repo/tabs.tsx @@ -1,10 +1,7 @@ import { useLocalStorage } from "@mantine/hooks"; import { useRepo } from "."; import { useMemo } from "react"; -import { - MobileFriendlyTabsSelector, - TabNoContent, -} from "mogh_ui"; +import { MobileFriendlyTabsSelector, TabNoContent } from "mogh_ui"; import { ICONS } from "@/lib/icons"; import { repoStateIntention } from "@/lib/color"; import { Tabs } from "@mantine/core"; @@ -16,24 +13,27 @@ type RepoTabsView = "Config" | "Resources"; export default function RepoTabs({ id }: { id: string }) { const [view, setView] = useLocalStorage({ - key: "repo-tabs-v1", + key: `repo-${id}-tab-v2`, defaultValue: "Config", }); const info = useRepo(id)?.info; const stacks = - useRead("ListStacks", {}).data?.filter( - (stack) => stack.info.linked_repo === id, - ) ?? []; + useRead("ListStacks", { + query: { specific: { linked_repos: [id] } }, + limit: 1, + }).data ?? []; const noStacks = stacks.length === 0; const builds = - useRead("ListBuilds", {}).data?.filter( - (build) => build.info.linked_repo === id, - ) ?? []; + useRead("ListBuilds", { + query: { specific: { linked_repos: [id] } }, + limit: 1, + }).data ?? []; const noBuilds = builds.length === 0; const syncs = - useRead("ListResourceSyncs", {}).data?.filter( - (sync) => sync.info.linked_repo === id, - ) ?? []; + useRead("ListResourceSyncs", { + query: { specific: { linked_repos: [id] } }, + limit: 1, + }).data ?? []; const noSyncs = syncs.length === 0; const noResources = noStacks && noBuilds && noSyncs; @@ -67,14 +67,7 @@ export default function RepoTabs({ id }: { id: string }) { View = ; break; case "Resources": - View = ( - - ); + View = ; break; } diff --git a/ui/src/resources/selector.tsx b/ui/src/resources/selector.tsx index 7bef4b19a2..f589cc3057 100644 --- a/ui/src/resources/selector.tsx +++ b/ui/src/resources/selector.tsx @@ -11,11 +11,13 @@ import { InputWrapperProps, Text, } from "@mantine/core"; -import { filterBySplit } from "mogh_ui"; import { ChevronsUpDown } from "lucide-react"; import { fmtResourceType } from "@/lib/formatting"; import { ICONS } from "@/lib/icons"; -import { useSearchCombobox } from "mogh_ui"; +import { useDebounce, useSearchCombobox } from "mogh_ui"; +import { keepPreviousData } from "@tanstack/react-query"; +import { useMemo } from "react"; +import { useRead } from "@/lib/hooks"; export interface ResourceSelectorProps extends ComboboxProps { type: UsableResource; @@ -47,35 +49,51 @@ export default function ResourceSelector({ clearable = true, ...comboboxProps }: ResourceSelectorProps) { - const templateFilterFn = - templates === Types.TemplatesQueryBehavior.Exclude - ? (r: Types.ResourceListItem) => !r.template - : templates === Types.TemplatesQueryBehavior.Only - ? (r: Types.ResourceListItem) => r.template - : () => true; - const Components = ResourceComponents[type]; - const resources = Components.useList()?.filter( - (r) => - templateFilterFn(r) && - (!state || (r.info as any).state === state) && - (!excludeIds || r.id === selected || !excludeIds?.includes(r.id)), + const { search, setSearch, combobox } = useSearchCombobox(); + const debouncedSearch = useDebounce(search, 200); + const terms = useMemo( + () => + debouncedSearch + .split(" ") + .map((item) => item.trim()) + .filter((item) => !!item), + [debouncedSearch], ); - const name = resources?.find((r) => r.id === selected)?.name; - const { search, setSearch, combobox } = useSearchCombobox(); + const Components = ResourceComponents[type]; + const selectedResource = Components.useListItem(selected); - const filtered = filterBySplit(resources, search, (item) => item.name).sort( - (a, b) => { - if (a.name > b.name) { - return 1; - } else if (a.name < b.name) { - return -1; - } else { - return 0; - } + const { data: __resources } = useRead( + `List${type}s`, + { + query: { templates, terms }, + limit: 10, }, + // Keep the previous options visible while the + // next search fetches, to prevent flashing. + { placeholderData: keepPreviousData }, ); + const resources = useMemo( + () => + (__resources?.filter( + (r) => + (!state || (r.info as any).state === state) && + (!excludeIds || r.id === selected || !excludeIds?.includes(r.id)), + ) ?? []) as Array>, + [__resources], + ); + + const name = selectedResource?.name; + + if ( + terms.length === 0 && + selectedResource && + resources.every((resource) => resource.id !== selectedResource.id) + ) { + resources.push(selectedResource); + } + const Selector = ( - {filtered.map((resource) => ( + {/* {isFetching && ( +
+ +
+ )} */} + {resources.map((resource) => ( @@ -142,7 +165,7 @@ export default function ResourceSelector({ ))} - {filtered.length === 0 && ( + {resources.length === 0 && ( No results. )}
diff --git a/ui/src/resources/server/docker/containers.tsx b/ui/src/resources/server/docker/containers.tsx index 74c57720d7..004700688b 100644 --- a/ui/src/resources/server/docker/containers.tsx +++ b/ui/src/resources/server/docker/containers.tsx @@ -12,7 +12,7 @@ export default function ServerContainers({ }) { const _search = useServerDockerSearch(); const containers = - useRead("ListDockerContainers", { server: id }, { refetchInterval: 10_000 }) + useRead("ListContainers", { server: id }, { refetchInterval: 10_000 }) .data ?? []; return ( ); } diff --git a/ui/src/resources/server/docker/images.tsx b/ui/src/resources/server/docker/images.tsx index 41da7e5fff..331d5eba47 100644 --- a/ui/src/resources/server/docker/images.tsx +++ b/ui/src/resources/server/docker/images.tsx @@ -1,6 +1,7 @@ import { ReactNode } from "react"; import { useServerDockerSearch } from "."; -import { useRead } from "@/lib/hooks"; +import { useDockerSelectionState, useRead } from "@/lib/hooks"; +import DockerBatchExecutions from "@/components/docker/batch-executions"; import { filterBySplit } from "mogh_ui"; import { fmtSizeBytes, Section } from "mogh_ui"; import { Badge, Group } from "@mantine/core"; @@ -17,28 +18,34 @@ export default function ServerImages({ titleOther: ReactNode; }) { const [search, setSearch] = useServerDockerSearch(); + const selectionState = useDockerSelectionState("Image"); const images = - useRead("ListDockerImages", { server: id }, { refetchInterval: 10_000 }) - .data ?? []; + useRead("ListImages", { server: id }, { refetchInterval: 10_000 }).data ?? + []; const allInUse = images.every((image) => image.in_use); const filtered = filterBySplit(images, search, (image) => image.name); return ( -
+ + {!allInUse && } - - } - > + + + + `${id} ${name}`, + state: selectionState, + }} columns={[ { accessorKey: "name", diff --git a/ui/src/resources/server/docker/networks.tsx b/ui/src/resources/server/docker/networks.tsx index 126958fce3..2b4fb5b827 100644 --- a/ui/src/resources/server/docker/networks.tsx +++ b/ui/src/resources/server/docker/networks.tsx @@ -1,6 +1,7 @@ import { ReactNode } from "react"; import { useServerDockerSearch } from "."; -import { useRead } from "@/lib/hooks"; +import { useDockerSelectionState, useRead } from "@/lib/hooks"; +import DockerBatchExecutions from "@/components/docker/batch-executions"; import { filterBySplit } from "mogh_ui"; import { Section } from "mogh_ui"; import { Prune } from "../executions"; @@ -17,9 +18,10 @@ export default function ServerNetworks({ titleOther: ReactNode; }) { const [search, setSearch] = useServerDockerSearch(); + const selectionState = useDockerSelectionState("Network"); const networks = - useRead("ListDockerNetworks", { server: id }, { refetchInterval: 10_000 }) - .data ?? []; + useRead("ListNetworks", { server: id }, { refetchInterval: 10_000 }).data ?? + []; const allInUse = networks.every((network) => // this ignores networks that come in with no name, but they should all come in with name @@ -37,19 +39,28 @@ export default function ServerNetworks({ ); return ( -
+ + {!allInUse && } - - } - > + + + + `${id} ${name}`, + state: selectionState, + // System networks (and unnamed ones) cannot be deleted. + disableRow: (row) => + !!row.original.name && + !["none", "host", "bridge"].includes(row.original.name), + }} columns={[ { accessorKey: "name", diff --git a/ui/src/resources/server/docker/volumes.tsx b/ui/src/resources/server/docker/volumes.tsx index 8979d25884..146007fcd5 100644 --- a/ui/src/resources/server/docker/volumes.tsx +++ b/ui/src/resources/server/docker/volumes.tsx @@ -1,6 +1,7 @@ import { ReactNode } from "react"; import { useServerDockerSearch } from "."; -import { useRead } from "@/lib/hooks"; +import { useDockerSelectionState, useRead } from "@/lib/hooks"; +import DockerBatchExecutions from "@/components/docker/batch-executions"; import { filterBySplit } from "mogh_ui"; import { Section } from "mogh_ui"; import { Prune } from "../executions"; @@ -17,28 +18,34 @@ export default function ServerVolumes({ titleOther: ReactNode; }) { const [search, setSearch] = useServerDockerSearch(); + const selectionState = useDockerSelectionState("Volume"); const volumes = - useRead("ListDockerVolumes", { server: id }, { refetchInterval: 10_000 }) - .data ?? []; + useRead("ListVolumes", { server: id }, { refetchInterval: 10_000 }).data ?? + []; const allInUse = volumes.every((volume) => volume.in_use); const filtered = filterBySplit(volumes, search, (volume) => volume.name); return ( -
+ + {!allInUse && } - - } - > + + + + `${id} ${name}`, + state: selectionState, + }} columns={[ { accessorKey: "name", diff --git a/ui/src/resources/server/executions.tsx b/ui/src/resources/server/executions.tsx index dad1dc00a4..d91bae6f0a 100644 --- a/ui/src/resources/server/executions.tsx +++ b/ui/src/resources/server/executions.tsx @@ -16,7 +16,7 @@ export const Prune = ({ const action_state = useRead( "GetServerActionState", { server: serverId }, - { refetchInterval: 5000 }, + { refetchInterval: 5_000 }, ).data; const { canExecute } = usePermissions({ type: "Server", id: serverId }); diff --git a/ui/src/resources/server/index.tsx b/ui/src/resources/server/index.tsx index 45743b1d32..6cad56232d 100644 --- a/ui/src/resources/server/index.tsx +++ b/ui/src/resources/server/index.tsx @@ -1,5 +1,5 @@ import { serverStateIntention } from "@/lib/color"; -import { useExecute, useRead } from "@/lib/hooks"; +import { useExecute, useListItem, useRead } from "@/lib/hooks"; import { ICONS } from "@/lib/icons"; import { RequiredResourceComponents } from ".."; import { Types } from "komodo_client"; @@ -17,29 +17,37 @@ import { fmtUpperCamelcase } from "mogh_ui"; import ConfirmModalWithDisable from "@/components/confirm-modal-with-disable"; import ResourceHeader from "../header"; import { useIsServerAvailable } from "./hooks"; -import BatchExecutions from "@/components/batch-executions"; +import BatchExecutions from "@/resources/batch-executions"; import { ServerLoadAverage } from "./stats/current/load-average"; import { ServerRamUsage } from "./stats/current/ram"; import ServerDiskUsage from "./diskUsage"; import ServerCpuUsage from "./stats/current/cpu"; import { HoverError } from "mogh_ui"; -export function useServer(id: string | undefined, useName?: boolean) { - return useRead("ListServers", {}).data?.find((r) => - useName ? r.name === id : r.id === id, - ); +export function useServer( + id: string | undefined, + useName?: boolean, + refetchInterval?: number | false, +) { + return useListItem("Server", id, useName, refetchInterval); } -export function useFullServer(id: string) { - return useRead("GetServer", { server: id }, { refetchInterval: 30_000 }).data; +export function useFullServer(id: string | undefined) { + return useRead( + "GetServer", + { server: id! }, + { refetchInterval: 30_000, enabled: !!id }, + ).data; } export const ServerComponents: RequiredResourceComponents< Types.ServerConfig, Types.ServerInfo, - Types.ServerListItemInfo + Types.ServerListItemInfo, + Types.ServerQuerySpecifics > = { - useList: () => useRead("ListServers", {}).data, + useList: (query, limit, page) => + useRead("ListServers", { query, limit, page }).data, useListItem: useServer, useFull: useFullServer, @@ -96,9 +104,7 @@ export const ServerComponents: RequiredResourceComponents< Icon: ({ id, size = "1rem", noColor }) => { const coreVersion = useRead("GetVersion", {}).data?.version; - const info = useRead("ListServers", {}).data?.find( - (r) => r.id === id, - )?.info; + const info = useServer(id)?.info; const state = info?.state; const color = noColor ? undefined @@ -191,7 +197,6 @@ export const ServerComponents: RequiredResourceComponents< { server: id }, { enabled: isServerAvailable, - refetchInterval: 5000, }, ).data?.core_count ?? 0; return ( @@ -217,7 +222,7 @@ export const ServerComponents: RequiredResourceComponents< { server: id }, { enabled: isServerAvailable, - refetchInterval: 5000, + refetchInterval: 5_000, }, ).data; @@ -244,7 +249,7 @@ export const ServerComponents: RequiredResourceComponents< { server: id }, { enabled: isServerAvailable, - refetchInterval: 5000, + refetchInterval: 5_000, }, ).data; return ( @@ -268,7 +273,7 @@ export const ServerComponents: RequiredResourceComponents< { server: id }, { enabled: isServerAvailable, - refetchInterval: 5000, + refetchInterval: 5_000, }, ).data; const diskTotalGb = stats?.disks.reduce( @@ -308,10 +313,10 @@ export const ServerComponents: RequiredResourceComponents< const starting = useRead( "GetServerActionState", { server: id }, - { refetchInterval: 5000 }, + { refetchInterval: 5_000 }, ).data?.starting_containers; const dontShow = - useRead("ListDockerContainers", { + useRead("ListContainers", { server: id, }).data?.every( (container) => @@ -320,7 +325,7 @@ export const ServerComponents: RequiredResourceComponents< if (dontShow) { return null; } - const pending = isPending || starting; + const pending = isPending || !!starting; return ( server && ( @@ -378,7 +383,7 @@ export const ServerComponents: RequiredResourceComponents< if (dontShow) { return null; } - const pending = isPending || pausing; + const pending = isPending || !!pausing; return ( server && ( @@ -413,7 +418,7 @@ export const ServerComponents: RequiredResourceComponents< if (dontShow) { return null; } - const pending = isPending || unpausing; + const pending = isPending || !!unpausing; return ( server && ( { serverId: string; - stacks: Types.StackListItem[]; - deployments: Types.DeploymentListItem[]; - repos: Types.RepoListItem[]; } export default function ServerHostedResourcesSection({ serverId, - stacks, - deployments, - repos, ...sectionProps }: ServerHostedResourcesSectionProps) { - const isAdmin = useUser().data?.admin ?? false; - const disableNonAdminCreate = - useRead("GetCoreInfo", {}).data?.disable_non_admin_create ?? true; - return (
-
} - actions={ - (isAdmin || !disableNonAdminCreate) && ( - - ) - } - > - +
}> +
-
} - actions={ - (isAdmin || !disableNonAdminCreate) && ( - - ) - } - > - +
}> +
-
} - actions={ - (isAdmin || !disableNonAdminCreate) && ( - - ) - } - > - +
}> +
); diff --git a/ui/src/resources/server/stats/containers.tsx b/ui/src/resources/server/stats/containers.tsx index 16a777ab30..fd261a342c 100644 --- a/ui/src/resources/server/stats/containers.tsx +++ b/ui/src/resources/server/stats/containers.tsx @@ -19,7 +19,7 @@ export default function ServerContainerStats({ id }: { id: string }) { }); const isServerAvailable = useIsServerAvailable(id); const containers = useRead( - "ListDockerContainers", + "ListContainers", { server: id, }, @@ -84,7 +84,7 @@ export default function ServerContainerStats({ id }: { id: string }) { ), cell: ({ row }) => ( - + {row.original.stats?.mem_perc} ({row.original.stats?.mem_usage}) diff --git a/ui/src/resources/server/stats/current/load-average.tsx b/ui/src/resources/server/stats/current/load-average.tsx index d1b50f1d20..a4e947b884 100644 --- a/ui/src/resources/server/stats/current/load-average.tsx +++ b/ui/src/resources/server/stats/current/load-average.tsx @@ -15,11 +15,12 @@ export function ServerLoadAverage({ if (!stats?.load_average) return null; const { one = 0, five = 0, fifteen = 0 } = stats.load_average || {}; const isServerAvailable = useIsServerAvailable(id); - const cores = useRead( + const sysInfo = useRead( "GetSystemInformation", { server: id }, { enabled: isServerAvailable }, - ).data?.core_count; + ).data; + const cores = sysInfo?.logical_core_count || sysInfo?.core_count; const pct = (load: number) => cores && cores > 0 ? Math.min((load / cores) * 100, 100) : undefined; @@ -37,13 +38,13 @@ export function ServerLoadAverage({ {/* CURRENT LOAD */} - + {one.toFixed(2)} {cores && cores > 0 - ? `${(pct(one) ?? 0).toFixed(0)}% of ${cores} cores` + ? `${(pct(one) ?? 0).toFixed(0)}% of ${cores} Cores` : "N/A"} diff --git a/ui/src/resources/server/stats/current/ram.tsx b/ui/src/resources/server/stats/current/ram.tsx index b449617553..21d38382f9 100644 --- a/ui/src/resources/server/stats/current/ram.tsx +++ b/ui/src/resources/server/stats/current/ram.tsx @@ -2,6 +2,7 @@ import { ICONS } from "@/lib/icons"; import { StatBar } from "mogh_ui"; import { Types } from "komodo_client"; import { useFullServer } from "@/resources/server"; +import { Group, Text } from "@mantine/core"; export function ServerRamUsage({ id, @@ -13,16 +14,30 @@ export function ServerRamUsage({ const server = useFullServer(id); const usedRam = stats?.mem_used_gb; const totalRam = stats?.mem_total_gb; + const zfsArc = stats?.mem_zfs_arc_gb ?? 0; + const buffCache = stats?.mem_buff_cache_gb ?? 0; + // = free + reclaimable cache + ARC + const availableRam = + usedRam !== undefined && totalRam !== undefined + ? Math.max(0, totalRam - usedRam) + : undefined; return ( } description={ - usedRam && - totalRam && ( + usedRam !== undefined && + totalRam !== undefined && ( <> - {usedRam?.toFixed(1)} GB of {totalRam?.toFixed(1)} GB{" "} - in use + + Used {usedRam.toFixed(1)} GB of{" "} + {totalRam.toFixed(1)} GB + + + {availableRam?.toFixed(1)} GB available + {zfsArc > 0 && · ZFS ARC {zfsArc.toFixed(1)} GB} + {buffCache > 0 && · Cache {buffCache.toFixed(1)} GB} + ) } diff --git a/ui/src/resources/server/stats/historical.tsx b/ui/src/resources/server/stats/historical.tsx index 1c62675fc3..65e222f7e3 100644 --- a/ui/src/resources/server/stats/historical.tsx +++ b/ui/src/resources/server/stats/historical.tsx @@ -9,6 +9,7 @@ import { ShowHideButton } from "mogh_ui"; import { Center, Group, + lighten, Loader, Select, SimpleGrid, @@ -144,6 +145,34 @@ function StatChart({ { label: "15m", data: fifteen }, ]; } + if (type === "Memory") { + // Stacked GB composition, bottom -> top: Used, then reclaimable bands. + const series: { label: string; data: StatDatapoint[] }[] = [ + { + label: "Used", + data: records.map((s) => ({ date: s.ts, value: s.mem_used_gb ?? 0 })), + }, + ]; + if (records.some((s) => (s.mem_buff_cache_gb ?? 0) > 0)) { + series.push({ + label: "Cache/Buffers", + data: records.map((s) => ({ + date: s.ts, + value: s.mem_buff_cache_gb ?? 0, + })), + }); + } + if (records.some((s) => (s.mem_zfs_arc_gb ?? 0) > 0)) { + series.push({ + label: "ZFS ARC", + data: records.map((s) => ({ + date: s.ts, + value: s.mem_zfs_arc_gb ?? 0, + })), + }); + } + return series; + } const single = records.map((stat) => ({ date: stat.ts, value: getStat(stat, type), @@ -165,6 +194,15 @@ function StatChart({ "15m": hexColorByIntention("Warning")!, }; } + if (type === "Memory") { + // Yellow ramp: base for Used, lighter for the reclaimable bands. + const base = hexColorByIntention("Warning")!; + return { + Used: base, + "Cache/Buffers": lighten(base, 0.3), + "ZFS ARC": lighten(base, 0.55), + }; + } return { [type]: getColor(type) }; }, [type]); @@ -181,10 +219,24 @@ function StatChart({ const isNetwork = type === "Network Ingress" || type === "Network Egress"; const isLoadAvg = type === "Load Average"; + const isMemory = type === "Memory"; + + // Cap Y axis at total RAM so the gap above the stack reads as free. + const memTotal = useMemo( + () => + isMemory + ? (data?.stats ?? []).reduce( + (max, s) => Math.max(max, s.mem_total_gb ?? 0), + 0, + ) + : 0, + [data, isMemory], + ); const yTickFormatter = (value: number) => { if (isNetwork) return fmtSizeBytes(value); if (isLoadAvg) return value.toFixed(1); + if (isMemory) return `${value.toFixed(0)} GB`; return `${value.toFixed(0)}%`; }; @@ -266,8 +318,14 @@ function StatChart({ tick={{ fontSize: 10, fill: "var(--mantine-color-dimmed)" }} axisLine={false} tickLine={false} - width={isNetwork ? 70 : 42} - domain={isNetwork || isLoadAvg ? ["auto", "auto"] : [0, 100]} + width={isNetwork ? 70 : isMemory ? 52 : 42} + domain={ + isNetwork || isLoadAvg + ? ["auto", "auto"] + : isMemory + ? [0, memTotal > 0 ? memTotal : "auto"] + : [0, 100] + } /> new Date(label).toLocaleString()} + // Memory tooltip lists top-of-stack first, matching the chart. + itemSorter={ + isMemory + ? (item) => + -seriesData.findIndex((s) => s.label === item.dataKey) + : undefined + } formatter={(value, name) => { const v = Number(value); const formatted = isNetwork ? fmtSizeBytes(v) : isLoadAvg ? v.toFixed(2) - : `${v.toFixed(1)}%`; + : isMemory + ? `${v.toFixed(2)} GB` + : `${v.toFixed(1)}%`; return [formatted, name]; }} /> - {isLoadAvg && ( + {(isLoadAvg || isMemory) && ( ( + + {[...(payload ?? [])].reverse().map((entry) => ( + + + + {entry.value} + + + ))} + + ) + : undefined + } /> )} {seriesData.map((series) => ( @@ -299,8 +389,15 @@ function StatChart({ key={series.label} type="monotone" dataKey={series.label} + // Stack only for Memory; other charts overlay. + stackId={isMemory ? "mem" : undefined} stroke={colors[series.label]} - fill={`url(#${gradientId(series.label)})`} + fill={ + isMemory + ? colors[series.label] + : `url(#${gradientId(series.label)})` + } + fillOpacity={isMemory ? 0.45 : undefined} strokeWidth={2} dot={false} activeDot={{ r: 3 }} @@ -316,7 +413,6 @@ function StatChart({ function getStat(stat: Types.SystemStatsRecord, type: StatType) { if (type === "Cpu") return stat.cpu_perc || 0; - if (type === "Memory") return (100 * stat.mem_used_gb) / stat.mem_total_gb; if (type === "Disk") return (100 * stat.disk_used_gb) / stat.disk_total_gb; if (type === "Network Ingress") return stat.network_ingress_bytes || 0; if (type === "Network Egress") return stat.network_egress_bytes || 0; @@ -325,7 +421,6 @@ function getStat(stat: Types.SystemStatsRecord, type: StatType) { function getColor(type: StatType) { if (type === "Cpu") return hexColorByIntention("Good")!; - if (type === "Memory") return hexColorByIntention("Warning")!; if (type === "Disk") return hexColorByIntention("Neutral")!; if (type === "Network Ingress") return hexColorByIntention("Good")!; if (type === "Network Egress") return hexColorByIntention("Critical")!; diff --git a/ui/src/resources/server/stats/system-info.tsx b/ui/src/resources/server/stats/system-info.tsx index 81b3189088..9344914550 100644 --- a/ui/src/resources/server/stats/system-info.tsx +++ b/ui/src/resources/server/stats/system-info.tsx @@ -54,10 +54,16 @@ export default function ServerSystemInfo({ header: "CPU", accessorKey: "cpu_brand", }, + { + header: "Arch", + accessorKey: "cpu_arch", + }, { header: "Core Count", - accessorFn: ({ core_count }) => - `${core_count} Core${(core_count || 0) > 1 ? "s" : ""}`, + accessorFn: ({ core_count, logical_core_count }) => + logical_core_count + ? `${logical_core_count} Logical / ${core_count} Physical Core${(core_count || 0) > 1 ? "s" : ""}` + : `${core_count} Physical Core${(core_count || 0) > 1 ? "s" : ""}`, }, { header: "Total Memory", diff --git a/ui/src/resources/server/table/index.tsx b/ui/src/resources/server/table/index.tsx index 292686cb0f..8040fcfeff 100644 --- a/ui/src/resources/server/table/index.tsx +++ b/ui/src/resources/server/table/index.tsx @@ -6,14 +6,30 @@ import StatsServerTable from "./stats"; export default function ServerTable({ resources, + onServerSort, ...boxProps }: { resources: Types.ServerListItem[]; + /** When provided, sorting is handled server side, + * and sort updates are passed to this callback. */ + onServerSort?: (sort: { sort_by?: string; sort_desc?: boolean }) => void; } & BoxProps) { const { preferences } = useDashboardPreferences(); if (preferences.showServerStats) { - return ; + return ( + + ); } else { - return ; + return ( + + ); } } diff --git a/ui/src/resources/server/table/standard.tsx b/ui/src/resources/server/table/standard.tsx index 2aad1f83dd..ec04c451d4 100644 --- a/ui/src/resources/server/table/standard.tsx +++ b/ui/src/resources/server/table/standard.tsx @@ -1,4 +1,4 @@ -import { useRead, useSelectedResources } from "@/lib/hooks"; +import { useRead, useResourceSelectionState } from "@/lib/hooks"; import ResourceLink from "@/resources/link"; import { DataTable, SortableHeader } from "mogh_ui"; import { Types } from "komodo_client"; @@ -7,16 +7,22 @@ import { ServerComponents } from ".."; import TableTags from "@/components/tags/table"; import { BoxProps } from "@mantine/core"; +const SORT_KEYS = ["Name", "Region", "Version", "State"]; + export default function StandardServerTable({ resources, + onServerSort, ...boxProps }: { resources: Types.ServerListItem[]; + /** When provided, sorting is handled server side, + * and sort updates are passed to this callback. */ + onServerSort?: (sort: { sort_by?: string; sort_desc?: boolean }) => void; } & BoxProps) { - const [_, setSelectedResources] = useSelectedResources("Server"); - const deployments = useRead("ListDeployments", {}).data; - const stacks = useRead("ListStacks", {}).data; - const repos = useRead("ListRepos", {}).data; + const selectionState = useResourceSelectionState("Server"); + const deployments = useRead("ListDeployments", { limit: 0 }).data; + const stacks = useRead("ListStacks", { limit: 0 }).data; + const repos = useRead("ListRepos", { limit: 0 }).data; const resourcesCount = useCallback( (id: string) => { return ( @@ -31,15 +37,24 @@ export default function StandardServerTable({ return ( { + const sort = sorting.find((s) => SORT_KEYS.includes(s.id)); + onServerSort(sort ? { sort_by: sort.id, sort_desc: sort.desc } : {}); + }) + } tableKey="standard-server-table" data={resources} selectOptions={{ selectKey: ({ name }) => name, - onSelect: setSelectedResources, + state: selectionState, }} columns={[ { size: 250, + id: "Name", accessorKey: "name", header: ({ column }) => ( @@ -51,6 +66,9 @@ export default function StandardServerTable({ { size: 100, accessorKey: "id", + // The resource count is computed on the client, + // it cannot be sorted server side. + enableSorting: !onServerSort, sortingFn: (a, b) => { const sa = resourcesCount(a.original.id); const sb = resourcesCount(b.original.id); @@ -72,6 +90,7 @@ export default function StandardServerTable({ }, { size: 200, + id: "Region", accessorKey: "info.region", header: ({ column }) => ( @@ -79,6 +98,7 @@ export default function StandardServerTable({ }, { size: 150, + id: "Version", accessorKey: "info.version", header: ({ column }) => ( @@ -87,6 +107,7 @@ export default function StandardServerTable({ }, { size: 150, + id: "State", accessorKey: "info.state", header: ({ column }) => ( diff --git a/ui/src/resources/server/table/stats.tsx b/ui/src/resources/server/table/stats.tsx index 221cd091d1..961fe65ead 100644 --- a/ui/src/resources/server/table/stats.tsx +++ b/ui/src/resources/server/table/stats.tsx @@ -1,4 +1,4 @@ -import { useSelectedResources } from "@/lib/hooks"; +import { useResourceSelectionState } from "@/lib/hooks"; import ResourceLink from "@/resources/link"; import { DataTable, fmtRateBytes, SortableHeader } from "mogh_ui"; import { BoxProps, Group, Text } from "@mantine/core"; @@ -8,61 +8,130 @@ import { StatCell } from "mogh_ui"; import ServerVersion from "@/resources/server/version"; import ServerDiskUsage from "../diskUsage"; +const SORT_KEYS = ["Name", "Cpu", "Memory", "Disk", "LoadAverage", "Network"]; + export default function StatsServerTable({ resources, + onServerSort, + disableSorting, ...boxProps }: { resources: Types.ServerListItem[]; + /** When provided, sorting is handled server side, + * and sort updates are passed to this callback. */ + onServerSort?: (sort: { sort_by?: string; sort_desc?: boolean }) => void; + disableSorting?: boolean; } & BoxProps) { - const [_, setSelectedResources] = useSelectedResources("Server"); + const selectionState = useResourceSelectionState("Server"); return ( { + const sort = sorting.find((s) => SORT_KEYS.includes(s.id)); + onServerSort( + sort + ? { + sort_by: sort.id, + // Descending first + sort_desc: !sort.desc, + } + : {}, + ); + }) + } tableKey="monitoring-server-table" data={resources} selectOptions={{ selectKey: ({ name }) => name, - onSelect: setSelectedResources, + state: selectionState, }} columns={[ { - size: 250, + id: "Name", accessorKey: "name", header: ({ column }) => ( - + ), cell: ({ row }) => ( ), }, { - header: "CPU", - size: 200, - cell: ({ row }) => , + id: "Cpu", + accessorKey: "id", + // The stats are fetched per row on the client, + // sorting is only available server side. + enableSorting: !!onServerSort, + header: ({ column }) => ( + + ), + cell: ({ row }) => , }, { - header: "Memory", - size: 200, - cell: ({ row }) => , + id: "Memory", + accessorKey: "id", + enableSorting: !!onServerSort, + header: ({ column }) => ( + + ), + cell: ({ row }) => , }, { - header: "Disk", - size: 200, - cell: ({ row }) => , + id: "Disk", + accessorKey: "id", + enableSorting: !!onServerSort, + header: ({ column }) => ( + + ), + cell: ({ row }) => , }, { - header: "Load Avg", - size: 160, - cell: ({ row }) => , + id: "LoadAverage", + accessorKey: "id", + enableSorting: !!onServerSort, + header: ({ column }) => ( + + ), + cell: ({ row }) => , }, { - header: "Net", - size: 100, - cell: ({ row }) => , + id: "Network", + accessorKey: "id", + enableSorting: !!onServerSort, + header: ({ column }) => ( + + ), + cell: ({ row }) => , }, { header: "Version", - size: 160, cell: ({ row }) => , }, ]} @@ -70,36 +139,38 @@ export default function StatsServerTable({ ); } -function CpuCell({ id }: { id: string }) { - const stats = useServerStats(id); +function CpuCell({ server }: { server: Types.ServerListItem }) { + const stats = server.info.stats; const cpu = stats?.cpu_perc ?? 0; - const { cpuWarning: warning, cpuCritical: critical } = - useServerThresholds(id); + const { cpuWarning: warning, cpuCritical: critical } = useServerThresholds( + server.id, + ); const intent: "Good" | "Warning" | "Critical" = cpu < warning ? "Good" : cpu < critical ? "Warning" : "Critical"; return ; } -function MemCell({ id }: { id: string }) { - const stats = useServerStats(id); +function MemCell({ server }: { server: Types.ServerListItem }) { + const stats = server.info.stats; const used = stats?.mem_used_gb ?? 0; const total = stats?.mem_total_gb ?? 0; const perc = total > 0 ? (used / total) * 100 : 0; - const { memWarning: warning, memCritical: critical } = - useServerThresholds(id); + const { memWarning: warning, memCritical: critical } = useServerThresholds( + server.id, + ); const intent: "Good" | "Warning" | "Critical" = perc < warning ? "Good" : perc < critical ? "Warning" : "Critical"; return ; } -function DiskCell({ id }: { id: string }) { - const stats = useServerStats(id); - const used = stats?.disks?.reduce((acc, d) => acc + (d.used_gb || 0), 0) ?? 0; - const total = - stats?.disks?.reduce((acc, d) => acc + (d.total_gb || 0), 0) ?? 0; +function DiskCell({ server }: { server: Types.ServerListItem }) { + const stats = server.info.stats; + const used = stats?.disk_used_gb ?? 0; + const total = stats?.disk_total_gb ?? 0; const perc = total > 0 ? (used / total) * 100 : 0; - const { diskWarning: warning, diskCritical: critical } = - useServerThresholds(id); + const { diskWarning: warning, diskCritical: critical } = useServerThresholds( + server.id, + ); const intent: "Good" | "Warning" | "Critical" = perc < warning ? "Good" : perc < critical ? "Warning" : "Critical"; return ( @@ -107,16 +178,28 @@ function DiskCell({ id }: { id: string }) { value={stats ? perc : undefined} intent={intent} infoDisabled={!stats} - info={} + info={} /> ); } -function LoadAvgCell({ id }: { id: string }) { +/** + * The individual disk list is not included in the list item stats. + * Only mounted when the disk hover card opens, so the full + * GetSystemStats query is only made on demand. + */ +function DiskUsageInfo({ id }: { id: string }) { const stats = useServerStats(id); + return ; +} + +function LoadAvgCell({ server }: { server: Types.ServerListItem }) { + const stats = server.info.stats; const one = stats?.load_average?.one; const five = stats?.load_average?.five; const fifteen = stats?.load_average?.fifteen; + const logicalCores = server.info.logical_core_count; + const physicalCores = server.info.core_count; return ( @@ -143,16 +226,23 @@ function LoadAvgCell({ id }: { id: string }) { {fifteen !== undefined ? fifteen.toFixed(2) : "N/A"} + {logicalCores && ( + + {logicalCores} / {physicalCores} Core{logicalCores === 1 ? "" : "s"} + + )} ); } -function NetCell({ id }: { id: string }) { - const stats = useServerStats(id); +function NetCell({ server }: { server: Types.ServerListItem }) { + const stats = server.info.stats; const ingress = stats?.network_ingress_bytes ?? 0; const egress = stats?.network_egress_bytes ?? 0; if (!stats) { return N/A; } - return {fmtRateBytes(ingress + egress)}; + return ( + {fmtRateBytes(ingress + egress)} + ); } diff --git a/ui/src/resources/server/tabs.tsx b/ui/src/resources/server/tabs.tsx index 97cce2a72f..f0746f13db 100644 --- a/ui/src/resources/server/tabs.tsx +++ b/ui/src/resources/server/tabs.tsx @@ -30,19 +30,22 @@ export default function ServerTabs({ id }: { id: string }) { !specificTerminal || (serverInfo?.terminals_disabled ?? true); const stacks = - useRead("ListStacks", {}).data?.filter( - (stack) => stack.info.server_id === id, - ) ?? []; + useRead("ListStacks", { + query: { specific: { server_ids: [id] } }, + limit: 1, + }).data ?? []; const noStacks = stacks.length === 0; const deployments = - useRead("ListDeployments", {}).data?.filter( - (deployment) => deployment.info.server_id === id, - ) ?? []; + useRead("ListDeployments", { + query: { specific: { server_ids: [id] } }, + limit: 1, + }).data ?? []; const noDeployments = deployments.length === 0; const repos = - useRead("ListRepos", {}).data?.filter( - (repo) => repo.info.server_id === id, - ) ?? []; + useRead("ListRepos", { + query: { specific: { server_ids: [id] } }, + limit: 1, + }).data ?? []; const noRepos = repos.length === 0; const noResources = noDeployments && noRepos && noStacks; @@ -107,13 +110,7 @@ export default function ServerTabs({ id }: { id: string }) { break; case "Resources": View = ( - + ); break; case "Terminals": diff --git a/ui/src/resources/server/version.tsx b/ui/src/resources/server/version.tsx index 519e2fa262..aa4e627a64 100644 --- a/ui/src/resources/server/version.tsx +++ b/ui/src/resources/server/version.tsx @@ -23,7 +23,9 @@ export default function ServerVersion({ id }: { id: string }) { ) : ( )} - {serverVersion ?? "Unknown"} + + {serverVersion ?? "Unknown"} + diff --git a/ui/src/resources/stack/config/index.tsx b/ui/src/resources/stack/config/index.tsx index 80b6feb5ff..f01b5cd8ce 100644 --- a/ui/src/resources/stack/config/index.tsx +++ b/ui/src/resources/stack/config/index.tsx @@ -185,7 +185,7 @@ export default function StackConfig({ { label: "Server", labelHidden: true, - hidden: !!currSwarmId, + hidden: swarmsExist && !!currSwarmId, fields: { server_id: (server_id, set) => { return ( diff --git a/ui/src/resources/stack/executions.tsx b/ui/src/resources/stack/executions.tsx index 964b3dd7b2..78369f9750 100644 --- a/ui/src/resources/stack/executions.tsx +++ b/ui/src/resources/stack/executions.tsx @@ -18,7 +18,7 @@ export const DeployStack = ({ const deploying = useRead( "GetStackActionState", { stack: id }, - { refetchInterval: 5000 }, + { refetchInterval: 5_000 }, ).data?.deploying; const services = useRead("ListStackServices", { stack: id }).data; const container_state = @@ -82,7 +82,7 @@ export const DestroyStack = ({ const destroying = useRead( "GetStackActionState", { stack: id }, - { refetchInterval: 5000 }, + { refetchInterval: 5_000 }, ).data?.destroying; const services = useRead("ListStackServices", { stack: id }).data; const container_state = @@ -126,7 +126,7 @@ export const PullStack = ({ const actionState = useRead( "GetStackActionState", { stack: id }, - { refetchInterval: 5000 }, + { refetchInterval: 5_000 }, ).data; if ( @@ -163,7 +163,7 @@ export const RestartStack = ({ const actionState = useRead( "GetStackActionState", { stack: id }, - { refetchInterval: 5000 }, + { refetchInterval: 5_000 }, ).data; const services = useRead("ListStackServices", { stack: id }).data; const container_state = @@ -211,7 +211,7 @@ export const StartStopStack = ({ const actionState = useRead( "GetStackActionState", { stack: id }, - { refetchInterval: 5000 }, + { refetchInterval: 5_000 }, ).data; const services = useRead("ListStackServices", { stack: id }).data; const container_state = @@ -285,7 +285,7 @@ export const PauseUnpauseStack = ({ const actionState = useRead( "GetStackActionState", { stack: id }, - { refetchInterval: 5000 }, + { refetchInterval: 5_000 }, ).data; const services = useRead("ListStackServices", { stack: id }).data; const container_state = diff --git a/ui/src/resources/stack/index.tsx b/ui/src/resources/stack/index.tsx index 592d4e9f5e..5c6e71c6d6 100644 --- a/ui/src/resources/stack/index.tsx +++ b/ui/src/resources/stack/index.tsx @@ -1,5 +1,11 @@ import { stackStateIntention } from "@/lib/color"; -import { useInvalidate, usePermissions, useRead, useWrite } from "@/lib/hooks"; +import { + useInvalidate, + useListItem, + usePermissions, + useRead, + useWrite, +} from "@/lib/hooks"; import { ICONS } from "@/lib/icons"; import { Types } from "komodo_client"; import { StatusBadge } from "mogh_ui"; @@ -32,14 +38,16 @@ import ResourceLink from "@/resources/link"; import HashCompare from "@/components/hash-compare"; import StackUpdateAvailable from "./update-available"; import ResourceHeader from "../header"; -import BatchExecutions from "@/components/batch-executions"; +import BatchExecutions from "@/resources/batch-executions"; import NewResourceWithDeployTarget from "../new-with-deploy-target"; import { hexColorByIntention } from "mogh_ui"; -export function useStack(id: string | undefined, useName?: boolean) { - return useRead("ListStacks", {}).data?.find((r) => - useName ? r.name === id : r.id === id, - ); +export function useStack( + id: string | undefined, + useName?: boolean, + refetchInterval?: number | false, +) { + return useListItem("Stack", id, useName, refetchInterval); } export function useFullStack(id: string) { @@ -49,9 +57,11 @@ export function useFullStack(id: string) { export const StackComponents: RequiredResourceComponents< Types.StackConfig, Types.StackInfo, - Types.StackListItemInfo + Types.StackListItemInfo, + Types.StackQuerySpecifics > = { - useList: () => useRead("ListStacks", {}).data, + useList: (query, limit, page) => + useRead("ListStacks", { query, limit, page }).data, useListItem: useStack, useFull: useFullStack, @@ -116,7 +126,7 @@ export const StackComponents: RequiredResourceComponents< Table: StackTable, Icon: ({ id, size = "1rem", noColor }) => { - const info = useRead("ListStacks", {}).data?.find((r) => r.id === id)?.info; + const info = useStack(id)?.info; const color = noColor ? undefined : info && diff --git a/ui/src/resources/stack/services.tsx b/ui/src/resources/stack/services.tsx index bb30a3a1ad..3b197b352f 100644 --- a/ui/src/resources/stack/services.tsx +++ b/ui/src/resources/stack/services.tsx @@ -19,21 +19,9 @@ export default function StackServices({ titleOther?: ReactNode; }) { const info = useStack(id)?.info; - const state = info?.state ?? Types.StackState.Unknown; - const services = useRead( - "ListStackServices", - { stack: id }, - { refetchInterval: 10_000 }, - ).data; - - if ( - !services || - services.length === 0 || - [Types.StackState.Unknown, Types.StackState.Down].includes(state) - ) { - // return
No Services Available
; - return null; - } + const services = + useRead("ListStackServices", { stack: id }, { refetchInterval: 10_000 }) + .data ?? []; return (
@@ -59,6 +47,7 @@ function StackServicesSwarm({ stackId: string; services: Types.ListStackServicesResponse; }) { + const isUnknown = useStack(stackId)?.info.state === Types.StackState.Unknown; return ( { const state = row.original.swarm_service?.State; return ( - + ); }, }, @@ -126,6 +124,7 @@ function StackServicesServer({ serverId: string; services: Types.ListStackServicesResponse; }) { + const isUnknown = useStack(stackId)?.info.state === Types.StackState.Unknown; return ( ); }, diff --git a/ui/src/resources/stack/table.tsx b/ui/src/resources/stack/table.tsx index bf25927860..a8e8391e27 100644 --- a/ui/src/resources/stack/table.tsx +++ b/ui/src/resources/stack/table.tsx @@ -1,4 +1,4 @@ -import { useResourceName, useSelectedResources } from "@/lib/hooks"; +import { useResourceSelectionState } from "@/lib/hooks"; import { DataTable, SortableHeader } from "mogh_ui"; import { Group, BoxProps } from "@mantine/core"; import { Types } from "komodo_client"; @@ -8,31 +8,48 @@ import TableTags from "@/components/tags/table"; import FileSource from "@/components/file-source"; import StackUpdateAvailable from "./update-available"; +const SORT_KEYS = ["Name", "Source", "Host", "State"]; + export default function StackTable({ resources, + onServerSort, ...boxProps }: { resources: Types.StackListItem[]; + /** When provided, sorting is handled server side, + * and sort updates are passed to this callback. */ + onServerSort?: (sort: { + sort_by?: string; + sort_desc?: boolean; + }) => void; } & BoxProps) { - const swarmName = useResourceName("Swarm"); - const serverName = useResourceName("Server"); - - const [_, setSelectedResources] = useSelectedResources("Stack"); + const selectionState = useResourceSelectionState("Stack"); return ( { + const sort = sorting.find((s) => SORT_KEYS.includes(s.id)); + onServerSort( + sort ? { sort_by: sort.id, sort_desc: sort.desc } : {}, + ); + }) + } tableKey="stack-table" data={resources} selectOptions={{ selectKey: ({ name }) => name, - onSelect: setSelectedResources, + state: selectionState, }} columns={[ { header: ({ column }) => ( ), + id: "Name", accessorKey: "name", cell: ({ row }) => { return ( @@ -48,6 +65,7 @@ export default function StackTable({ header: ({ column }) => ( ), + id: "Source", accessorKey: "info.repo", cell: ({ row }) => , size: 200, @@ -56,14 +74,15 @@ export default function StackTable({ header: ({ column }) => ( ), + id: "Host", accessorKey: "info.server_id", sortingFn: (a, b) => { const name_a = a.original.info.swarm_id - ? swarmName(a.original.info.swarm_id) - : serverName(a.original.info.server_id); + ? a.original.info.swarm_name + : a.original.info.server_name; const name_b = b.original.info.swarm_id - ? swarmName(b.original.info.swarm_id) - : serverName(b.original.info.server_id); + ? b.original.info.swarm_name + : b.original.info.server_name; if (!name_a && !name_b) return 0; if (!name_a) return 1; @@ -82,6 +101,7 @@ export default function StackTable({ size: 200, }, { + id: "State", accessorKey: "info.state", header: ({ column }) => ( diff --git a/ui/src/resources/stack/tabs.tsx b/ui/src/resources/stack/tabs.tsx index e5ce95f683..ec0320e4eb 100644 --- a/ui/src/resources/stack/tabs.tsx +++ b/ui/src/resources/stack/tabs.tsx @@ -3,10 +3,7 @@ import { useStack } from "."; import { usePermissions, useRead } from "@/lib/hooks"; import { Types } from "komodo_client"; import { useMemo } from "react"; -import { - MobileFriendlyTabsSelector, - TabNoContent, -} from "mogh_ui"; +import { MobileFriendlyTabsSelector, TabNoContent } from "mogh_ui"; import { Tabs } from "@mantine/core"; import { ICONS } from "@/lib/icons"; import { stackStateIntention } from "@/lib/color"; @@ -37,14 +34,16 @@ export default function StackTabs({ id }: { id: string }) { const state = info?.state; const hideInfo = !info?.files_on_host && !info?.repo && !info?.linked_repo; - const hideServices = + const hideLogs = state === undefined || state === Types.StackState.Unknown || - state === Types.StackState.Down; - const hideLogs = hideServices || !specificLogs; + state === Types.StackState.Down || + !specificLogs; const terminalDisabled = !specificTerminal || containerTerminalsDisabled || + // Not attached to swarm + !!info?.swarm_id || // All services are not running services?.every( (service) => @@ -54,7 +53,6 @@ export default function StackTabs({ id }: { id: string }) { const view = (_view === "Info" && hideInfo) || - (_view === "Services" && hideServices) || (_view === "Terminals" && terminalDisabled) || (_view === "Log" && hideLogs) ? "Config" @@ -73,7 +71,6 @@ export default function StackTabs({ id }: { id: string }) { }, { value: "Services", - disabled: hideServices, icon: ICONS.Service, }, { @@ -90,7 +87,6 @@ export default function StackTabs({ id }: { id: string }) { ], [ hideInfo, - hideServices, specificLogs, hideLogs, specificTerminal, diff --git a/ui/src/resources/stack/update-available.tsx b/ui/src/resources/stack/update-available.tsx index f99f1a38fa..bec48c1147 100644 --- a/ui/src/resources/stack/update-available.tsx +++ b/ui/src/resources/stack/update-available.tsx @@ -40,11 +40,14 @@ export default function StackUpdateAvailable({ }, }, ); + const deploying = useRead( "GetStackActionState", { stack: id }, - { refetchInterval: 5000 }, + { refetchInterval: 5_000, enabled: !small && canExecute }, ).data?.deploying; + const pending = isPending || deploying; + const stack = useStack(id); const fullStack = useFullStack(id); const info = stack?.info; @@ -62,8 +65,6 @@ export default function StackUpdateAvailable({ const updateAvailable = servicesWithUpdate.length > 0; - const pending = isPending || deploying; - // No quick deploy action / check for update button if (small || !canExecute) { if (!updateAvailable) { @@ -109,18 +110,6 @@ export default function StackUpdateAvailable({ return ( <> - - - {updateAvailable && ( )} + + + ); } diff --git a/ui/src/resources/swarm/index.tsx b/ui/src/resources/swarm/index.tsx index 4d98a3364b..e1f6d8ff22 100644 --- a/ui/src/resources/swarm/index.tsx +++ b/ui/src/resources/swarm/index.tsx @@ -3,7 +3,7 @@ import { swarmNodeStateIntention, swarmTaskStateIntention, } from "@/lib/color"; -import { usePermissions, useRead } from "@/lib/hooks"; +import { useListItem, usePermissions, useRead } from "@/lib/hooks"; import { ICONS } from "@/lib/icons"; import { RequiredResourceComponents } from ".."; import { Types } from "komodo_client"; @@ -15,15 +15,17 @@ import { useDisclosure } from "@mantine/hooks"; import { Box, Button, Modal, Text } from "@mantine/core"; import JoinSwarmCommands from "./join-commands"; import ResourceHeader from "../header"; -import BatchExecutions from "@/components/batch-executions"; +import BatchExecutions from "@/resources/batch-executions"; import SwarmHeaderInfo from "./header-info"; import { HoverError } from "mogh_ui"; import { hexColorByIntention } from "mogh_ui"; -export function useSwarm(id: string | undefined, useName?: boolean) { - return useRead("ListSwarms", {}).data?.find((r) => - useName ? r.name === id : r.id === id, - ); +export function useSwarm( + id: string | undefined, + useName?: boolean, + refetchInterval?: number | false, +) { + return useListItem("Swarm", id, useName, refetchInterval); } export function useFullSwarm(id: string) { @@ -33,9 +35,11 @@ export function useFullSwarm(id: string) { export const SwarmComponents: RequiredResourceComponents< Types.SwarmConfig, Types.SwarmInfo, - Types.SwarmListItemInfo + Types.SwarmListItemInfo, + Types.SwarmQuerySpecifics > = { - useList: () => useRead("ListSwarms", {}).data, + useList: (query, limit, page) => + useRead("ListSwarms", { query, limit, page }).data, useListItem: useSwarm, useFull: useFullSwarm, @@ -71,8 +75,7 @@ export const SwarmComponents: RequiredResourceComponents< Table: SwarmTable, Icon: ({ id, size = "1rem", noColor }) => { - const state = useRead("ListSwarms", {}).data?.find((r) => r.id === id)?.info - .state; + const state = useSwarm(id)?.info.state; const color = noColor ? undefined : state && hexColorByIntention(swarmStateIntention(state)); diff --git a/ui/src/resources/swarm/resources.tsx b/ui/src/resources/swarm/resources.tsx index eaa50b342a..573b6f5b3b 100644 --- a/ui/src/resources/swarm/resources.tsx +++ b/ui/src/resources/swarm/resources.tsx @@ -1,53 +1,33 @@ -import { useRead, useUser } from "@/lib/hooks"; import { ICONS } from "@/lib/icons"; import { Section, SectionProps } from "mogh_ui"; -import { Types } from "komodo_client"; -import StackTable from "@/resources/stack/table"; -import DeploymentTable from "@/resources/deployment/table"; -import { ResourceComponents } from ".."; +import ResourceTable from "../table"; export interface SwarmHostedResourcesSectionProps extends Omit< SectionProps, "children" > { swarmId: string; - stacks: Types.StackListItem[]; - deployments: Types.DeploymentListItem[]; } export default function SwarmHostedResourcesSection({ swarmId, - stacks, - deployments, ...sectionProps }: SwarmHostedResourcesSectionProps) { - const isAdmin = useUser().data?.admin ?? false; - const disableNonAdminCreate = - useRead("GetCoreInfo", {}).data?.disable_non_admin_create ?? true; - return (
-
} - actions={ - (isAdmin || !disableNonAdminCreate) && ( - - ) - } - > - +
}> +
-
} - actions={ - (isAdmin || !disableNonAdminCreate) && ( - - ) - } - > - +
}> +
); diff --git a/ui/src/resources/swarm/table.tsx b/ui/src/resources/swarm/table.tsx index 9bdf8a883e..1bf0805146 100644 --- a/ui/src/resources/swarm/table.tsx +++ b/ui/src/resources/swarm/table.tsx @@ -1,4 +1,4 @@ -import { useSelectedResources } from "@/lib/hooks"; +import { useResourceSelectionState } from "@/lib/hooks"; import ResourceLink from "@/resources/link"; import { DataTable, SortableHeader } from "mogh_ui"; import { Types } from "komodo_client"; @@ -6,28 +6,48 @@ import { SwarmComponents } from "."; import TableTags from "@/components/tags/table"; import { BoxProps } from "@mantine/core"; +const SORT_KEYS = ["Name", "State"]; + export default function SwarmTable({ resources, + onServerSort, ...boxProps }: { resources: Types.SwarmListItem[]; + /** When provided, sorting is handled server side, + * and sort updates are passed to this callback. */ + onServerSort?: (sort: { + sort_by?: string; + sort_desc?: boolean; + }) => void; } & BoxProps) { - const [_, setSelectedResources] = useSelectedResources("Swarm"); + const selectionState = useResourceSelectionState("Swarm"); return ( { + const sort = sorting.find((s) => SORT_KEYS.includes(s.id)); + onServerSort( + sort ? { sort_by: sort.id, sort_desc: sort.desc } : {}, + ); + }) + } tableKey="swarm-table" data={resources} selectOptions={{ selectKey: ({ name }) => name, - onSelect: setSelectedResources, + state: selectionState, }} columns={[ { header: ({ column }) => ( ), + id: "Name", accessorKey: "name", cell: ({ row }) => , size: 200, @@ -36,6 +56,7 @@ export default function SwarmTable({ header: ({ column }) => ( ), + id: "State", accessorKey: "info.state", cell: ({ row }) => , size: 120, diff --git a/ui/src/resources/swarm/tabs.tsx b/ui/src/resources/swarm/tabs.tsx index 08fe8e9292..e556f30385 100644 --- a/ui/src/resources/swarm/tabs.tsx +++ b/ui/src/resources/swarm/tabs.tsx @@ -1,9 +1,6 @@ import { usePermissions, useRead } from "@/lib/hooks"; import { ICONS } from "@/lib/icons"; -import { - MobileFriendlyTabsSelector, - TabNoContent, -} from "mogh_ui"; +import { MobileFriendlyTabsSelector, TabNoContent } from "mogh_ui"; import { useLocalStorage } from "@mantine/hooks"; import { useMemo } from "react"; import { useSwarm } from "."; @@ -27,14 +24,16 @@ export default function SwarmTabs({ id }: { id: string }) { const swarmInfo = useSwarm(id)?.info; const stacks = - useRead("ListStacks", {}).data?.filter( - (stack) => stack.info.swarm_id === id, - ) ?? []; + useRead("ListStacks", { + query: { specific: { swarm_ids: [id] } }, + limit: 1, + }).data ?? []; const noStacks = stacks.length === 0; const deployments = - useRead("ListDeployments", {}).data?.filter( - (deployment) => deployment.info.swarm_id === id, - ) ?? []; + useRead("ListDeployments", { + query: { specific: { swarm_ids: [id] } }, + limit: 1, + }).data ?? []; const noDeployments = deployments.length === 0; const noResources = noDeployments && noStacks; @@ -90,14 +89,7 @@ export default function SwarmTabs({ id }: { id: string }) { ); break; case "Resources": - View = ( - - ); + View = ; break; } diff --git a/ui/src/resources/sync/config.tsx b/ui/src/resources/sync/config.tsx index 929b9787e3..9d60b929a6 100644 --- a/ui/src/resources/sync/config.tsx +++ b/ui/src/resources/sync/config.tsx @@ -2,8 +2,6 @@ import { AccountSelectorConfig } from "@/components/config/account-selector"; import LinkedRepo from "@/components/config/linked-repo"; import { ProviderSelectorConfig } from "@/components/config/provider-selector"; import { MonacoEditor } from "mogh_ui"; -import Tags from "@/components/tags"; -import TagSelector from "@/components/tags/selector"; import WebhookBuilder from "@/components/webhook/builder"; import CopyWebhookUrl from "@/components/webhook/copy-url"; import { @@ -22,12 +20,12 @@ import { ConfigSwitch, } from "mogh_ui"; import { ShowHideButton } from "mogh_ui"; -import { Group, Select, Text } from "@mantine/core"; +import { Select, Text } from "@mantine/core"; import { useLocalStorage } from "@mantine/hooks"; import { Types } from "komodo_client"; -import { CircleMinus } from "lucide-react"; import { ReactNode } from "react"; import { useFullResourceSync } from "."; +import TagMultiSelector from "@/components/tags/multi-selector"; type SyncMode = "UI Defined" | "Files On Server" | "Git Repo" | undefined; const SYNC_MODES = ["UI Defined", "Files On Server", "Git Repo"] as const; @@ -189,36 +187,14 @@ export default function ResourceSyncConfig({ description: "Only sync resources matching all of these tags.", fields: { match_tags: (values, set) => { - const tags = useRead("ListTags", {}).data; - const otherTags = tags?.filter((tag) => !values?.includes(tag.name)); return ( - - set({ match_tags: [...(values ?? []), tag] })} - disabled={disabled || !includeResources} - position="bottom-start" - useName - canCreate - /> - - values?.includes(tag.name)) - .map((tag) => tag.name) ?? [] - } - onBadgeClick={(toRemove) => - set({ - match_tags: values?.filter((tagName) => tagName !== toRemove), - }) - } - icon={} - fz="1rem" - useName - /> - + set({ match_tags })} + disabled={disabled || !includeResources} + useName + canCreate + /> ); }, }, diff --git a/ui/src/resources/sync/executions.tsx b/ui/src/resources/sync/executions.tsx index 64fa0f3117..aeb6c935b6 100644 --- a/ui/src/resources/sync/executions.tsx +++ b/ui/src/resources/sync/executions.tsx @@ -43,7 +43,7 @@ export function ExecuteSync({ id }: { id: string }) { const syncing = useRead( "GetResourceSyncActionState", { sync: id }, - { refetchInterval: 5000 }, + { refetchInterval: 5_000 }, ).data?.syncing; const sync = useFullResourceSync(id); const { view } = useResourceSyncTabsView(sync); diff --git a/ui/src/resources/sync/hooks.ts b/ui/src/resources/sync/hooks.ts index e5d22acdec..34e7a29f08 100644 --- a/ui/src/resources/sync/hooks.ts +++ b/ui/src/resources/sync/hooks.ts @@ -1,16 +1,14 @@ -import { atomWithStorage } from "@/lib/hooks"; import { resourceSyncNoChanges } from "@/lib/utils"; -import { useAtom } from "jotai"; +import { useLocalStorage } from "@mantine/hooks"; import { Types } from "komodo_client"; type ResourceSyncTabsView = "Config" | "Info" | "Execute" | "Commit"; -const syncTabsViewAtom = atomWithStorage( - "sync-tabs-v5", - "Config", -); export function useResourceSyncTabsView(sync: Types.ResourceSync | undefined) { - const [_view, setView] = useAtom(syncTabsViewAtom); + const [_view, setView] = useLocalStorage({ + key: `sync-${sync?._id?.$oid}-tab-v1`, + defaultValue: "Config", + }); const hideInfo = sync?.config?.files_on_host ? false diff --git a/ui/src/resources/sync/index.tsx b/ui/src/resources/sync/index.tsx index ab712563fd..e8be1e6001 100644 --- a/ui/src/resources/sync/index.tsx +++ b/ui/src/resources/sync/index.tsx @@ -1,5 +1,5 @@ import { resourceSyncStateIntention } from "@/lib/color"; -import { useRead } from "@/lib/hooks"; +import { useListItem, useRead } from "@/lib/hooks"; import { ICONS } from "@/lib/icons"; import { Types } from "komodo_client"; import { fmtDate, StatusBadge } from "mogh_ui"; @@ -13,13 +13,15 @@ import { Box, Group } from "@mantine/core"; import HashCompare from "@/components/hash-compare"; import ResourceSyncTabs from "./tabs"; import ResourceHeader from "../header"; -import BatchExecutions from "@/components/batch-executions"; +import BatchExecutions from "@/resources/batch-executions"; import { hexColorByIntention } from "mogh_ui"; -export function useResourceSync(id: string | undefined, useName?: boolean) { - return useRead("ListResourceSyncs", {}).data?.find((r) => - useName ? r.name === id : r.id === id, - ); +export function useResourceSync( + id: string | undefined, + useName?: boolean, + refetchInterval?: number | false, +) { + return useListItem("ResourceSync", id, useName, refetchInterval); } export function useFullResourceSync(id: string) { @@ -30,9 +32,11 @@ export function useFullResourceSync(id: string) { export const ResourceSyncComponents: RequiredResourceComponents< Types.ResourceSyncConfig, Types.ResourceSyncInfo, - Types.ResourceSyncListItemInfo + Types.ResourceSyncListItemInfo, + Types.ResourceSyncQuerySpecifics > = { - useList: () => useRead("ListResourceSyncs", {}).data, + useList: (query, limit, page) => + useRead("ListResourceSyncs", { query, limit, page }).data, useListItem: useResourceSync, useFull: useFullResourceSync, @@ -71,7 +75,13 @@ export const ResourceSyncComponents: RequiredResourceComponents< Description: () => <>Declare resources in TOML files., - New: () => , + New: ({ repoId }) => ( + ({ linked_repo: repoId })} + /> + ), BatchExecutions: () => ( { - const state = useRead("ListResourceSyncs", {}).data?.find( - (r) => r.id === id, - )?.info.state; + const state = useResourceSync(id)?.info.state; const color = noColor ? undefined : state && hexColorByIntention(resourceSyncStateIntention(state)); diff --git a/ui/src/resources/sync/table.tsx b/ui/src/resources/sync/table.tsx index a351829da9..4599ec1208 100644 --- a/ui/src/resources/sync/table.tsx +++ b/ui/src/resources/sync/table.tsx @@ -1,4 +1,4 @@ -import { useSelectedResources } from "@/lib/hooks"; +import { useResourceSelectionState } from "@/lib/hooks"; import { DataTable, SortableHeader } from "mogh_ui"; import { Types } from "komodo_client"; import ResourceLink from "@/resources/link"; @@ -7,27 +7,47 @@ import TableTags from "@/components/tags/table"; import { BoxProps } from "@mantine/core"; import FileSource from "@/components/file-source"; +const SORT_KEYS = ["Name", "Source", "Branch", "State"]; + export default function ResourceSyncTable({ resources, + onServerSort, ...boxProps }: { resources: Types.ResourceSyncListItem[]; + /** When provided, sorting is handled server side, + * and sort updates are passed to this callback. */ + onServerSort?: (sort: { + sort_by?: string; + sort_desc?: boolean; + }) => void; } & BoxProps) { - const [_, setSelectedResources] = useSelectedResources("ResourceSync"); + const selectionState = useResourceSelectionState("ResourceSync"); return ( { + const sort = sorting.find((s) => SORT_KEYS.includes(s.id)); + onServerSort( + sort ? { sort_by: sort.id, sort_desc: sort.desc } : {}, + ); + }) + } tableKey="syncs" data={resources} selectOptions={{ selectKey: ({ name }) => name, - onSelect: setSelectedResources, + state: selectionState, }} columns={[ { header: ({ column }) => ( ), + id: "Name", accessorKey: "name", cell: ({ row }) => ( @@ -38,6 +58,7 @@ export default function ResourceSyncTable({ header: ({ column }) => ( ), + id: "Source", accessorKey: "info.repo", cell: ({ row }) => , size: 200, @@ -46,6 +67,7 @@ export default function ResourceSyncTable({ header: ({ column }) => ( ), + id: "Branch", accessorKey: "info.branch", size: 200, }, @@ -53,6 +75,7 @@ export default function ResourceSyncTable({ header: ({ column }) => ( ), + id: "State", accessorKey: "info.state", cell: ({ row }) => ( diff --git a/ui/src/resources/table.tsx b/ui/src/resources/table.tsx new file mode 100644 index 0000000000..5bc4f8b717 --- /dev/null +++ b/ui/src/resources/table.tsx @@ -0,0 +1,180 @@ +import { Group, Stack, StackProps } from "@mantine/core"; +import { + RequiredResourceComponents, + ResourceComponents, + UsableResource, +} from "."; +import ResourceNotFound from "./not-found"; +import ListPagination from "@/components/list-pagination"; +import { LabelledSwitch, SearchInput, TableSkeleton } from "mogh_ui"; +import TemplateQuerySelector from "@/components/template-query-selector"; +import TagsFilter from "@/components/tags/filter"; +import { useCallback, useEffect, useMemo, useState } from "react"; +import { + useDebouncedTermSearch, + useFilterByUpdateAvailable, + useRead, + useTagsFilter, + useTemplatesQueryBehavior, + useUser, +} from "@/lib/hooks"; +import { Types } from "komodo_client"; + +export interface ResourceTableProps extends StackProps { + type: UsableResource; + showTagsQuery?: boolean; + showTemplateQuery?: boolean; + showAvailableUpdatesQuery?: boolean; + onQueryChange?: (query: Types.ResourceQuery) => void; + newProps?: Parameters[0]; + specific?: any; +} + +export default function ResourceTable({ + type, + showTagsQuery, + showTemplateQuery, + showAvailableUpdatesQuery, + onQueryChange, + newProps, + specific, + ...stackProps +}: ResourceTableProps) { + const isAdmin = useUser().data?.admin ?? false; + const disableNonAdminCreate = + useRead("GetCoreInfo", {}).data?.disable_non_admin_create ?? true; + + const [page, setPage] = useState(0); + + const { search, setSearch, terms } = useDebouncedTermSearch({ + onUpdate: () => setPage(0), + }); + + const [filterUpdateAvailable, toggleFilterUpdateAvailable] = + useFilterByUpdateAvailable(); + + const tags = useTagsFilter(); + const [templates] = useTemplatesQueryBehavior(); + + // Server side sort, passed up from the table. + // The sort keys are resource type specific, so ignore + // any sort captured on a previously selected type. + const [_sort, _setSort] = useState<{ + type: UsableResource; + sort_by?: string; + sort_desc?: boolean; + }>({ type }); + const sort = _sort.type === type ? _sort : { type }; + const setSort = useCallback( + (sort: { sort_by?: string; sort_desc?: boolean }) => + _setSort({ type, ...sort }), + [type], + ); + + const query: Types.ResourceQuery = useMemo(() => { + const query = { + terms, + tags: showTagsQuery ? tags : undefined, + templates: showTemplateQuery ? templates : undefined, + specific: + showAvailableUpdatesQuery && (type === "Stack" || type === "Deployment") + ? { update_available: filterUpdateAvailable, ...specific } + : specific, + }; + onQueryChange?.(query); + return query; + }, [ + type, + terms, + showTagsQuery, + tags, + showTemplateQuery, + templates, + showAvailableUpdatesQuery, + filterUpdateAvailable, + specific, + ]); + + const resources = + useRead( + `List${type}s`, + { + query, + page, + sort_by: sort.sort_by as any, + sort_desc: sort.sort_desc, + }, + { + refetchInterval: 15_000, + // Keep the previous rows visible while fetching after a query key + // change (page / sort / search / filters) to prevent table flashing. + // Must NOT keep them across a change of resource type. + placeholderData: (prev, prevQuery) => + prevQuery?.queryKey[0] === `List${type}s` ? prev : undefined, + }, + ).data ?? []; + + // Set to page 0 whenever the resource type, any filter, + // or the sort changes, otherwise the query can point past + // the last page and come back empty. + useEffect(() => { + setPage(0); + }, [ + type, + tags, + templates, + filterUpdateAvailable, + sort.sort_by, + sort.sort_desc, + ]); + + const RC = ResourceComponents[type]; + + const Table = useMemo( + () => + resources && RC ? ( + + ) : ( + + ), + [resources, setSort], + ); + + if (!RC) { + return ; + } + + return ( + + + + {(isAdmin || !disableNonAdminCreate) && } + + + + + + {showAvailableUpdatesQuery && + (type === "Stack" || type === "Deployment") && ( + + )} + {showTemplateQuery && } + {showTagsQuery && } + + + + + {Table} + + ); +} diff --git a/ui/src/resources/tags.tsx b/ui/src/resources/tags.tsx index 7f6258efbc..0c7d94a1dd 100644 --- a/ui/src/resources/tags.tsx +++ b/ui/src/resources/tags.tsx @@ -1,6 +1,6 @@ import Tags from "@/components/tags"; import { tagColor } from "@/lib/color"; -import { useInvalidate, useRead, useWrite } from "@/lib/hooks"; +import { useInvalidate, useListItem, useRead, useWrite } from "@/lib/hooks"; import { ICONS } from "@/lib/icons"; import { filterBySplit } from "mogh_ui"; import { @@ -28,7 +28,7 @@ export const ResourceTags = ({ }) => { const inv = useInvalidate(); const { type, id } = target; - const resource = useRead(`List${type}s`, {}).data?.find((d) => d.id === id); + const resource = useListItem(type, id); const { mutate } = useWrite("UpdateResourceMeta", { onSuccess: () => { inv([`List${type}s`]); diff --git a/ui/src/router.tsx b/ui/src/router.tsx index 11559d8d21..1b7a057657 100644 --- a/ui/src/router.tsx +++ b/ui/src/router.tsx @@ -26,6 +26,7 @@ const Profile = lazy(() => import("@/pages/profile")); const User = lazy(() => import("@/pages/user")); const UserGroup = lazy(() => import("@/pages/user-group")); const Schedules = lazy(() => import("@/pages/schedules")); +const Stats = lazy(() => import("@/pages/stats")); const Terminals = lazy(() => import("@/pages/terminals")); const Terminal = lazy(() => import("@/pages/terminal")); const Containers = lazy(() => import("@/pages/containers")); @@ -62,6 +63,7 @@ export const Router = () => { } /> } /> } /> + } /> } /> } /> } /> diff --git a/ui/vite.config.ts b/ui/vite.config.ts index ab640ae617..58e1051abf 100644 --- a/ui/vite.config.ts +++ b/ui/vite.config.ts @@ -12,9 +12,17 @@ export default defineConfig({ allowedHosts: process.env.ALLOWED_HOSTS?.split(","), }, resolve: { - alias: { - "@": path.resolve(__dirname, "./src"), - }, + alias: [ + { find: "@", replacement: path.resolve(__dirname, "./src") }, + // monaco-editor >= 0.53 has an exports map ("./*.js": "./esm/vs/*.js"), + // so legacy deep imports like "monaco-editor/esm/vs/..." no longer + // resolve. monaco-worker-manager (used by monaco-yaml's worker) still + // imports the legacy path — rewrite it to the exports-map form. + { + find: /^monaco-editor\/esm\/vs\/(.*)$/, + replacement: "monaco-editor/$1", + }, + ], dedupe: [ "@mantine/core", "@mantine/form", @@ -32,6 +40,7 @@ export default defineConfig({ "react-router-dom", ], }, + optimizeDeps: { exclude: ["mogh_ui"] }, css: { preprocessorOptions: { scss: { diff --git a/ui/yarn.lock b/ui/yarn.lock index 5eb3104f42..a441a54699 100644 --- a/ui/yarn.lock +++ b/ui/yarn.lock @@ -12,25 +12,25 @@ resolved "https://registry.yarnpkg.com/@bufbuild/protobuf/-/protobuf-2.11.0.tgz#3ec3985c9074b23aea337957225fe15a0e845f8e" integrity sha512-sBXGT13cpmPR5BMgHE6UEEfEaShh5Ror6rfN3yEK5si7QVrtZg8LEPQb0VVhiLRUslD2yLnXtnRzG035J/mZXQ== -"@emnapi/core@1.10.0": - version "1.10.0" - resolved "https://registry.yarnpkg.com/@emnapi/core/-/core-1.10.0.tgz#380ccc8f2412ea22d1d972df7f8ee23a3b9c7467" - integrity sha512-yq6OkJ4p82CAfPl0u9mQebQHKPJkY7WrIuk205cTYnYe+k2Z8YBh11FrbRG/H6ihirqcacOgl2BIO8oyMQLeXw== +"@emnapi/core@1.11.1": + version "1.11.1" + resolved "https://registry.yarnpkg.com/@emnapi/core/-/core-1.11.1.tgz#b9e1064f3a6b1631e241e638eb48d736bfd372a6" + integrity sha512-RSvbQmHzdKzNsLYa/wHrbc3KN4sYLKAdPZxqiM2HATqv/SBk2/ENSHpvXGaLOMcsAyz0poEGqkmmKYG3OWiJEQ== dependencies: - "@emnapi/wasi-threads" "1.2.1" + "@emnapi/wasi-threads" "1.2.2" tslib "^2.4.0" -"@emnapi/runtime@1.10.0": - version "1.10.0" - resolved "https://registry.yarnpkg.com/@emnapi/runtime/-/runtime-1.10.0.tgz#4b260c0d3534204e98c6110b8db1a987d26ec87c" - integrity sha512-ewvYlk86xUoGI0zQRNq/mC+16R1QeDlKQy21Ki3oSYXNgLb45GV1P6A0M+/s6nyCuNDqe5VpaY84BzXGwVbwFA== +"@emnapi/runtime@1.11.1": + version "1.11.1" + resolved "https://registry.yarnpkg.com/@emnapi/runtime/-/runtime-1.11.1.tgz#58f1f3d5d81a9b12f793ab688c96371901027c24" + integrity sha512-vgj7R3y3Wgx24IQaGPA/R6YFXLHVMOZ0uVEyIQPaWs+rd1AzfEMXlAC22FYwO1XkKR6NPsq7mUandH8oIRdZFw== dependencies: tslib "^2.4.0" -"@emnapi/wasi-threads@1.2.1": - version "1.2.1" - resolved "https://registry.yarnpkg.com/@emnapi/wasi-threads/-/wasi-threads-1.2.1.tgz#28fed21a1ba1ce797c44a070abc94d42f3ae8548" - integrity sha512-uTII7OYF+/Mes/MrcIOYp5yOtSMLBWSIoLPpcgwipoiKbli6k322tcoFsxoIIxPDqW01SQGAgko4EzZi2BNv2w== +"@emnapi/wasi-threads@1.2.2": + version "1.2.2" + resolved "https://registry.yarnpkg.com/@emnapi/wasi-threads/-/wasi-threads-1.2.2.tgz#4c93becf5bfa3b13d1bbdcc06aee38321ad8139a" + integrity sha512-c95qOXkHdydNKhscBTebqEC1CVAZpyqOfVfBzQ1qgzyl3gfeldUjIggDbIZgDKsHLgnsM+igH7TJ/eAasaVuMA== dependencies: tslib "^2.4.0" @@ -70,50 +70,50 @@ resolved "https://registry.yarnpkg.com/@floating-ui/utils/-/utils-0.2.11.tgz#a269e055e40e2f45873bae9d1a2fdccbd314ea3f" integrity sha512-RiB/yIh78pcIxl6lLMG0CgBXAZ2Y0eVHqMPYugu+9U0AeT6YBeiJpf7lbdJNIugFP5SIjwNRgo4DhR1Qxi26Gg== -"@mantine/core@^9.1.1": - version "9.1.1" - resolved "https://registry.yarnpkg.com/@mantine/core/-/core-9.1.1.tgz#7d2125a334c3828950707822dd02d39c72336e98" - integrity sha512-vClOZdCeZ4oLYuA/3jAOgKGQ6dXbF6ZkzpYz09Gied9nZpB7HcQeb3dcMh8UPBE4f+EM7KlYWk6dch7GoASeaA== +"@mantine/core@^9.4.1": + version "9.4.1" + resolved "https://registry.yarnpkg.com/@mantine/core/-/core-9.4.1.tgz#071684a01ad302116bd3ed2d3727d873f7baabdd" + integrity sha512-lZWEICrum4+vwKxzh/mk4RB1N6BqZ0Cshdl6lhm7OYLiQzmOaxKtOgyaWz+vr65G8mqTXjalZalB+wmMVBYK2Q== dependencies: "@floating-ui/react" "^0.27.19" clsx "^2.1.1" react-number-format "^5.4.5" react-remove-scroll "^2.7.2" - type-fest "^5.6.0" + type-fest "^5.7.0" -"@mantine/form@^9.1.1": - version "9.1.1" - resolved "https://registry.yarnpkg.com/@mantine/form/-/form-9.1.1.tgz#50f8b2e54297e6db7779d199c7cad90001fead3e" - integrity sha512-xmebZ3s8GGMrCOPOaOwA+gQkdgNVfT2F9kBtkjAbRoZrMoY+vYFbiPWbIvWFl8pU1jBslYZrj+M0PIawJmFOdQ== +"@mantine/form@^9.4.1": + version "9.4.1" + resolved "https://registry.yarnpkg.com/@mantine/form/-/form-9.4.1.tgz#e5ef1f08e58ecdcbcea7e77eb910e5770d8cd5fd" + integrity sha512-e4tqC/D8ii7maPySWIWY306MNljPRprHPhu0/jaZrX7sRyJy+C8V9aU+ZFKYliM1CeiH1UX88+q4hWFUWVEdQw== dependencies: "@standard-schema/spec" "^1.1.0" fast-deep-equal "^3.1.3" klona "^2.0.6" -"@mantine/hooks@^9.1.1": - version "9.1.1" - resolved "https://registry.yarnpkg.com/@mantine/hooks/-/hooks-9.1.1.tgz#2349a910014353210f0ffc0e26621f05aa297869" - integrity sha512-tTJK73nGFyy1v214TLdvBq0be7QCoc6osfbXVuJgOH3YG85lWk9Mvvor6k+w6hC6HXSqKMqLKePyiGm83xGcMg== +"@mantine/hooks@^9.4.1": + version "9.4.1" + resolved "https://registry.yarnpkg.com/@mantine/hooks/-/hooks-9.4.1.tgz#3d04f2f1d09df3ba962aa2724898fa95a2150973" + integrity sha512-eTI8wmzPx3r98zgKIEuvukmoGTHBhmtI6+9E6o2DbTmEU2eM1bCdjE2vFdf0op2AlRO0KEYEcZhNQi4T/SJk0A== -"@mantine/notifications@^9.1.1": - version "9.1.1" - resolved "https://registry.yarnpkg.com/@mantine/notifications/-/notifications-9.1.1.tgz#63cdf4f5afcdd8991dd7ab82d99c8a02e2d3435a" - integrity sha512-ZfcEMMDp0BQ+yKmVp8ifPXLKej8pv9TcaRnmy2CZ07USD61E9LH5ClRAP/hxQuCyf/qLb5BPHsI7+f3K8uhj4Q== +"@mantine/notifications@^9.4.1": + version "9.4.1" + resolved "https://registry.yarnpkg.com/@mantine/notifications/-/notifications-9.4.1.tgz#c730908c11b59f09502153c573d36a05513b34c6" + integrity sha512-Sm1g8E8RkFmesxpNz9zFvOjkTVxOa7dJJjMGoHNIXH+9/wO0s9kInny4mVnxTbLJJsD6nTqIQlk1v8tq2C4Ftw== dependencies: - "@mantine/store" "9.1.1" + "@mantine/store" "9.4.1" react-transition-group "4.4.5" -"@mantine/spotlight@^9.1.1": - version "9.1.1" - resolved "https://registry.yarnpkg.com/@mantine/spotlight/-/spotlight-9.1.1.tgz#355890f6bfe8be68614626cbbd71b6872a8b4090" - integrity sha512-UWeL3ADxfMKltxSygdzoUlVH5PO9CbbEu41kXs5sBItU87SQxppyG35wCsYfZ0plIJ7/LBAjnl5lQcKWl8AOxA== +"@mantine/spotlight@^9.4.1": + version "9.4.1" + resolved "https://registry.yarnpkg.com/@mantine/spotlight/-/spotlight-9.4.1.tgz#f67c89bb2fa4a68c47385439c95fd892e74dfd60" + integrity sha512-gJ+7DdnGULeobtGXVGbVgvJSG3TeecxLXNxzpZo+ohl1OetPftCBGJOUrX2tYOOC6nJE1eHSIr/kpB31YIRjhQ== dependencies: - "@mantine/store" "9.1.1" + "@mantine/store" "9.4.1" -"@mantine/store@9.1.1": - version "9.1.1" - resolved "https://registry.yarnpkg.com/@mantine/store/-/store-9.1.1.tgz#09e63b31db5b3c0202b29ba052a751d4ad433396" - integrity sha512-kbxEU8wVGbobHlmQmk0lu9M+xCILKjuAPcMAshgzPznGLfXeE9zrB0gNT2cbk11Ik8dlV9J6Vsn9cuACyOSpfQ== +"@mantine/store@9.4.1": + version "9.4.1" + resolved "https://registry.yarnpkg.com/@mantine/store/-/store-9.4.1.tgz#c071271624c70acca1cb432d8c26d8a10c2ff452" + integrity sha512-/DCZq0aqqIdm03J0RJ7pbzoau2cwY8ndSOjskHOKHhTeWMnOBdCKXA3oMO5aIDldDqJ9T7Io4Boh4Pt461zNQw== "@monaco-editor/loader@^1.5.0": version "1.7.0" @@ -129,17 +129,17 @@ dependencies: "@monaco-editor/loader" "^1.5.0" -"@napi-rs/wasm-runtime@^1.1.4": - version "1.1.4" - resolved "https://registry.yarnpkg.com/@napi-rs/wasm-runtime/-/wasm-runtime-1.1.4.tgz#a46bbfedc29751b7170c5d23bc1d8ee8c7e3c1e1" - integrity sha512-3NQNNgA1YSlJb/kMH1ildASP9HW7/7kYnRI2szWJaofaS1hWmbGI4H+d3+22aGzXXN9IJ+n+GiFVcGipJP18ow== +"@napi-rs/wasm-runtime@^1.1.6": + version "1.1.6" + resolved "https://registry.yarnpkg.com/@napi-rs/wasm-runtime/-/wasm-runtime-1.1.6.tgz#ed33806d0f9be98dc76d0c3d4fd872fda701b5d5" + integrity sha512-ZLv/JdUfkvOy9eCnnBaGfiO+XimbjebAeO+MRQqD/B+FR1tnRN0tpKSJHRbE8sFfS6aqsXZ67TQjfwfsxULVbg== dependencies: - "@tybys/wasm-util" "^0.10.1" + "@tybys/wasm-util" "^0.10.3" -"@oxc-project/types@=0.128.0": - version "0.128.0" - resolved "https://registry.yarnpkg.com/@oxc-project/types/-/types-0.128.0.tgz#efc7524f948ff9e8ab1404ecad1823849c6fe149" - integrity sha512-huv1Y/LzBJkBVHt3OlC7u0zHBW9qXf1FdD7sGmc1rXc2P1mTwHssYv7jyGx5KAACSCH+9B3Bhn6Z9luHRvf7pQ== +"@oxc-project/types@=0.137.0": + version "0.137.0" + resolved "https://registry.yarnpkg.com/@oxc-project/types/-/types-0.137.0.tgz#56e77f8bb221fa05f18b1cd34d73f94f0954a773" + integrity sha512-WT+Gb24i8hmvo85AIv2oEYouEXkRlKAlT9WaCa3TfLgNCN+GhrJOGZuIlMouAh38Qe4QOx26eUOVsq70qXrywA== "@parcel/watcher-android-arm64@2.5.6": version "2.5.6" @@ -242,94 +242,89 @@ redux-thunk "^3.1.0" reselect "^5.1.0" -"@rolldown/binding-android-arm64@1.0.0-rc.18": - version "1.0.0-rc.18" - resolved "https://registry.yarnpkg.com/@rolldown/binding-android-arm64/-/binding-android-arm64-1.0.0-rc.18.tgz#3af8b2242086125934a85c1915b76e0a6a2054c1" - integrity sha512-lIDyUAfD7U3+BWKzdxMbJcsYHuqXqmGz40aeRqvuAm3y5TkJSYTBW2RDrn65DJFPQqVjUAUqq5uz8urzQ8aBdQ== - -"@rolldown/binding-darwin-arm64@1.0.0-rc.18": - version "1.0.0-rc.18" - resolved "https://registry.yarnpkg.com/@rolldown/binding-darwin-arm64/-/binding-darwin-arm64-1.0.0-rc.18.tgz#ae0b4467d24ecd6c6589f03d4d4699616ee9649c" - integrity sha512-apJq2ktnGp27nSInMR5Vcj8kY6xJzDAvfdIFlpDcAK/w4cDO58qVoi1YQsES/SKiFNge/6e4CUzgjfHduYqWpQ== - -"@rolldown/binding-darwin-x64@1.0.0-rc.18": - version "1.0.0-rc.18" - resolved "https://registry.yarnpkg.com/@rolldown/binding-darwin-x64/-/binding-darwin-x64-1.0.0-rc.18.tgz#23cf24b0a7b96c8990bbdd8a91e7fd3ba82b00e7" - integrity sha512-5Ofot8xbs+pxRHJqm9/9N/4sTQOvdrwEsmPE9pdLEEoAbdZtG6F2LMDfO1sp6ZAtXJuJV/21ew2srq3W8NXB5g== - -"@rolldown/binding-freebsd-x64@1.0.0-rc.18": - version "1.0.0-rc.18" - resolved "https://registry.yarnpkg.com/@rolldown/binding-freebsd-x64/-/binding-freebsd-x64-1.0.0-rc.18.tgz#a047a770f94dc451c062b729e5d1cf82e5c6f9c4" - integrity sha512-7h8eeOTT1eyqJyx64BFCnWZpNm486hGWt2sqeLLgDxA0xI1oGZ9H7gK1S85uNGmBhkdPwa/6reTxfFFKvIsebw== - -"@rolldown/binding-linux-arm-gnueabihf@1.0.0-rc.18": - version "1.0.0-rc.18" - resolved "https://registry.yarnpkg.com/@rolldown/binding-linux-arm-gnueabihf/-/binding-linux-arm-gnueabihf-1.0.0-rc.18.tgz#c0b7f346cbf50301cea669a4632bc63aabe6a72c" - integrity sha512-eRcm/HVt9U/JFu5RKAEKwGQYtDCKWLiaH6wOnsSEp6NMBb/3Os8LgHZlNyzMpFVNmiiMFlfb2zEnebfzJrHFmg== - -"@rolldown/binding-linux-arm64-gnu@1.0.0-rc.18": - version "1.0.0-rc.18" - resolved "https://registry.yarnpkg.com/@rolldown/binding-linux-arm64-gnu/-/binding-linux-arm64-gnu-1.0.0-rc.18.tgz#af56373c7996ebe6379207cd699c9f7f705e235d" - integrity sha512-SOrT/cT4ukTmgnrEz/Hg3m7LBnuCLW9psDeMKrimRWY4I8DmnO7Lco8W2vtqPmMkbVu8iJ+g4GFLVLLOVjJ9DQ== - -"@rolldown/binding-linux-arm64-musl@1.0.0-rc.18": - version "1.0.0-rc.18" - resolved "https://registry.yarnpkg.com/@rolldown/binding-linux-arm64-musl/-/binding-linux-arm64-musl-1.0.0-rc.18.tgz#a8f5acd21fcffc8991aa84710e3ae603c4240ea4" - integrity sha512-QWjdxN1HJCpBTAcZ5N5F7wju3gVPzRzSpmGzx7na0c/1qpN9CFil+xt+l9lV/1M6/gqHSNXCiqPfwhVJPeLnug== - -"@rolldown/binding-linux-ppc64-gnu@1.0.0-rc.18": - version "1.0.0-rc.18" - resolved "https://registry.yarnpkg.com/@rolldown/binding-linux-ppc64-gnu/-/binding-linux-ppc64-gnu-1.0.0-rc.18.tgz#1d4a89e040ff82141fc46e717cfab80b05f7c13f" - integrity sha512-ugCOyj7a4d9h3q9B+wXmf6g3a68UsjGh6dob5DHevHGMwDUbhsYNbSPxJsENcIttJZ9jv7qGM2UesLw5jqIhdg== - -"@rolldown/binding-linux-s390x-gnu@1.0.0-rc.18": - version "1.0.0-rc.18" - resolved "https://registry.yarnpkg.com/@rolldown/binding-linux-s390x-gnu/-/binding-linux-s390x-gnu-1.0.0-rc.18.tgz#97c21feeb2ed87d07820f0b2dcc5dd663e7a7f3b" - integrity sha512-kKWRhbsotpXkGbcd5dllUWg5gEXcDAa8u5YnP9AV5DYNbvJHGzzuwv7dpmhc8NqKMJldl0a+x76IHbspEpEmdA== - -"@rolldown/binding-linux-x64-gnu@1.0.0-rc.18": - version "1.0.0-rc.18" - resolved "https://registry.yarnpkg.com/@rolldown/binding-linux-x64-gnu/-/binding-linux-x64-gnu-1.0.0-rc.18.tgz#06310d40fe139ccc3c433b361120d337c66ebec2" - integrity sha512-uCo8ElcCIAMyYAZyuIZ81oFkhTSIllNvUCHCAlbhlN4ji3uC28h7IIdlXyIvGO7HsuqnV9p3rD/bpH7XhIyhRw== - -"@rolldown/binding-linux-x64-musl@1.0.0-rc.18": - version "1.0.0-rc.18" - resolved "https://registry.yarnpkg.com/@rolldown/binding-linux-x64-musl/-/binding-linux-x64-musl-1.0.0-rc.18.tgz#6a711258841f42609b238050cfcd5db13ac136d0" - integrity sha512-XNOQZtuE6yUIvx4rwGemwh8kpL1xvU41FXy/s9K7T/3JVcqGzo3NfKM2HrbrGgfPYGFW42f07Wk++aOC6B9NWA== - -"@rolldown/binding-openharmony-arm64@1.0.0-rc.18": - version "1.0.0-rc.18" - resolved "https://registry.yarnpkg.com/@rolldown/binding-openharmony-arm64/-/binding-openharmony-arm64-1.0.0-rc.18.tgz#15cb644beeafdbec930d79ed45c2a7c2573eac70" - integrity sha512-tSn/kzrfa7tNOXr7sEacDBN4YsIqTyLqh45IO0nHDwtpKIDNDJr+VFojt+4klSpChxB29JLyduSsE0MKEwa65A== - -"@rolldown/binding-wasm32-wasi@1.0.0-rc.18": - version "1.0.0-rc.18" - resolved "https://registry.yarnpkg.com/@rolldown/binding-wasm32-wasi/-/binding-wasm32-wasi-1.0.0-rc.18.tgz#ca3a56d11dfd533d743711141b3bb4c1ec10110e" - integrity sha512-+J9YGmc+czgqlhYmwun3S3O0FIZhsH8ep2456xwjAdIOmuJxM7xz4P4PtrxU+Bz17a/5bqPA8o3HAAoX0teUdg== - dependencies: - "@emnapi/core" "1.10.0" - "@emnapi/runtime" "1.10.0" - "@napi-rs/wasm-runtime" "^1.1.4" - -"@rolldown/binding-win32-arm64-msvc@1.0.0-rc.18": - version "1.0.0-rc.18" - resolved "https://registry.yarnpkg.com/@rolldown/binding-win32-arm64-msvc/-/binding-win32-arm64-msvc-1.0.0-rc.18.tgz#8c2117d68331d7de59d24631146d538fc203d27c" - integrity sha512-zsu47DgU0FQzSwi6sU9dZoEdUv7pc1AptSEz/Z8HBg54sV0Pbs3N0+CrIbTsgiu6EyoaNN9CHboqbLaz9lhOyQ== - -"@rolldown/binding-win32-x64-msvc@1.0.0-rc.18": - version "1.0.0-rc.18" - resolved "https://registry.yarnpkg.com/@rolldown/binding-win32-x64-msvc/-/binding-win32-x64-msvc-1.0.0-rc.18.tgz#bb5c28df3095046778cc1b020ef52fc5ee7b7e70" - integrity sha512-7H+3yqGgmnlDTRRhw/xpYY9J1kf4GC681nVc4GqKhExZTDrVVrV2tsOR9kso0fvgBdcTCcQShx4SLLoHgaLwhg== - -"@rolldown/pluginutils@1.0.0-rc.18": - version "1.0.0-rc.18" - resolved "https://registry.yarnpkg.com/@rolldown/pluginutils/-/pluginutils-1.0.0-rc.18.tgz#51cf2589596a179ebe8cbf313f1358c7b51a2fdc" - integrity sha512-CUY5Mnhe64xQBGZEEXQ5WyZwsc1JU3vAZLIxtrsBt3LO6UOb+C8GunVKqe9sT8NeWb4lqSaoJtp2xo6GxT1MNw== - -"@rolldown/pluginutils@1.0.0-rc.7": - version "1.0.0-rc.7" - resolved "https://registry.yarnpkg.com/@rolldown/pluginutils/-/pluginutils-1.0.0-rc.7.tgz#0414869467f0e471a6515d4f506c85fde867e022" - integrity sha512-qujRfC8sFVInYSPPMLQByRh7zhwkGFS4+tyMQ83srV1qrxL4g8E2tyxVVyxd0+8QeBM1mIk9KbWxkegRr76XzA== +"@rolldown/binding-android-arm64@1.1.3": + version "1.1.3" + resolved "https://registry.yarnpkg.com/@rolldown/binding-android-arm64/-/binding-android-arm64-1.1.3.tgz#cc6153029c3d9afc9caaae2dc362d899ae94ac4f" + integrity sha512-DT6Z3PhvioeHMvxo+xHc3KtqggrI7CCTXCmC2h/5zUlp5jVitv7XEy+9q5/7v8IolhlioawpMo8Kg0EEBy7J0g== + +"@rolldown/binding-darwin-arm64@1.1.3": + version "1.1.3" + resolved "https://registry.yarnpkg.com/@rolldown/binding-darwin-arm64/-/binding-darwin-arm64-1.1.3.tgz#3af681a5d7610340257b3ac7753353b23e884765" + integrity sha512-0NwgwsjM7LrsuVnXMK3koTpagBNOhloc/BNjKqZjv4V5zI5r13qx69uVhRx+o5Z0yy4Hzq+lpy7TAgUG/ocvrw== + +"@rolldown/binding-darwin-x64@1.1.3": + version "1.1.3" + resolved "https://registry.yarnpkg.com/@rolldown/binding-darwin-x64/-/binding-darwin-x64-1.1.3.tgz#80ada35e9f35efb7e48a887444ce2052f615d645" + integrity sha512-YtiBp4disu6V560loT6PjMdiRaWmVvDNrUunAalbiFx2ggeJwxdAsgZMcoGP17uyAsTwAj5V1niksxlHnVQ1Sw== + +"@rolldown/binding-freebsd-x64@1.1.3": + version "1.1.3" + resolved "https://registry.yarnpkg.com/@rolldown/binding-freebsd-x64/-/binding-freebsd-x64-1.1.3.tgz#65b2bbb82f005f08aeeff0b6d81e19be68360201" + integrity sha512-yD3EkEdXk2LypPxnf/kSZHirarsI8gcPzc62SukhR9VJTyvV+F9Q/GxWNuCojc7sXyuVC4DxRGhdDK4X8VSsbw== + +"@rolldown/binding-linux-arm-gnueabihf@1.1.3": + version "1.1.3" + resolved "https://registry.yarnpkg.com/@rolldown/binding-linux-arm-gnueabihf/-/binding-linux-arm-gnueabihf-1.1.3.tgz#7e8d34ad0c7bcfd3baed268e9798571e3888ca71" + integrity sha512-c+8vieQbsD7HNAHKIA34w0GJ9FedFFuJGD+7E6vz7Q3uqAIugL5p45fhlsj4UaAsHpcmlqugBWMhA0/j7o0sIg== + +"@rolldown/binding-linux-arm64-gnu@1.1.3": + version "1.1.3" + resolved "https://registry.yarnpkg.com/@rolldown/binding-linux-arm64-gnu/-/binding-linux-arm64-gnu-1.1.3.tgz#52bbf400ff219bda1e56c042160d96deb08bfecc" + integrity sha512-50jD0uUwLvur7Zz9LHz17kaAdTPjn5wN93hEgjvmYFRZwiR7ZJYovTd5ipyWJDAnXKvZ+wgc+/Ika6dwSF5OcA== + +"@rolldown/binding-linux-arm64-musl@1.1.3": + version "1.1.3" + resolved "https://registry.yarnpkg.com/@rolldown/binding-linux-arm64-musl/-/binding-linux-arm64-musl-1.1.3.tgz#9e82899186f73329f3d8155fa1618ae2e86ffa2a" + integrity sha512-BO9+oPL8K9poZJBfYPsXNtYjPE5uM3qeehT3aFcW4LITOl+iSqhp0abzjR2nWBUNjIZeKXjAEWBZ64WjNoHd6w== + +"@rolldown/binding-linux-ppc64-gnu@1.1.3": + version "1.1.3" + resolved "https://registry.yarnpkg.com/@rolldown/binding-linux-ppc64-gnu/-/binding-linux-ppc64-gnu-1.1.3.tgz#050520177316586ccad816eb466ea11015e17ba7" + integrity sha512-f3VpLB1vQ0Eo6ecr/6cekLnvYMFF4YBFoVGkfkvPLq1bAkbAwHYQPZKoAmG6OJyTcxxoC+AvezGx/S1obNC0Mw== + +"@rolldown/binding-linux-s390x-gnu@1.1.3": + version "1.1.3" + resolved "https://registry.yarnpkg.com/@rolldown/binding-linux-s390x-gnu/-/binding-linux-s390x-gnu-1.1.3.tgz#50efa7b20219c6e31235fded0fd3427f36123e5a" + integrity sha512-AmurZ26Pqx/RI9N1gzEOCklkKXl927yjfXWUUS0O7Puh8ARM/Ob8qfrD3qnWksScdw6cSrW5PSHE9DyLu7+PtA== + +"@rolldown/binding-linux-x64-gnu@1.1.3": + version "1.1.3" + resolved "https://registry.yarnpkg.com/@rolldown/binding-linux-x64-gnu/-/binding-linux-x64-gnu-1.1.3.tgz#c5973445113dff50d4077d0edaa4b8a69533dc6f" + integrity sha512-JJpqs8bRGITDOdbkNKnlojzBabbOHrqjSvDr0IVsZObE1lBcPjxItUEY9eWIDbxaJ3cGrXPWGfGkIxFijg/URg== + +"@rolldown/binding-linux-x64-musl@1.1.3": + version "1.1.3" + resolved "https://registry.yarnpkg.com/@rolldown/binding-linux-x64-musl/-/binding-linux-x64-musl-1.1.3.tgz#ddb023f7fc98ccbb8c1b683545216ca7b4e7ebdd" + integrity sha512-rSJcdjPxzA/by/6/rYs+v+bXU7UjvnbUWz8MJb6kh6+knqB1dCrtHg0uu7C/4haqJvqdkYHQ5IGn+tCH9GLW/g== + +"@rolldown/binding-openharmony-arm64@1.1.3": + version "1.1.3" + resolved "https://registry.yarnpkg.com/@rolldown/binding-openharmony-arm64/-/binding-openharmony-arm64-1.1.3.tgz#832a6da3472722427c73d178c75681858b76aeed" + integrity sha512-hQ3/PYkDJICgevvyNcVrihVeqq7k1Pp3VZ9lY+dauAYUJKO+auqApvANhvR1An9BhmqYKvW2Mu1F9u4DXSMLxQ== + +"@rolldown/binding-wasm32-wasi@1.1.3": + version "1.1.3" + resolved "https://registry.yarnpkg.com/@rolldown/binding-wasm32-wasi/-/binding-wasm32-wasi-1.1.3.tgz#256cdcc06ad9ada611606526f319642fc0830b0f" + integrity sha512-Elcv/BtML9lXrV6JuKITc/grN2kYV9gjsQpW8Jfw4ioK0TOkjBjye0nnyqQNy9STNaI20lXNaQBRrD5gSgR0Yg== + dependencies: + "@emnapi/core" "1.11.1" + "@emnapi/runtime" "1.11.1" + "@napi-rs/wasm-runtime" "^1.1.6" + +"@rolldown/binding-win32-arm64-msvc@1.1.3": + version "1.1.3" + resolved "https://registry.yarnpkg.com/@rolldown/binding-win32-arm64-msvc/-/binding-win32-arm64-msvc-1.1.3.tgz#e735c7024a5e17ebaf13689112fa0bdfc6886c38" + integrity sha512-2DrEfhluH9yhiaFApmsjsjwrSYbNcY1oFTzYSP1a535jDbV98zCFanA/96TBUd0iDFcxGmw9QRExwGCXz3U+/g== + +"@rolldown/binding-win32-x64-msvc@1.1.3": + version "1.1.3" + resolved "https://registry.yarnpkg.com/@rolldown/binding-win32-x64-msvc/-/binding-win32-x64-msvc-1.1.3.tgz#f06c09db5c8ad4b6904b4d406c9b6f17f392b5c6" + integrity sha512-OL4OMk7UPXOeVGGd3qo5zJyPIljf4AFgk5QAkPPS+OoLuOOozhuaQGC18MxVTnw/06q93gShAJzlwnSCY9YtqA== + +"@rolldown/pluginutils@^1.0.0", "@rolldown/pluginutils@^1.0.1": + version "1.0.1" + resolved "https://registry.yarnpkg.com/@rolldown/pluginutils/-/pluginutils-1.0.1.tgz#e3fcee093fbb5ce765e1ad088ff4de2889f6f9be" + integrity sha512-2j9bGt5Jh8hj+vPtgzPtl72j0yRxHAyumoo6TNfAjsLB04UtpSvPbPcDcBMxz7n+9CYB0c1GxQFxYRg2jimqGw== "@standard-schema/spec@^1.0.0", "@standard-schema/spec@^1.1.0": version "1.1.0" @@ -341,17 +336,17 @@ resolved "https://registry.yarnpkg.com/@standard-schema/utils/-/utils-0.3.0.tgz#3d5e608f16c2390c10528e98e59aef6bf73cae7b" integrity sha512-e7Mew686owMaPJVNNLs55PUvgz371nKgwsc4vxE49zsODpJEnxgxRo2y/OKrqueavXgZNMDVj3DdHFlaSAeU8g== -"@tanstack/query-core@5.100.9": - version "5.100.9" - resolved "https://registry.yarnpkg.com/@tanstack/query-core/-/query-core-5.100.9.tgz#dcf44ef25cf42a4da229bcab1d8d33e80a740a99" - integrity sha512-SJSFw1S8+kQ0+knv/XGfrbocWoAlT7vDKsSImtLx3ZPQmEcR46hkDjLSvynSy25N8Ms4tIEini1FuBd5k7IscQ== +"@tanstack/query-core@5.101.2": + version "5.101.2" + resolved "https://registry.yarnpkg.com/@tanstack/query-core/-/query-core-5.101.2.tgz#c901fb76b68169ff5e1e44017404e7e2e90ce1af" + integrity sha512-hH5MLoJhF7KaIGd7q3xTXGXvslI+GYlM1Z/35aSHHWaCJWB7XvTSHYuV3eM7tw+aE0mT/xMro4M4Q9rCGHT0lw== -"@tanstack/react-query@^5.100.9": - version "5.100.9" - resolved "https://registry.yarnpkg.com/@tanstack/react-query/-/react-query-5.100.9.tgz#0c701bf56f38b484602255a92d4c9e452a04807d" - integrity sha512-Oa44XkaI3kCNN6ME0KByU3xT3SEUNOMfZpHxL6+wFoTm+OeUFYHKdeYVe0aOXlRDm/f15sgLwEt2HDorIdW8+A== +"@tanstack/react-query@^5.101.2": + version "5.101.2" + resolved "https://registry.yarnpkg.com/@tanstack/react-query/-/react-query-5.101.2.tgz#d886331bcd29df84bc04c45d93192b8207d17c7f" + integrity sha512-seDkr6kzGzX1okaaTtZPtgA688CDPlXUz1C6xSg0ESqn04Vuc8tlrYms1s3de+znBqhPVxFRfpAfUf+6XvfPWg== dependencies: - "@tanstack/query-core" "5.100.9" + "@tanstack/query-core" "5.101.2" "@tanstack/react-table@^8.21.3": version "8.21.3" @@ -365,10 +360,10 @@ resolved "https://registry.yarnpkg.com/@tanstack/table-core/-/table-core-8.21.3.tgz#2977727d8fc8dfa079112d9f4d4c019110f1732c" integrity sha512-ldZXEhOBb8Is7xLs01fR3YEc3DERiz5silj8tnGkFZytt1abEvl/GhUmCE0PMLaMPTa3Jk4HbKmRlHmu+gCftg== -"@tybys/wasm-util@^0.10.1": - version "0.10.1" - resolved "https://registry.yarnpkg.com/@tybys/wasm-util/-/wasm-util-0.10.1.tgz#ecddd3205cf1e2d5274649ff0eedd2991ed7f414" - integrity sha512-9tTaPJLSiejZKx+Bmog4uSubteqTvFrVrURwkmHixBo0G4seD0zUxp98E1DzUBJxLQ3NPwXrGKDiVjwx/DpPsg== +"@tybys/wasm-util@^0.10.3": + version "0.10.3" + resolved "https://registry.yarnpkg.com/@tybys/wasm-util/-/wasm-util-0.10.3.tgz#015cba9e9dd47ce14d03d2a8c5d547bfb169665d" + integrity sha512-F3fo1MYrRJYL3zER0OUOmkutjr1Vp23m7OsSgp7nq4SP6OqX6C/56XFIPAl5bt3zaBRjmW7SGz3u/6LwFpYcOg== dependencies: tslib "^2.4.0" @@ -423,22 +418,22 @@ resolved "https://registry.yarnpkg.com/@types/d3-timer/-/d3-timer-3.0.2.tgz#70bbda77dc23aa727413e22e214afa3f0e852f70" integrity sha512-Ps3T8E8dZDam6fUyNiMkekK3XUsaUEik+idO9/YjPtfj2qruF8tFBXS7XhtE4iIXBLxhmLjP3SXpLhVf21I9Lw== -"@types/node@^25.6.2": - version "25.6.2" - resolved "https://registry.yarnpkg.com/@types/node/-/node-25.6.2.tgz#8c491201373690e4ef2a2ffed0dfb510a5830b92" - integrity sha512-sokuT28dxf9JT5Kady1fsXOvI4HVpjZa95NKT5y9PNTIrs2AsobR4GFAA90ZG8M+nxVRLysCXsVj6eGC7Vbrlw== +"@types/node@^26.0.1": + version "26.0.1" + resolved "https://registry.yarnpkg.com/@types/node/-/node-26.0.1.tgz#4a60e2c7a6d68bd261e265f8983bfe1601263ce3" + integrity sha512-fc3KiUoBt6kie0N9bIW3E47vZsuaMf0PM2AaUpLCLT0s/LvX1nxAim6Fc049cNxODPpGm6qRAuUOB86SkRuPQw== dependencies: - undici-types "~7.19.0" + undici-types "~8.3.0" "@types/react-dom@^19.2.3": version "19.2.3" resolved "https://registry.yarnpkg.com/@types/react-dom/-/react-dom-19.2.3.tgz#c1e305d15a52a3e508d54dca770d202cb63abf2c" integrity sha512-jp2L/eY6fn+KgVVQAOqYItbF0VY/YApe5Mz2F0aykSO8gx31bYCZyvSeYxCHKvzHG5eZjc+zyaS5BrBWya2+kQ== -"@types/react@^19.2.14": - version "19.2.14" - resolved "https://registry.yarnpkg.com/@types/react/-/react-19.2.14.tgz#39604929b5e3957e3a6fa0001dafb17c7af70bad" - integrity sha512-ilcTH/UniCkMdtexkoCN0bI7pMcJDvmQFPvuPvmEaYA/NSfFTAgdUSLAoVjaRJm7+6PvcM+q1zYOwS4wTYMF9w== +"@types/react@^19.2.17": + version "19.2.17" + resolved "https://registry.yarnpkg.com/@types/react/-/react-19.2.17.tgz#dccac365baa0f1734ec270ff4b51c89465e8dc7f" + integrity sha512-MXfmqaVPEVgkBT/aY0aGCkRWWtByiYQXo3xdQ8r5RzuFrPiRn8Gar2tQdXSUQ2GKV3bkXckek89V8wQBY2Q/Aw== dependencies: csstype "^3.2.2" @@ -464,12 +459,117 @@ resolved "https://registry.yarnpkg.com/@types/use-sync-external-store/-/use-sync-external-store-0.0.6.tgz#60be8d21baab8c305132eb9cb912ed497852aadc" integrity sha512-zFDAD+tlpf2r4asuHEj0XH6pY6i0g5NeAHPn+15wk3BV6JA69eERFXC1gyGThDkVa1zCyKr5jox1+2LbV/AMLg== -"@vitejs/plugin-react@^6.0.1": - version "6.0.1" - resolved "https://registry.yarnpkg.com/@vitejs/plugin-react/-/plugin-react-6.0.1.tgz#d9113b71a0a592714913eafd9e5e63bcafd0ff15" - integrity sha512-l9X/E3cDb+xY3SWzlG1MOGt2usfEHGMNIaegaUGFsLkb3RCn/k8/TOXBcab+OndDI4TBtktT8/9BwwW8Vi9KUQ== +"@typescript/typescript-aix-ppc64@7.0.2": + version "7.0.2" + resolved "https://registry.yarnpkg.com/@typescript/typescript-aix-ppc64/-/typescript-aix-ppc64-7.0.2.tgz#cdc7ce81d60f1e09034960ddfb1fb880d7a776b6" + integrity sha512-MTKKkWB7p/0E9xi1d1tHtZ5PiLkGEMIq88pK2CubZjOsLtYTLqhgIgi6zepFa+9GHZ6h05NMCkQxGKiPXMxXtQ== + +"@typescript/typescript-darwin-arm64@7.0.2": + version "7.0.2" + resolved "https://registry.yarnpkg.com/@typescript/typescript-darwin-arm64/-/typescript-darwin-arm64-7.0.2.tgz#a55fdfcfa58df58d27db2237cde6a5c1e35a7235" + integrity sha512-gowzar9MwS/aRWp6f3a4KUqzRjAZjOsmGNCM6LcTgXum+dBfgsBVMN+AgvOCCbguXyick6LJhpBszxMebJ8syA== + +"@typescript/typescript-darwin-x64@7.0.2": + version "7.0.2" + resolved "https://registry.yarnpkg.com/@typescript/typescript-darwin-x64/-/typescript-darwin-x64-7.0.2.tgz#38d1c9172800a91d707bec64d2a370a016634db4" + integrity sha512-SZ9xZInqApNlNGc9s0W1VSsktYSOe9cFqNOIqmN1Gs8SmkjKZYFt017G4VwPxASInODuAdbTW7sXiFUf893RgA== + +"@typescript/typescript-freebsd-arm64@7.0.2": + version "7.0.2" + resolved "https://registry.yarnpkg.com/@typescript/typescript-freebsd-arm64/-/typescript-freebsd-arm64-7.0.2.tgz#f1ff8810030b35d2b5be0db6a2dc650460ea94fa" + integrity sha512-W5NH4y/J0plIIS5b2xvTEkU7JFxyqdMAOgf+Ilhl0vHQXKO5dZoxd+C/jEtq56c4F3wk71RB4BMRQ2XdI+bwYQ== + +"@typescript/typescript-freebsd-x64@7.0.2": + version "7.0.2" + resolved "https://registry.yarnpkg.com/@typescript/typescript-freebsd-x64/-/typescript-freebsd-x64-7.0.2.tgz#3d86b03f353c5b1ba95162eb6ce35533bfc294bd" + integrity sha512-UMGDx5sTpzNw3WiPebH7l90IWfJggEd+egHt/q6p7/Cm3zqoV7VxkGXt+3DxPIw8CcmvAB0j3sVVfbhX+M4Tpw== + +"@typescript/typescript-linux-arm64@7.0.2": + version "7.0.2" + resolved "https://registry.yarnpkg.com/@typescript/typescript-linux-arm64/-/typescript-linux-arm64-7.0.2.tgz#d9334d96d6dac6ff85da9c865588948de939e91f" + integrity sha512-Qh4eU4/y3yDjnfjjyPYihMj5/ODIlmt+Bzu17OI+fiSRDW57QmU5SiN63exPRNJPKUzcc1INa1NXdrJ+MqHjUQ== + +"@typescript/typescript-linux-arm@7.0.2": + version "7.0.2" + resolved "https://registry.yarnpkg.com/@typescript/typescript-linux-arm/-/typescript-linux-arm-7.0.2.tgz#ad94b41e1aee2a4dcc6a298c7b67c43345fde32e" + integrity sha512-gffT3xPz9sR7j/YJExkyPntrI0P2EP9XbOyWzth2/Gs0RstK+90RBcO0ncXoXy/beYll1SXw846Nf2zdnEz0QQ== + +"@typescript/typescript-linux-loong64@7.0.2": + version "7.0.2" + resolved "https://registry.yarnpkg.com/@typescript/typescript-linux-loong64/-/typescript-linux-loong64-7.0.2.tgz#2965aee4fc873360139d893daafe6397a29138ad" + integrity sha512-uEHck9i8hoAzXPiYRib1O7miOnz23SxIeVl6F4LXox+qov1K35jHcEW6VHKvZI+pyvl7fZEP4MCU5LYvIq1GuQ== + +"@typescript/typescript-linux-mips64el@7.0.2": + version "7.0.2" + resolved "https://registry.yarnpkg.com/@typescript/typescript-linux-mips64el/-/typescript-linux-mips64el-7.0.2.tgz#1a887a311bed3a833f80bfd4a9ed37c271936cf0" + integrity sha512-R4KvAMnE43W5Qeqb0Ly56O3mWMWIAgsMyz36DCaycd5nbg/9kzm0liw3JocfRqyJY0KPmzFjbswozXyW0DnIYA== + +"@typescript/typescript-linux-ppc64@7.0.2": + version "7.0.2" + resolved "https://registry.yarnpkg.com/@typescript/typescript-linux-ppc64/-/typescript-linux-ppc64-7.0.2.tgz#8b63c9b2f445b393eb4e43ec21da225dade3577d" + integrity sha512-DORx5b3sd/4S7eayxm4FQv+A7CrkUIGRaHiwI8oiHTAI1fAPWhF4J0vAlkC8biAlHSVVwxMQ3tjZ2/DVbnQiiA== + +"@typescript/typescript-linux-riscv64@7.0.2": + version "7.0.2" + resolved "https://registry.yarnpkg.com/@typescript/typescript-linux-riscv64/-/typescript-linux-riscv64-7.0.2.tgz#b6e8a35c289b3ea97a92a41d461aaeed0d3b36e1" + integrity sha512-wf0jqEDOjrPRnKwYRyyJDRo11KMbvMFrU+q4zqKyChODBzvlkbhNQfKvLxQCcwTpdDaXSHZTVuh0JoCrKCUMHQ== + +"@typescript/typescript-linux-s390x@7.0.2": + version "7.0.2" + resolved "https://registry.yarnpkg.com/@typescript/typescript-linux-s390x/-/typescript-linux-s390x-7.0.2.tgz#2ef96693be4861f6d17965427e5b009cbbed1a3e" + integrity sha512-IkwJc3L7yhytWd/ewjyxNDfOmswCm9GWMJT/ue/dU4aZNbwZeYAetq42VyLmsmSjvoX7z74X6ZaYCtzAr0EuGw== + +"@typescript/typescript-linux-x64@7.0.2": + version "7.0.2" + resolved "https://registry.yarnpkg.com/@typescript/typescript-linux-x64/-/typescript-linux-x64-7.0.2.tgz#73269cb0baba50aea0ca060445a6b88e583f1ce2" + integrity sha512-EYdf2cNg7rgCWJnxCdJ+F3V39O8ihb37eHAu1LK8oAFizgTQbPOK7zHHXbPt8rX24COqODXeI3sIf0fCXG7H/A== + +"@typescript/typescript-netbsd-arm64@7.0.2": + version "7.0.2" + resolved "https://registry.yarnpkg.com/@typescript/typescript-netbsd-arm64/-/typescript-netbsd-arm64-7.0.2.tgz#3a3649f97fafa210b4e6e3798c15e06605c8a901" + integrity sha512-+polYF4MF04aPpO5FTkHran9yUQDSXqy5GiSDKpsll5jy3l3+g9QLhpf39T+ePtefhXLOGrLl0QIjkQP6VnelA== + +"@typescript/typescript-netbsd-x64@7.0.2": + version "7.0.2" + resolved "https://registry.yarnpkg.com/@typescript/typescript-netbsd-x64/-/typescript-netbsd-x64-7.0.2.tgz#47ec59491a40c470d2807dc4d2b825528fd979ab" + integrity sha512-8YIT0EHM/3dq10ZOVF/A7pc/YSMtbcecct4rWtexrnSCHOPcpC2KTLXfTCR6vDpnSiY12heNb1GiN/wu+T/FyA== + +"@typescript/typescript-openbsd-arm64@7.0.2": + version "7.0.2" + resolved "https://registry.yarnpkg.com/@typescript/typescript-openbsd-arm64/-/typescript-openbsd-arm64-7.0.2.tgz#796be8da0bd989d8a3fb96f2801e38a8365b4baf" + integrity sha512-APT8+ClYnuYm1u9+kgGXoMj2VzWzcymwh2gNSQVySHfkRDGOTVkoWLjCmOQSaO+PoqQ57B0flRp9SA+7GnnkzQ== + +"@typescript/typescript-openbsd-x64@7.0.2": + version "7.0.2" + resolved "https://registry.yarnpkg.com/@typescript/typescript-openbsd-x64/-/typescript-openbsd-x64-7.0.2.tgz#d37fe2a729eb942c076c454ee7f1815faf7d560f" + integrity sha512-yX7s+Q0Dln0Dt9tEzZsAjXXR/+ytBM7AlglaqyeMPxQszJ1JhlJdZ6jLA+IzldHtflX81em7lDao1xXu+aRRkg== + +"@typescript/typescript-sunos-x64@7.0.2": + version "7.0.2" + resolved "https://registry.yarnpkg.com/@typescript/typescript-sunos-x64/-/typescript-sunos-x64-7.0.2.tgz#aba8d3464c3565a7044789baba96916bd4ab2c88" + integrity sha512-dLJDGaLZ1D4HPQn62u1n8mBDkJREwMsAkCdkwd4Ieqw+x3TUyTsqY0YiBCtE6H6OzzgGk3iuZ3vFWRS+E8/d1g== + +"@typescript/typescript-win32-arm64@7.0.2": + version "7.0.2" + resolved "https://registry.yarnpkg.com/@typescript/typescript-win32-arm64/-/typescript-win32-arm64-7.0.2.tgz#b9de50a17196383f62620b5f9d0a2f34ad3b60d7" + integrity sha512-Gyl1Vy6OsWesLzmq+EP0Fb7b4Nid5232AvcA2SFcdYreldpNtYFFofPjnt62y9hQy7VTaZp65ICJjuAQRaVcIQ== + +"@typescript/typescript-win32-x64@7.0.2": + version "7.0.2" + resolved "https://registry.yarnpkg.com/@typescript/typescript-win32-x64/-/typescript-win32-x64-7.0.2.tgz#cf3b7b0d6ce5635daca4c8e01c189cdcde47ec3c" + integrity sha512-0BQ3HkAHHlKLSp1qRvf3SUhGpGsDuhB/jgFw75guyqbxJqEaS0Cw/VFO8i2nHglJUzQCRtMMR/IBAKE3ETMC4g== + +"@vitejs/plugin-react@^6.0.3": + version "6.0.3" + resolved "https://registry.yarnpkg.com/@vitejs/plugin-react/-/plugin-react-6.0.3.tgz#55f1d7f558534d10aef03c007dc208b7c3771ce4" + integrity sha512-vmFvco5/QuC2f9Oj+wTk0+9XeDFkHxSamwZKYc7MxYwKICfvUvlMhqKI0VuICPltGqh1neqBKDvO4kes1ya8vg== dependencies: - "@rolldown/pluginutils" "1.0.0-rc.7" + "@rolldown/pluginutils" "^1.0.1" + +"@vscode/l10n@^0.0.18": + version "0.0.18" + resolved "https://registry.yarnpkg.com/@vscode/l10n/-/l10n-0.0.18.tgz#916d3a5e960dbab47c1c56f58a7cb5087b135c95" + integrity sha512-KYSIHVmslkaCDyw013pphY+d7x1qV8IZupYfeIfzNA+nsaWHbn5uPuQRvdRFsa9zFzGeudPuoGoZ1Op4jrJXIQ== "@xterm/addon-fit@^0.11.0": version "0.11.0" @@ -493,12 +593,12 @@ camelcase-css@^2.0.1: resolved "https://registry.yarnpkg.com/camelcase-css/-/camelcase-css-2.0.1.tgz#ee978f6947914cc30c6b44741b6ed1df7f043fd5" integrity sha512-QOSvevhslijgYwRx6Rv7zKdMF8lbRmx+uQGx2+vDc+KI/eBnsy9kit5aj23AgGu3pa4t9AgwbnXWqS+iOY+2aA== -chokidar@^4.0.0: - version "4.0.3" - resolved "https://registry.yarnpkg.com/chokidar/-/chokidar-4.0.3.tgz#7be37a4c03c9aee1ecfe862a4a23b2c70c205d30" - integrity sha512-Qgzu8kfBvo+cA4962jnP1KkS6Dop5NS6g7R5LFYJr4b8Ub94PPQXUksCw9PvXoeXPRRddRNC5C1JQUR2SMGtnA== +chokidar@^5.0.0: + version "5.0.0" + resolved "https://registry.yarnpkg.com/chokidar/-/chokidar-5.0.0.tgz#949c126a9238a80792be9a0265934f098af369a5" + integrity sha512-TQMmc3w+5AxjpL8iIiwebF73dRDF4fBIieAqGn9RGCWaEVwQ6Fb2cGe31Yns0RRIzii5goJ1Y7xbMwo1TxMplw== dependencies: - readdirp "^4.0.1" + readdirp "^5.0.0" clsx@^2.1.1: version "2.1.1" @@ -596,6 +696,11 @@ d3-timer@^3.0.1: resolved "https://registry.yarnpkg.com/d3-timer/-/d3-timer-3.0.1.tgz#6284d2a2708285b1abb7e201eda4380af35e63b0" integrity sha512-ndfJ/JxxMd3nw31uyKoY2naivF+r29V+Lc0svZxe1JvvIRmi8hUsrMvdOwgS1o6uBHmiz91geQ0ylPP0aj1VUA== +dayjs@^1.11.7: + version "1.11.21" + resolved "https://registry.yarnpkg.com/dayjs/-/dayjs-1.11.21.tgz#57f87562e62de76f3c704bd2b8d522fc33068eb2" + integrity sha512-98IT+HOahAisibz/yjKbzuOBwYcjJ7BCLPzARyHiyEBmRz4fatF+KPJszEHXsGYjUG234aH/cOjW1wwTbKUZlA== + decimal.js-light@^2.5.1: version "2.5.1" resolved "https://registry.yarnpkg.com/decimal.js-light/-/decimal.js-light-2.5.1.tgz#134fd32508f19e208f4fb2f8dac0d2626a867934" @@ -645,10 +750,10 @@ domhandler@^5.0.2, domhandler@^5.0.3: dependencies: domelementtype "^2.3.0" -dompurify@3.2.7: - version "3.2.7" - resolved "https://registry.yarnpkg.com/dompurify/-/dompurify-3.2.7.tgz#721d63913db5111dd6dfda8d3a748cfd7982d44a" - integrity sha512-WhL/YuveyGXJaerVlMYGWhvQswa7myDG17P7Vu65EWC05o8vfeNbvNf4d/BOvH99+ZW+LlQsc1GDKMa1vNK6dw== +dompurify@3.4.8: + version "3.4.8" + resolved "https://registry.yarnpkg.com/dompurify/-/dompurify-3.4.8.tgz#6c54f8c207160e7f83fcb7f4fd05a82ac36b1cdc" + integrity sha512-yb1cEmaOum7wFvOCSQxyfgVlv5D47Rc30iZWoMpbDIWTnJ6grDDQyu2KFJzB2k7u0pMuJcQ1zphH//fFnw2tjQ== optionalDependencies: "@types/trusted-types" "^2.0.7" @@ -731,16 +836,16 @@ htmlparser2@^10.1, htmlparser2@^10.1.0: domutils "^3.2.2" entities "^7.0.1" -immer@^10.1.1: - version "10.2.0" - resolved "https://registry.yarnpkg.com/immer/-/immer-10.2.0.tgz#88a4ce06a1af64172d254b70f7cb04df51c871b1" - integrity sha512-d/+XTN3zfODyjr89gM3mPq1WNX2B8pYsu7eORitdwyA2sBubnTl3laYlBk4sXY5FUa5qTZGBDPJICVbvqzjlbw== - immer@^11.0.0: version "11.1.4" resolved "https://registry.yarnpkg.com/immer/-/immer-11.1.4.tgz#37aee86890b134a8f1a2fadd44361fb86c6ae67e" integrity sha512-XREFCPo6ksxVzP4E0ekD5aMdf8WMwmdNaz6vuvxgI40UaEiu6q3p8X52aU6GdyvLY3XXX/8R7JOTXStz/nBbRw== +immer@^11.1.8: + version "11.1.8" + resolved "https://registry.yarnpkg.com/immer/-/immer-11.1.8.tgz#08a6426f7019dbce8d6dff8c4a43bb25c550a575" + integrity sha512-/tbkHMW7y10Lx6i1crLjD4/OhNkRG+Fo7byZHtah0547nIeXYcpIXaUh0IAQY6gO5459qpGGYapcEOHtFXkIuA== + immutable@^5.1.5: version "5.1.5" resolved "https://registry.yarnpkg.com/immutable/-/immutable-5.1.5.tgz#93ee4db5c2a9ab42a4a783069f3c5d8847d40165" @@ -768,20 +873,20 @@ is-plain-object@^5.0.0: resolved "https://registry.yarnpkg.com/is-plain-object/-/is-plain-object-5.0.0.tgz#4427f50ab3429e9025ea7d52e9043a9ef4159344" integrity sha512-VRSzKkbMm5jMDoKLbltAkFQ5Qr7VDiTFGXxYFXXowVj387GeGNOCsOH6Msy00SGZ3Fp84b1Naa1psqgcCIEP5Q== -jotai-family@^1.0.1: - version "1.0.1" - resolved "https://registry.yarnpkg.com/jotai-family/-/jotai-family-1.0.1.tgz#0782aaaf22042b63111df4c1f9acf7b2ceff03cb" - integrity sha512-Zb/79GNDhC/z82R+6qTTpeKW4l4H6ZCApfF5W8G4SH37E4mhbysU7r8DkP0KX94hWvjB/6lt/97nSr3wB+64Zg== +jotai-family@^1.0.2: + version "1.0.2" + resolved "https://registry.yarnpkg.com/jotai-family/-/jotai-family-1.0.2.tgz#11d9f88a39579aaf0d80dcd8826b8c32cf6b5269" + integrity sha512-U1aTMGxmsmz2Z8gaJD1/ljmMnsmG4/dqrcsfwGbjWV7p6boB9Vy3+75YwUpwPj7GbLNJk9O8rl1VNi8d7+6Rxw== jotai-location@^0.6.2: version "0.6.2" resolved "https://registry.yarnpkg.com/jotai-location/-/jotai-location-0.6.2.tgz#7f90ec768d9aca62ce7d509baa771848225edb39" integrity sha512-D+fqGNNgPHpehOsGBsYsS7F3kNgml099MJrGYt8YlLumcnoTVrBdcO+Y11KyMlyzwPihEN0HPUgke9Iihz9OXw== -jotai@^2.20.0: - version "2.20.0" - resolved "https://registry.yarnpkg.com/jotai/-/jotai-2.20.0.tgz#0e3422f6f7141758e8212d72338f9cf3bef5fd45" - integrity sha512-b5GAqgmXmXzB4WPaTH26ppk9Sl7AA9WSQX7yfdM+gJ1rFROiWcVbi97gFuN/yVCojOcbcvop2sfLL+fjxW0JVg== +jotai@^2.20.1: + version "2.20.1" + resolved "https://registry.yarnpkg.com/jotai/-/jotai-2.20.1.tgz#473274f1b78c60acce1b868be5655b3c706dec8e" + integrity sha512-dnuKfU/GLi8B28RRMjQ3AfoN7kfzP8o41+AX2FmITZqEMY8PHnjABq+VkEooomLwYaGjda+pgy0yFSjaHX/ZPg== "js-tokens@^3.0.0 || ^4.0.0": version "4.0.0" @@ -803,6 +908,13 @@ klona@^2.0.6: resolved "https://registry.yarnpkg.com/klona/-/klona-2.0.6.tgz#85bffbf819c03b2f53270412420a4555ef882e22" integrity sha512-dhG34DXATL5hSxJbIexCft8FChFXtmskoZYnoPWjXQuebWYCNkVeV3KkGegCK9CP1oswI/vQibS2GY7Em/sJJA== +launder@^1.7.1: + version "1.7.1" + resolved "https://registry.yarnpkg.com/launder/-/launder-1.7.1.tgz#ef7155ab0c3ddec2323089c961d2e9a249aa5b0d" + integrity sha512-mU6WRz5EusL9ZZuiZ5SO4Y6C0P9PAUR9iwdb6bzj4KDihm28DiHFw+/yk9DBH4f+Pv1wuzQ4e2jV3oQ7mkIqvw== + dependencies: + dayjs "^1.11.7" + lightningcss-android-arm64@1.32.0: version "1.32.0" resolved "https://registry.yarnpkg.com/lightningcss-android-arm64/-/lightningcss-android-arm64-1.32.0.tgz#f033885116dfefd9c6f54787523e3514b61e1968" @@ -884,34 +996,34 @@ loose-envify@^1.4.0: dependencies: js-tokens "^3.0.0 || ^4.0.0" -lucide-react@^1.14.0: - version "1.14.0" - resolved "https://registry.yarnpkg.com/lucide-react/-/lucide-react-1.14.0.tgz#3c3867749f5ff4eeb5a6f423557ec6a50521366f" - integrity sha512-+1mdWcfSJVUsaTIjN9zoezmUhfXo5l0vP7ekBMPo3jcS/aIkxHnXqAPsByszMZx/Y8oQBRJxJx5xg+RH3urzxA== +lucide-react@^1.22.0: + version "1.22.0" + resolved "https://registry.yarnpkg.com/lucide-react/-/lucide-react-1.22.0.tgz#f2930b21a1d3941fbee8a1017a5973d06ec4860b" + integrity sha512-c9o3l0PiNcgOQDW4F31BEYHudE7kgxVt3o30qMl36ZPwTxXlGB4QnLilhERvVM4uh/pl5MDyY1/gzZSYcHDtBg== marked@14.0.0: version "14.0.0" resolved "https://registry.yarnpkg.com/marked/-/marked-14.0.0.tgz#79a1477358a59e0660276f8fec76de2c33f35d83" integrity sha512-uIj4+faQ+MgHgwUW1l2PsPglZLOLOT1uErt06dAPtx2kjteLAkbsd/0FiYg/MGS+i7ZKLb7w2WClxHkzOOuryQ== -mogh_auth_client@^1.5.0: - version "1.5.0" - resolved "https://registry.yarnpkg.com/mogh_auth_client/-/mogh_auth_client-1.5.0.tgz#f54b8ae35648d5bf1e60d7b67fcdcef45b0ccd7a" - integrity sha512-eUQmx682AzJRaY4QJSALtIGxXzg2r2Tr8exnDeFMrNe0Ibn4Oqm3h9SkOzYiqqZjKQ13wvHm0RoxT2QKo+908g== +mogh_auth_client@^1.7.0: + version "1.7.0" + resolved "https://registry.yarnpkg.com/mogh_auth_client/-/mogh_auth_client-1.7.0.tgz#a016c8dafd0f73633098d41aa95889c444e481d9" + integrity sha512-v4TJVNIiFYBLd70yGw/eS37E15he4I5w3poj5X7W75D+m6ry9gbhtlePp5elRbqjDZ/WXkOZ/XYCHqGYb/PQ2A== dependencies: jwt-decode "^4.0.0" -mogh_ui@^0.6.1: - version "0.6.1" - resolved "https://registry.yarnpkg.com/mogh_ui/-/mogh_ui-0.6.1.tgz#c03db66bc4f830706bb17c2733eb541566930b29" - integrity sha512-OpEN7HAYOvj/RWLOtkNem4eU82etribTQrfVi0bmNyBh5Zw0GY2FoanAHU0cCB2a5Q2rZD6keWb2axQhWog30A== +mogh_ui@^0.7.6: + version "0.7.6" + resolved "https://registry.yarnpkg.com/mogh_ui/-/mogh_ui-0.7.6.tgz#9c9ab00f6d15a8fc6b31b3176a794eb3ef806bf2" + integrity sha512-THEdqJ/3y6RnHn3eWqxbKj0STx6aUAI4dsWekJFTrGIbE/rEBvWl449eo0v+ZpH2dgo9gIlkwDXBYx8xyBuzLA== -monaco-editor@^0.55.1: - version "0.55.1" - resolved "https://registry.yarnpkg.com/monaco-editor/-/monaco-editor-0.55.1.tgz#e74c6fe5a6bf985b817d2de3eb88d56afc494a1b" - integrity sha512-jz4x+TJNFHwHtwuV9vA9rMujcZRb0CEilTEwG2rRSpe/A7Jdkuj8xPKttCgOh+v/lkHy7HsZ64oj+q3xoAFl9A== +monaco-editor@^0.56.0: + version "0.56.0" + resolved "https://registry.yarnpkg.com/monaco-editor/-/monaco-editor-0.56.0.tgz#e85ac00b7ab7092ca1d077894504919af5073cf0" + integrity sha512-sXboRm3BeBeLm938eaiyLMe0OxzfXIlZvbv4ir/jVgQy1zDhWjgmny0WoN45fuDKhCCQsYMbBJrv/A6jd8aCUg== dependencies: - dompurify "3.2.7" + dompurify "3.4.8" marked "14.0.0" monaco-languageserver-types@^0.4.0: @@ -940,11 +1052,12 @@ monaco-worker-manager@^2.0.0: resolved "https://registry.yarnpkg.com/monaco-worker-manager/-/monaco-worker-manager-2.0.1.tgz#f67c54dfca34ed4b225d5de84e77b24b4e36de8a" integrity sha512-kdPL0yvg5qjhKPNVjJoym331PY/5JC11aPJXtCZNwWRvBr6jhkIamvYAyiY5P1AWFmNOy0aRDRoMdZfa71h8kg== -monaco-yaml@^5.4.1: - version "5.4.1" - resolved "https://registry.yarnpkg.com/monaco-yaml/-/monaco-yaml-5.4.1.tgz#c26106c6e497d514aa6254c78ab184610a9923b4" - integrity sha512-YQ6d/Ei98Uk073SJLFbwuSi95qhnl8F8NNmIUqN2XhDt9psZN2LqQ1T7pPQ866NJb2wFj44IrjnANgpa2jTfag== +monaco-yaml@^5.5.1: + version "5.5.1" + resolved "https://registry.yarnpkg.com/monaco-yaml/-/monaco-yaml-5.5.1.tgz#9771c91d6b54456a1a398b028dd27435f473b58e" + integrity sha512-vLR1EoV7Au0tkuA9T6ZJr2q7CooRP69AvlqzWh9Gui6amuijdTsfCAQHtCFobRMwkhbuly6J1Q16NziTTiWtWQ== dependencies: + "@vscode/l10n" "^0.0.18" jsonc-parser "^3.0.0" monaco-languageserver-types "^0.4.0" monaco-marker-data-provider "^1.0.0" @@ -952,6 +1065,7 @@ monaco-yaml@^5.4.1: monaco-worker-manager "^2.0.0" path-browserify "^1.0.0" prettier "^3.0.0" + proxy-disposable "^1.0.0" vscode-languageserver-textdocument "^1.0.0" vscode-languageserver-types "^3.0.0" vscode-uri "^3.0.0" @@ -962,6 +1076,11 @@ nanoid@^3.3.11: resolved "https://registry.yarnpkg.com/nanoid/-/nanoid-3.3.11.tgz#4f4f112cefbe303202f2199838128936266d185b" integrity sha512-N8SpfPUnUp1bK+PMYW8qSWdl9U+wwNWI4QKxOYDy9JAro3WMX7p2OeVRF9v+347pnakNevPmiHhNmZ2HbFA76w== +nanoid@^3.3.12: + version "3.3.12" + resolved "https://registry.yarnpkg.com/nanoid/-/nanoid-3.3.12.tgz#ab3d912e217a6d0a514f00a72a16543a28982c05" + integrity sha512-ZB9RH/39qpq5Vu6Y+NmUaFhQR6pp+M2Xt76XBnEwDaGcVAqhlvxrl3B2bKS5D3NH3QR76v3aSrKaF/Kiy7lEtQ== + node-addon-api@^7.0.0: version "7.1.1" resolved "https://registry.yarnpkg.com/node-addon-api/-/node-addon-api-7.1.1.tgz#1aba6693b0f255258a049d621329329322aad558" @@ -1051,12 +1170,12 @@ postcss@^8.3.11: picocolors "^1.1.1" source-map-js "^1.2.1" -postcss@^8.5.14: - version "8.5.14" - resolved "https://registry.yarnpkg.com/postcss/-/postcss-8.5.14.tgz#a66c2d7808fadf69ebb5b84a03f8bafd76c4919c" - integrity sha512-SoSL4+OSEtR99LHFZQiJLkT59C5B1amGO1NzTwj7TT1qCUgUO6hxOvzkOYxD+vMrXBM3XJIKzokoERdqQq/Zmg== +postcss@^8.5.16: + version "8.5.16" + resolved "https://registry.yarnpkg.com/postcss/-/postcss-8.5.16.tgz#1230ce0b5df354c24c0ea45f99ce5f6a88279d28" + integrity sha512-vuwillviilfKZsg0VGj5R/YwwcHx4SLsIOI/7K6mQkWx+l5cUHTjj5g0AasTBcyXsbfTgrwsUNmVUb5xVwyPwg== dependencies: - nanoid "^3.3.11" + nanoid "^3.3.12" picocolors "^1.1.1" source-map-js "^1.2.1" @@ -1074,10 +1193,15 @@ prop-types@^15.6.2: object-assign "^4.1.1" react-is "^16.13.1" -react-dom@^19.2.6: - version "19.2.6" - resolved "https://registry.yarnpkg.com/react-dom/-/react-dom-19.2.6.tgz#44a81b0bcca22da814c00847d09d01c8615529b7" - integrity sha512-0prMI+hvBbPjsWnxDLxlCGyM8PN6UuWjEUCYmZhO67xIV9Xasa/r/vDnq+Xyq4Lo27g8QSbO5YzARu0D1Sps3g== +proxy-disposable@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/proxy-disposable/-/proxy-disposable-1.0.0.tgz#c61f2ebcf1215600088de75badf5882873c64382" + integrity sha512-Y673BKpFqk8XkYuoK9DGZ1RIzldz5SkzxTKc/+DkpaMdrDEbEalWlO/uMRMdJsXvdbTybJI26XDwN0KJpZDN4w== + +react-dom@^19.2.7: + version "19.2.7" + resolved "https://registry.yarnpkg.com/react-dom/-/react-dom-19.2.7.tgz#0450dc9ae9ddbff76ef196401cd8b8c7fb466ccc" + integrity sha512-t0BRVXvbiE/o20Hfw669rLbMCDWtYZLvmJigy2f0MxsXF+71pxhR3xOkspmsO8h3ZlNzyibAmtCa3l4lYKk6gQ== dependencies: scheduler "^0.27.0" @@ -1125,17 +1249,17 @@ react-remove-scroll@^2.7.2: use-callback-ref "^1.3.3" use-sidecar "^1.1.3" -react-router-dom@^7.15.0: - version "7.15.0" - resolved "https://registry.yarnpkg.com/react-router-dom/-/react-router-dom-7.15.0.tgz#a4b95c4402d896c2ad437014aff9076b94673063" - integrity sha512-VcrVg64Fo8nwBvDscajG8gRTLIuTC6N50nb22l2HOOV4PTOHgoGp8mUjy9wLiHYoYTSYI36tUnXZgasSRFZorQ== +react-router-dom@^7.18.1: + version "7.18.1" + resolved "https://registry.yarnpkg.com/react-router-dom/-/react-router-dom-7.18.1.tgz#0d1b138e291393059ad481c3e10e366385a978a4" + integrity sha512-KaZh+X/6UtEp28x51AUYZDMg9NGoz2ja3dNHa+ta/tk40vCzKhQ/RypCWBMLbmDr6//E24Vv5uPsrqXFozdkAg== dependencies: - react-router "7.15.0" + react-router "7.18.1" -react-router@7.15.0: - version "7.15.0" - resolved "https://registry.yarnpkg.com/react-router/-/react-router-7.15.0.tgz#cb438ff254ab5a1e356ef5a23d7821d8f6fbe652" - integrity sha512-HW9vYwuM8f4yx66Izy8xfrzCM+SBJluoZcCbww9A1TySax11S5Vgw6fi3ZjMONw9J4gQwngL7PzkyIpJJpJ7RQ== +react-router@7.18.1: + version "7.18.1" + resolved "https://registry.yarnpkg.com/react-router/-/react-router-7.18.1.tgz#61259d1594b95c1ace299ee4c57453570f0c22f1" + integrity sha512-GDLgg3i3uM0aeJO3Fm+TCS+sDQ7gu12T6x0qdTEzcwqEfleci7JwugVNIF3U//0FWKnJT7ptG+20B2jfDqnZAg== dependencies: cookie "^1.0.1" set-cookie-parser "^2.6.0" @@ -1158,34 +1282,34 @@ react-transition-group@4.4.5: loose-envify "^1.4.0" prop-types "^15.6.2" -react-xtermjs@^1.0.11: - version "1.0.11" - resolved "https://registry.yarnpkg.com/react-xtermjs/-/react-xtermjs-1.0.11.tgz#8ea013735a698a3c8173fc2d3d745f395ca72693" - integrity sha512-GyC4krigc+LREQKM25aFRCZf5kmUmx+xgEB2X0XEwZXYh/2QHeegHfdBP+oFztPgKsSROa1OIW4wqQzSCKSP+Q== +react-xtermjs@^1.0.12: + version "1.0.12" + resolved "https://registry.yarnpkg.com/react-xtermjs/-/react-xtermjs-1.0.12.tgz#d678fee0f04315d87613ecbb10b0820c54c4ee38" + integrity sha512-8uXBzVF4vP6r2NABxt/9heH1Y5Vx2v2hFrAk1kre+HyY3CjM6Prbc8ViqBRia6EBzI57IFBWOicYV1/k05JBtw== -react@^19.2.6: - version "19.2.6" - resolved "https://registry.yarnpkg.com/react/-/react-19.2.6.tgz#3dadb8e12b2a7934c1d5317973e5dce1301f9a4d" - integrity sha512-sfWGGfavi0xr8Pg0sVsyHMAOziVYKgPLNrS7ig+ivMNb3wbCBw3KxtflsGBAwD3gYQlE/AEZsTLgToRrSCjb0Q== +react@^19.2.7: + version "19.2.7" + resolved "https://registry.yarnpkg.com/react/-/react-19.2.7.tgz#1f47a1bfc06f8ec885752c6f4af14369a9f8260b" + integrity sha512-HNe9WslTbXmFK8o8cmwgAeJFSBvt1bPdHCVKtaaV+WlAN36mpT4hcRpwbf3fY56ar2oIXzsBpOAiIRHAdY0OlQ== -readdirp@^4.0.1: - version "4.1.2" - resolved "https://registry.yarnpkg.com/readdirp/-/readdirp-4.1.2.tgz#eb85801435fbf2a7ee58f19e0921b068fc69948d" - integrity sha512-GDhwkLfywWL2s6vEjyhri+eXmfH6j1L7JE27WhqLeYzoh/A3DBaYGEj2H/HFZCn/kMfim73FXxEJTw06WtxQwg== +readdirp@^5.0.0: + version "5.0.0" + resolved "https://registry.yarnpkg.com/readdirp/-/readdirp-5.0.0.tgz#fbf1f71a727891d685bb1786f9ba74084f6e2f91" + integrity sha512-9u/XQ1pvrQtYyMpZe7DXKv2p5CNvyVwzUB6uhLAnQwHMSgKMBR62lc7AHljaeteeHXn11XTAaLLUVZYVZyuRBQ== -recharts@^3.8.1: - version "3.8.1" - resolved "https://registry.yarnpkg.com/recharts/-/recharts-3.8.1.tgz#1784b14784dab9a27eb426c475e6a9187f14cf01" - integrity sha512-mwzmO1s9sFL0TduUpwndxCUNoXsBw3u3E/0+A+cLcrSfQitSG62L32N69GhqUrrT5qKcAE3pCGVINC6pqkBBQg== +recharts@^3.9.1: + version "3.9.1" + resolved "https://registry.yarnpkg.com/recharts/-/recharts-3.9.1.tgz#6fe1b4617147e2016fa6f4cd681c8c818255955e" + integrity sha512-WMcwlXcB7l+BbxiEdyClkG+1sxrMHNZpzT577LEvU4+rXPd8oTAy1wXk72hnk2KOOmxuLvw3z5DtXT7HEAydtg== dependencies: "@reduxjs/toolkit" "^1.9.0 || 2.x.x" clsx "^2.1.1" decimal.js-light "^2.5.1" es-toolkit "^1.39.3" eventemitter3 "^5.0.1" - immer "^10.1.1" + immer "^11.1.8" react-redux "8.x.x || 9.x.x" - reselect "5.1.1" + reselect "5.2.0" tiny-invariant "^1.3.3" use-sync-external-store "^1.2.2" victory-vendor "^37.0.2" @@ -1200,34 +1324,39 @@ redux@^5.0.1: resolved "https://registry.yarnpkg.com/redux/-/redux-5.0.1.tgz#97fa26881ce5746500125585d5642c77b6e9447b" integrity sha512-M9/ELqF6fy8FwmkpnF0S3YKOqMyoWJ4+CS5Efg2ct3oY9daQvd/Pc71FpGZsVsbl3Cpb+IIcjBDUnnyBdQbq4w== -reselect@5.1.1, reselect@^5.1.0: +reselect@5.2.0: + version "5.2.0" + resolved "https://registry.yarnpkg.com/reselect/-/reselect-5.2.0.tgz#f380ef7664332d26ea06c1cba04bdbbdcaa955f1" + integrity sha512-AgZ3UOZm3YndfrJ4OYjgrT7bmCm/1iqkjvEfH/oYjzh6PD2qw4QuT3jjnXIrpdt4MTpMXclMT3lXbmRY+XRakw== + +reselect@^5.1.0: version "5.1.1" resolved "https://registry.yarnpkg.com/reselect/-/reselect-5.1.1.tgz#c766b1eb5d558291e5e550298adb0becc24bb72e" integrity sha512-K/BG6eIky/SBpzfHZv/dd+9JBFiS4SWV7FIujVyJRux6e45+73RaUHXLmIR1f7WOMaQ0U1km6qwklRQxpJJY0w== -rolldown@1.0.0-rc.18: - version "1.0.0-rc.18" - resolved "https://registry.yarnpkg.com/rolldown/-/rolldown-1.0.0-rc.18.tgz#c597f89a4ce12e6fc918fa91e4f892b340aa92f0" - integrity sha512-phmyKBpuBdRYDf4hgyynGAYn/rDDe+iZXKVJ7WX5b1zQzpLkP5oJRPGsfJuHdzPMlyyEO/4sPW6yfSx2gf7lVg== +rolldown@~1.1.3: + version "1.1.3" + resolved "https://registry.yarnpkg.com/rolldown/-/rolldown-1.1.3.tgz#87072bfd0d1bdd02a66076a261a62e8e49b3f0e2" + integrity sha512-1F1eEtUBtFvcGm1HQ9TiUIUHPQG7mSAODrhIzjxoUEFuo8OcbrGLiVLkevNgj84TE4lnHvnumwFjhJO5Eu135g== dependencies: - "@oxc-project/types" "=0.128.0" - "@rolldown/pluginutils" "1.0.0-rc.18" + "@oxc-project/types" "=0.137.0" + "@rolldown/pluginutils" "^1.0.0" optionalDependencies: - "@rolldown/binding-android-arm64" "1.0.0-rc.18" - "@rolldown/binding-darwin-arm64" "1.0.0-rc.18" - "@rolldown/binding-darwin-x64" "1.0.0-rc.18" - "@rolldown/binding-freebsd-x64" "1.0.0-rc.18" - "@rolldown/binding-linux-arm-gnueabihf" "1.0.0-rc.18" - "@rolldown/binding-linux-arm64-gnu" "1.0.0-rc.18" - "@rolldown/binding-linux-arm64-musl" "1.0.0-rc.18" - "@rolldown/binding-linux-ppc64-gnu" "1.0.0-rc.18" - "@rolldown/binding-linux-s390x-gnu" "1.0.0-rc.18" - "@rolldown/binding-linux-x64-gnu" "1.0.0-rc.18" - "@rolldown/binding-linux-x64-musl" "1.0.0-rc.18" - "@rolldown/binding-openharmony-arm64" "1.0.0-rc.18" - "@rolldown/binding-wasm32-wasi" "1.0.0-rc.18" - "@rolldown/binding-win32-arm64-msvc" "1.0.0-rc.18" - "@rolldown/binding-win32-x64-msvc" "1.0.0-rc.18" + "@rolldown/binding-android-arm64" "1.1.3" + "@rolldown/binding-darwin-arm64" "1.1.3" + "@rolldown/binding-darwin-x64" "1.1.3" + "@rolldown/binding-freebsd-x64" "1.1.3" + "@rolldown/binding-linux-arm-gnueabihf" "1.1.3" + "@rolldown/binding-linux-arm64-gnu" "1.1.3" + "@rolldown/binding-linux-arm64-musl" "1.1.3" + "@rolldown/binding-linux-ppc64-gnu" "1.1.3" + "@rolldown/binding-linux-s390x-gnu" "1.1.3" + "@rolldown/binding-linux-x64-gnu" "1.1.3" + "@rolldown/binding-linux-x64-musl" "1.1.3" + "@rolldown/binding-openharmony-arm64" "1.1.3" + "@rolldown/binding-wasm32-wasi" "1.1.3" + "@rolldown/binding-win32-arm64-msvc" "1.1.3" + "@rolldown/binding-win32-x64-msvc" "1.1.3" rxjs@^7.4.0: version "7.8.2" @@ -1236,116 +1365,117 @@ rxjs@^7.4.0: dependencies: tslib "^2.1.0" -sanitize-html@^2.17.3: - version "2.17.3" - resolved "https://registry.yarnpkg.com/sanitize-html/-/sanitize-html-2.17.3.tgz#b2f79f75afeda3a90e87d480052dfc1601b7aeee" - integrity sha512-Kn4srCAo2+wZyvCNKCSyB2g8RQ8IkX/gQs2uqoSRNu5t9I2qvUyAVvRDiFUVAiX3N3PNuwStY0eNr+ooBHVWEg== +sanitize-html@^2.17.5: + version "2.17.5" + resolved "https://registry.yarnpkg.com/sanitize-html/-/sanitize-html-2.17.5.tgz#bef1d103773760e1c52a01b1f58178bee9c10556" + integrity sha512-ZmU1joGRrvoyctKIiuwUxqR6moLoU2Wk+2bMccN6f7UwhAmwYDvWziqPxRDDN2Qip62NqnIrVrT9akbL6Wretg== dependencies: deepmerge "^4.2.2" escape-string-regexp "^4.0.0" htmlparser2 "^10.1.0" is-plain-object "^5.0.0" + launder "^1.7.1" parse-srcset "^1.0.2" postcss "^8.3.11" -sass-embedded-all-unknown@1.99.0: - version "1.99.0" - resolved "https://registry.yarnpkg.com/sass-embedded-all-unknown/-/sass-embedded-all-unknown-1.99.0.tgz#a17211a0e6862bc36eea063fbd0c55dc2a41c3a1" - integrity sha512-qPIRG8Uhjo6/OKyAKixTnwMliTz+t9K6Duk0mx5z+K7n0Ts38NSJz2sjDnc7cA/8V9Lb3q09H38dZ1CLwD+ssw== - dependencies: - sass "1.99.0" - -sass-embedded-android-arm64@1.99.0: - version "1.99.0" - resolved "https://registry.yarnpkg.com/sass-embedded-android-arm64/-/sass-embedded-android-arm64-1.99.0.tgz#670907b907eef644d45c609b06d57c3659cf77a7" - integrity sha512-fNHhdnP23yqqieCbAdym4N47AleSwjbNt6OYIYx4DdACGdtERjQB4iOX/TaKsW034MupfF7SjnAAK8w7Ptldtg== - -sass-embedded-android-arm@1.99.0: - version "1.99.0" - resolved "https://registry.yarnpkg.com/sass-embedded-android-arm/-/sass-embedded-android-arm-1.99.0.tgz#2c0d5719d2022578e97d0296bfc7771c4588a2e9" - integrity sha512-EHvJ0C7/VuP78Qr6f8gIUVUmCqIorEQpw2yp3cs3SMg02ZuumlhjXvkTcFBxHmFdFR23vTNk1WnhY6QSeV1nFQ== - -sass-embedded-android-riscv64@1.99.0: - version "1.99.0" - resolved "https://registry.yarnpkg.com/sass-embedded-android-riscv64/-/sass-embedded-android-riscv64-1.99.0.tgz#7b9006c5921205c252d9928dabc41d93713b0ae6" - integrity sha512-4zqDFRvgGDTL5vTHuIhRxUpXFoh0Cy7Gm5Ywk19ASd8Settmd14YdPRZPmMxfgS1GH292PofV1fq1ifiSEJWBw== - -sass-embedded-android-x64@1.99.0: - version "1.99.0" - resolved "https://registry.yarnpkg.com/sass-embedded-android-x64/-/sass-embedded-android-x64-1.99.0.tgz#cb0c4e0ec1991faab41a6d60c894ce12fcaaf339" - integrity sha512-Uk53k/dGYt04RjOL4gFjZ0Z9DH9DKh8IA8WsXUkNqsxerAygoy3zqRBS2zngfE9K2jiOM87q+1R1p87ory9oQQ== - -sass-embedded-darwin-arm64@1.99.0: - version "1.99.0" - resolved "https://registry.yarnpkg.com/sass-embedded-darwin-arm64/-/sass-embedded-darwin-arm64-1.99.0.tgz#aa836ba0b2342f8d0fa03394781a9385fb8f222d" - integrity sha512-u61/7U3IGLqoO6gL+AHeiAtlTPFwJK1+964U8gp45ZN0hzh1yrARf5O1mivXv8NnNgJvbG2wWJbiNZP0lG/lTg== - -sass-embedded-darwin-x64@1.99.0: - version "1.99.0" - resolved "https://registry.yarnpkg.com/sass-embedded-darwin-x64/-/sass-embedded-darwin-x64-1.99.0.tgz#47a8a95579f6b8b66f07d233d59d43a99f6000ac" - integrity sha512-j/kkk/NcXdIameLezSfXjgCiBkVcA+G60AXrX768/3g0miK1g7M9dj7xOhCb1i7/wQeiEI3rw2LLuO63xRIn4A== - -sass-embedded-linux-arm64@1.99.0: - version "1.99.0" - resolved "https://registry.yarnpkg.com/sass-embedded-linux-arm64/-/sass-embedded-linux-arm64-1.99.0.tgz#201232294df6d119e89f12c1e35e0e9f316263bd" - integrity sha512-btNcFpItcB56L40n8hDeL7sRSMLDXQ56nB5h2deddJx1n60rpKSElJmkaDGHtpkrY+CTtDRV0FZDjHeTJddYew== - -sass-embedded-linux-arm@1.99.0: - version "1.99.0" - resolved "https://registry.yarnpkg.com/sass-embedded-linux-arm/-/sass-embedded-linux-arm-1.99.0.tgz#013ac6f04dedbcbdd3f2d460d078809c5244d1eb" - integrity sha512-d4IjJZrX2+AwB2YCy1JySwdptJECNP/WfAQLUl8txI3ka8/d3TUI155GtelnoZUkio211PwIeFvvAeZ9RXPQnw== - -sass-embedded-linux-musl-arm64@1.99.0: - version "1.99.0" - resolved "https://registry.yarnpkg.com/sass-embedded-linux-musl-arm64/-/sass-embedded-linux-musl-arm64-1.99.0.tgz#cc3cc0f4dcaf5072bc777719419a8fdf997910df" - integrity sha512-Hi2bt/IrM5P4FBKz6EcHAlniwfpoz9mnTdvSd58y+avA3SANM76upIkAdSayA8ZGwyL3gZokru1AKDPF9lJDNw== - -sass-embedded-linux-musl-arm@1.99.0: - version "1.99.0" - resolved "https://registry.yarnpkg.com/sass-embedded-linux-musl-arm/-/sass-embedded-linux-musl-arm-1.99.0.tgz#4af04ac13b242253a276ccecc0905a4119501d16" - integrity sha512-2gvHOupgIw3ytatXT4nFUow71LFbuOZPEwG+HUzcNQDH8ue4Ez8cr03vsv5MDv3lIjOKcXwDvWD980t18MwkoQ== - -sass-embedded-linux-musl-riscv64@1.99.0: - version "1.99.0" - resolved "https://registry.yarnpkg.com/sass-embedded-linux-musl-riscv64/-/sass-embedded-linux-musl-riscv64-1.99.0.tgz#5e6fb8d41a6280d9dfda2b636a3b1c3a7c3c2e53" - integrity sha512-mKqGvVaJ9rHMqyZsF0kikQe4NO0f4osb67+X6nLhBiVDKvyazQHJ3zJQreNefIE36yL2sjHIclSB//MprzaQDg== - -sass-embedded-linux-musl-x64@1.99.0: - version "1.99.0" - resolved "https://registry.yarnpkg.com/sass-embedded-linux-musl-x64/-/sass-embedded-linux-musl-x64-1.99.0.tgz#e26a0d65cf5a488f24951ddce1842d6482c78a7d" - integrity sha512-huhgOMmOc30r7CH7qbRbT9LerSEGSnWuS4CYNOskr9BvNeQp4dIneFufNRGZ7hkOAxUM8DglxIZJN/cyAT95Ew== - -sass-embedded-linux-riscv64@1.99.0: - version "1.99.0" - resolved "https://registry.yarnpkg.com/sass-embedded-linux-riscv64/-/sass-embedded-linux-riscv64-1.99.0.tgz#3a114700b58e4d0d8fb2191f29f873068bb42afc" - integrity sha512-mevFPIFAVhrH90THifxLfOntFmHtcEKOcdWnep2gJ0X4DVva4AiVIRlQe/7w9JFx5+gnDRE1oaJJkzuFUuYZsA== - -sass-embedded-linux-x64@1.99.0: - version "1.99.0" - resolved "https://registry.yarnpkg.com/sass-embedded-linux-x64/-/sass-embedded-linux-x64-1.99.0.tgz#b23e34de696eeef621ada3671014208ae56b747b" - integrity sha512-9k7IkULqIZdCIVt4Mboryt6vN8Mjmm3EhI1P3mClU5y5i3wLK5ExC3cbVWk047KsID/fvB1RLslqghXJx5BoxA== - -sass-embedded-unknown-all@1.99.0: - version "1.99.0" - resolved "https://registry.yarnpkg.com/sass-embedded-unknown-all/-/sass-embedded-unknown-all-1.99.0.tgz#202d955a376c413706224dd6f83ce8df2a5f71fe" - integrity sha512-P7MxiUtL/XzGo3PX0CaB8lNNEFLQWKikPA8pbKytx9ZCLZSDkt2NJcdAbblB/sqMs4AV3EK2NadV8rI/diq3xg== - dependencies: - sass "1.99.0" - -sass-embedded-win32-arm64@1.99.0: - version "1.99.0" - resolved "https://registry.yarnpkg.com/sass-embedded-win32-arm64/-/sass-embedded-win32-arm64-1.99.0.tgz#1461a6a14cd3596ee122fc1b7ead25b039a066fc" - integrity sha512-8whpsW7S+uO8QApKfQuc36m3P9EISzbVZOgC79goob4qGy09u8Gz/rYvw8h1prJDSjltpHGhOzBE6LDz7WvzVw== - -sass-embedded-win32-x64@1.99.0: - version "1.99.0" - resolved "https://registry.yarnpkg.com/sass-embedded-win32-x64/-/sass-embedded-win32-x64-1.99.0.tgz#3d08d273b268918647093fd7d0bf0fb84ac2c321" - integrity sha512-ipuOv1R2K4MHeuCEAZGpuUbAgma4gb0sdacyrTjJtMOy/OY9UvWfVlwErdB09KIkp4fPDpQJDJfvYN6bC8jeNg== - -sass-embedded@^1.99.0: - version "1.99.0" - resolved "https://registry.yarnpkg.com/sass-embedded/-/sass-embedded-1.99.0.tgz#348fdb98b02694d8b193e7a0e1bfc47408f89d4f" - integrity sha512-gF/juR1aX02lZHkvwxdF80SapkQeg2fetoDF6gIQkNbSw5YEUFspMkyGTjPjgZSgIHuZpy+Wz4PlebKnLXMjdg== +sass-embedded-all-unknown@1.100.0: + version "1.100.0" + resolved "https://registry.yarnpkg.com/sass-embedded-all-unknown/-/sass-embedded-all-unknown-1.100.0.tgz#efee7111ec5e2d6c34a2f57d090e6416c01890ff" + integrity sha512-auFtXY/kwYILmSVjtBDwyj0axcLbYYiffOKWoaXHnI5bsYwiRbBh3EneR1rpbX2ZIZCrwX93i5pxKLTZF/662Q== + dependencies: + sass "1.100.0" + +sass-embedded-android-arm64@1.100.0: + version "1.100.0" + resolved "https://registry.yarnpkg.com/sass-embedded-android-arm64/-/sass-embedded-android-arm64-1.100.0.tgz#0a4456a751f76a378ac9f3516a034f56eca9b171" + integrity sha512-W+Ru9JwTnfU0UX3jSZcbqFdtKFMcYdfFwytc57h2DgnqCOIiAqI2E06mABZBZC+r3LwXCBuS5GbXAGeVgvVDkA== + +sass-embedded-android-arm@1.100.0: + version "1.100.0" + resolved "https://registry.yarnpkg.com/sass-embedded-android-arm/-/sass-embedded-android-arm-1.100.0.tgz#7de61d65cfa997c58df426a8b20a096a60497313" + integrity sha512-70f3HgX2pFNmzpGQ86n5e6QfWn2fP4QUQGfFQK0P1XH73ZLIzLo2YqygrGKGKeeqtc5eU2Wl1/xQzhzuKnO4kw== + +sass-embedded-android-riscv64@1.100.0: + version "1.100.0" + resolved "https://registry.yarnpkg.com/sass-embedded-android-riscv64/-/sass-embedded-android-riscv64-1.100.0.tgz#cc6721c1526ef5b10d9da02f1e44e7dbaffc11e0" + integrity sha512-icU3o0V/uCSytSpf+tX5Lf51BvyQEbLzDUJfUi9etSauYBGHpPKkdtdZH0si4v98phq11Kl8rSV1SggksxF1Hg== + +sass-embedded-android-x64@1.100.0: + version "1.100.0" + resolved "https://registry.yarnpkg.com/sass-embedded-android-x64/-/sass-embedded-android-x64-1.100.0.tgz#dc38f6c633f8bf61b470ac00452ebb50e36f421f" + integrity sha512-mevF9VQk6gEYByy8+jusaHGmd7Usb2ytX/DsEOd0JtOGCtcf1kh575xJ6OUBDIcJ15uLnbau/0iy1eP6WVBvWA== + +sass-embedded-darwin-arm64@1.100.0: + version "1.100.0" + resolved "https://registry.yarnpkg.com/sass-embedded-darwin-arm64/-/sass-embedded-darwin-arm64-1.100.0.tgz#7773c0586e78caab599d53370626c28ca08110eb" + integrity sha512-1PVlYi61POo93IT/FfrG1mc1tAHxeSTyUALF2aOFmXGWjVXr3bQzEQiBGCOvQbj/ix+5hNyXFXcEMEyKvtUJJA== + +sass-embedded-darwin-x64@1.100.0: + version "1.100.0" + resolved "https://registry.yarnpkg.com/sass-embedded-darwin-x64/-/sass-embedded-darwin-x64-1.100.0.tgz#91066f64875b87eba0cb699ba8254b4863d33901" + integrity sha512-x97o3JnGyImZNCIVs9wQHJUE5QCvmVIKaH1cwrz/5dK7OT1FpeNiW+u9TUomP9hG6Ekjd8EL8NBHpxTfIhdjmg== + +sass-embedded-linux-arm64@1.100.0: + version "1.100.0" + resolved "https://registry.yarnpkg.com/sass-embedded-linux-arm64/-/sass-embedded-linux-arm64-1.100.0.tgz#88d2f51a0882f399c327a1d6cd06757d13de4f82" + integrity sha512-Dwjmj8Z6VRy7rAi53JAdEwIyUjpfl7PhpSc2/LpQPQx+aO5Dp7Spaipkax0ufJl1SoDUdchCsM4y/88YaluorQ== + +sass-embedded-linux-arm@1.100.0: + version "1.100.0" + resolved "https://registry.yarnpkg.com/sass-embedded-linux-arm/-/sass-embedded-linux-arm-1.100.0.tgz#9721dca177348c1448847ceee1d031a692df0771" + integrity sha512-9Ul7O1eKrc5YlhwWjkp8tZPSe3UEwSZ1uwUZOQom1HL0pRlBA6F/IlGZYFTLwnHMIP1fc77MMNaBRfc05mKMpw== + +sass-embedded-linux-musl-arm64@1.100.0: + version "1.100.0" + resolved "https://registry.yarnpkg.com/sass-embedded-linux-musl-arm64/-/sass-embedded-linux-musl-arm64-1.100.0.tgz#63a1e0618a2a3acf10bab915585c4dc495327581" + integrity sha512-XpACJB2KjSLjf2e9uuvGVdOURsoNrFqgRiihhXyUHK9W0t3LIHb7z5MA/7XGPIT9bWSOO2zyw+rH/FHtDV/Yrg== + +sass-embedded-linux-musl-arm@1.100.0: + version "1.100.0" + resolved "https://registry.yarnpkg.com/sass-embedded-linux-musl-arm/-/sass-embedded-linux-musl-arm-1.100.0.tgz#44f9472ae663411de82b7792a918e162c7bcbc02" + integrity sha512-sl0JgbGloPyJg66XXx5UDSDScZ0oU85DpMQU4JU/sCUCFj1Z8zZ69SJWKTCNE4/jwnce7WI2zPCV5AG+RHOZJw== + +sass-embedded-linux-musl-riscv64@1.100.0: + version "1.100.0" + resolved "https://registry.yarnpkg.com/sass-embedded-linux-musl-riscv64/-/sass-embedded-linux-musl-riscv64-1.100.0.tgz#21954e480cfb55d4ba8167626cde1cc8560d7eee" + integrity sha512-ShvI0Kx04mwoCARwZ0UjiT97isQvzO80tAt91zmFyHLN9kelc/IrQi940farSm2xQVPCKdeVyeG0ekBsokSpYQ== + +sass-embedded-linux-musl-x64@1.100.0: + version "1.100.0" + resolved "https://registry.yarnpkg.com/sass-embedded-linux-musl-x64/-/sass-embedded-linux-musl-x64-1.100.0.tgz#a6c93d51999a8b22795d383b67e61962d32c96cd" + integrity sha512-TDBCRWNuS4RDLQXvRc1gjZlWiWTWaWGp0Bwu/IKwJxov81lsvrCs3TihTyNXtW7V5aoN4Ky3r0QOkNb3mwmBnA== + +sass-embedded-linux-riscv64@1.100.0: + version "1.100.0" + resolved "https://registry.yarnpkg.com/sass-embedded-linux-riscv64/-/sass-embedded-linux-riscv64-1.100.0.tgz#01f20471a1be16d18e150993e5ac771d618abd34" + integrity sha512-j4ENJGOheO+fm3j/yorLxCjBP6/XskrZx7dTLlT+lXYwN/qqCqoA/gsNLI0McS3DFM6GBwPiffzWsdWS8t6sEQ== + +sass-embedded-linux-x64@1.100.0: + version "1.100.0" + resolved "https://registry.yarnpkg.com/sass-embedded-linux-x64/-/sass-embedded-linux-x64-1.100.0.tgz#809649a0df17f7afb1dfd02e76dbf0676dd8471e" + integrity sha512-0vUSN8j0WGtCJIOPh//EmUvYGHW0QOe5iul8qyhPk50MAcw49MA0r34AhftjDdx94ILPF6vApFs0gwHPQRlpVA== + +sass-embedded-unknown-all@1.100.0: + version "1.100.0" + resolved "https://registry.yarnpkg.com/sass-embedded-unknown-all/-/sass-embedded-unknown-all-1.100.0.tgz#fdc210a25a06e9d79655a02c1c77d656fa6f2ca0" + integrity sha512-c+naBgWId4MIpToXcI0DgqetjdAkwTTAxFAuOaBz7HUXLdyG1oZRrEvSsbe41nEdQOKH0vgofVFCeSQgoXOG9A== + dependencies: + sass "1.100.0" + +sass-embedded-win32-arm64@1.100.0: + version "1.100.0" + resolved "https://registry.yarnpkg.com/sass-embedded-win32-arm64/-/sass-embedded-win32-arm64-1.100.0.tgz#fd8cec45b41390c3db40d13c4d291be7f35b6b0b" + integrity sha512-iE+yxj+hUXwwbqpHkXxgAWTzeRfcWxJ7SSTQEPMk48lwq3oCrWLlz5sQuWHbuTK/i0GKQfROdP+hOmPi89yjUg== + +sass-embedded-win32-x64@1.100.0: + version "1.100.0" + resolved "https://registry.yarnpkg.com/sass-embedded-win32-x64/-/sass-embedded-win32-x64-1.100.0.tgz#a758862b1455d6616be301b363e7e1c93e6361b7" + integrity sha512-qI4F8MI7/KYoy9NdjJfhSspG42WPkADSNDvwEV7qWvCSFC83koJssRsKO2/PfY+niZz6BG65Ic/D+A11h959hw== + +sass-embedded@^1.100.0: + version "1.100.0" + resolved "https://registry.yarnpkg.com/sass-embedded/-/sass-embedded-1.100.0.tgz#fe4742f2f80c21b287e3b90399b9a3f7ca1dcf6f" + integrity sha512-Ut8wlQSk19tm7jMK6mz6cF1+e+E7tUnW2tM02zQDPnOTcVbV8qCQG8UWxZkkNlY50+hV3hqP24OOkUlMz8xBpw== dependencies: "@bufbuild/protobuf" "^2.5.0" colorjs.io "^0.5.0" @@ -1355,31 +1485,31 @@ sass-embedded@^1.99.0: sync-child-process "^1.0.2" varint "^6.0.0" optionalDependencies: - sass-embedded-all-unknown "1.99.0" - sass-embedded-android-arm "1.99.0" - sass-embedded-android-arm64 "1.99.0" - sass-embedded-android-riscv64 "1.99.0" - sass-embedded-android-x64 "1.99.0" - sass-embedded-darwin-arm64 "1.99.0" - sass-embedded-darwin-x64 "1.99.0" - sass-embedded-linux-arm "1.99.0" - sass-embedded-linux-arm64 "1.99.0" - sass-embedded-linux-musl-arm "1.99.0" - sass-embedded-linux-musl-arm64 "1.99.0" - sass-embedded-linux-musl-riscv64 "1.99.0" - sass-embedded-linux-musl-x64 "1.99.0" - sass-embedded-linux-riscv64 "1.99.0" - sass-embedded-linux-x64 "1.99.0" - sass-embedded-unknown-all "1.99.0" - sass-embedded-win32-arm64 "1.99.0" - sass-embedded-win32-x64 "1.99.0" - -sass@1.99.0: - version "1.99.0" - resolved "https://registry.yarnpkg.com/sass/-/sass-1.99.0.tgz#ff9d1594da4886249dfaafabbeea2dea2dc74b26" - integrity sha512-kgW13M54DUB7IsIRM5LvJkNlpH+WhMpooUcaWGFARkF1Tc82v9mIWkCbCYf+MBvpIUBSeSOTilpZjEPr2VYE6Q== - dependencies: - chokidar "^4.0.0" + sass-embedded-all-unknown "1.100.0" + sass-embedded-android-arm "1.100.0" + sass-embedded-android-arm64 "1.100.0" + sass-embedded-android-riscv64 "1.100.0" + sass-embedded-android-x64 "1.100.0" + sass-embedded-darwin-arm64 "1.100.0" + sass-embedded-darwin-x64 "1.100.0" + sass-embedded-linux-arm "1.100.0" + sass-embedded-linux-arm64 "1.100.0" + sass-embedded-linux-musl-arm "1.100.0" + sass-embedded-linux-musl-arm64 "1.100.0" + sass-embedded-linux-musl-riscv64 "1.100.0" + sass-embedded-linux-musl-x64 "1.100.0" + sass-embedded-linux-riscv64 "1.100.0" + sass-embedded-linux-x64 "1.100.0" + sass-embedded-unknown-all "1.100.0" + sass-embedded-win32-arm64 "1.100.0" + sass-embedded-win32-x64 "1.100.0" + +sass@1.100.0: + version "1.100.0" + resolved "https://registry.yarnpkg.com/sass/-/sass-1.100.0.tgz#b4cab1bed286fe22ac6c879c514f71cd36aa06c8" + integrity sha512-B5j0rYMlinhhOo9tjQebMVVn0TfyXAF+wB3b2ggZUuJ/is/Y+7+JGjirAMxHZ9Z3hIP98NPfamlAkBHa1lAaXQ== + dependencies: + chokidar "^5.0.0" immutable "^5.1.5" source-map-js ">=0.6.2 <2.0.0" optionalDependencies: @@ -1395,10 +1525,10 @@ set-cookie-parser@^2.6.0: resolved "https://registry.yarnpkg.com/set-cookie-parser/-/set-cookie-parser-2.7.2.tgz#ccd08673a9ae5d2e44ea2a2de25089e67c7edf68" integrity sha512-oeM1lpU/UvhTxw+g3cIfxXHyJRc/uidd3yK1P242gzHds0udQBYzs3y8j4gCCW+ZJ7ad0yctld8RYO+bdurlvw== -shell-quote@^1.8.3: - version "1.8.3" - resolved "https://registry.yarnpkg.com/shell-quote/-/shell-quote-1.8.3.tgz#55e40ef33cf5c689902353a3d8cd1a6725f08b4b" - integrity sha512-ObmnIF4hXNg1BqhnHmgbDETF8dLPCggZWBjkQfhZpbszZnYur5DUljTcCHii5LC3J5E0yeO/1LIMyH+UvHQgyw== +shell-quote@^1.9.0: + version "1.9.0" + resolved "https://registry.yarnpkg.com/shell-quote/-/shell-quote-1.9.0.tgz#e108b1a136586d5964edb3300016d4bedba0fe57" + integrity sha512-Iov+JwFv/2HcTpcwNMKd8+IWNb8tboQJNQTkAY/LLVK7gGH9jy+LGkVqPxfekHl+yMmiqXszdGWXgkfml7hjqA== "source-map-js@>=0.6.2 <2.0.0", source-map-js@^1.2.1: version "1.2.1" @@ -1462,10 +1592,10 @@ tinyglobby@^0.2.14: fdir "^6.5.0" picomatch "^4.0.3" -tinyglobby@^0.2.16: - version "0.2.16" - resolved "https://registry.yarnpkg.com/tinyglobby/-/tinyglobby-0.2.16.tgz#1c3b7eb953fce42b226bc5a1ee06428281aff3d6" - integrity sha512-pn99VhoACYR8nFHhxqix+uvsbXineAasWm5ojXoN8xEwK5Kd3/TrhNn1wByuD52UxWRLy8pu+kRMniEi6Eq9Zg== +tinyglobby@^0.2.17: + version "0.2.17" + resolved "https://registry.yarnpkg.com/tinyglobby/-/tinyglobby-0.2.17.tgz#562a9a6c9eb2b3b123d39719f9af5bb44fcd7631" + integrity sha512-wXR/dYpcqKmfWpEdZjiKJOwCNFndD0DMnrW/cYjVGttEkBfVgcLFHoNrlj47mjOVic9yyNu65alsgF4NQyTa2g== dependencies: fdir "^6.5.0" picomatch "^4.0.4" @@ -1475,22 +1605,43 @@ tslib@^2.0.0, tslib@^2.1.0, tslib@^2.4.0: resolved "https://registry.yarnpkg.com/tslib/-/tslib-2.8.1.tgz#612efe4ed235d567e8aba5f2a5fab70280ade83f" integrity sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w== -type-fest@^5.6.0: - version "5.6.0" - resolved "https://registry.yarnpkg.com/type-fest/-/type-fest-5.6.0.tgz#502f7a003b7309e96a7e17052cc2ab2c7e5c7a31" - integrity sha512-8ZiHFm91orbSAe2PSAiSVBVko18pbhbiB3U9GglSzF/zCGkR+rxpHx6sEMCUm4kxY4LjDIUGgCfUMtwfZfjfUA== +type-fest@^5.7.0: + version "5.7.0" + resolved "https://registry.yarnpkg.com/type-fest/-/type-fest-5.7.0.tgz#bae586d3b7c2596bd9c7e62195f33c7fcada1c91" + integrity sha512-1URUxUqfHFM1c+zfSPsa3gnkO7Aq21qyH75SIduNYz4SzY964rn1X2vCMQaHSHhktiw+0kPa2iyb6PUpXqB6Vg== dependencies: tagged-tag "^1.0.0" -typescript@^6.0.3: - version "6.0.3" - resolved "https://registry.yarnpkg.com/typescript/-/typescript-6.0.3.tgz#90251dc007916e972786cb94d74d15b185577d21" - integrity sha512-y2TvuxSZPDyQakkFRPZHKFm+KKVqIisdg9/CZwm9ftvKXLP8NRWj38/ODjNbr43SsoXqNuAisEf1GdCxqWcdBw== - -undici-types@~7.19.0: - version "7.19.2" - resolved "https://registry.yarnpkg.com/undici-types/-/undici-types-7.19.2.tgz#1b67fc26d0f157a0cba3a58a5b5c1e2276b8ba2a" - integrity sha512-qYVnV5OEm2AW8cJMCpdV20CDyaN3g0AjDlOGf1OW4iaDEx8MwdtChUp4zu4H0VP3nDRF/8RKWH+IPp9uW0YGZg== +typescript@^7.0.2: + version "7.0.2" + resolved "https://registry.yarnpkg.com/typescript/-/typescript-7.0.2.tgz#9ec773d7954a8c182c17cc5bbd575aa28bc51582" + integrity sha512-8FYau96o3NKOhbjKi/qNvG/W5jhzxkbdm5sj9AbZ/5T5sWqn3hJgLfGx27sRKZWTvyzCP8dLRBTf5tBTSRVUNA== + optionalDependencies: + "@typescript/typescript-aix-ppc64" "7.0.2" + "@typescript/typescript-darwin-arm64" "7.0.2" + "@typescript/typescript-darwin-x64" "7.0.2" + "@typescript/typescript-freebsd-arm64" "7.0.2" + "@typescript/typescript-freebsd-x64" "7.0.2" + "@typescript/typescript-linux-arm" "7.0.2" + "@typescript/typescript-linux-arm64" "7.0.2" + "@typescript/typescript-linux-loong64" "7.0.2" + "@typescript/typescript-linux-mips64el" "7.0.2" + "@typescript/typescript-linux-ppc64" "7.0.2" + "@typescript/typescript-linux-riscv64" "7.0.2" + "@typescript/typescript-linux-s390x" "7.0.2" + "@typescript/typescript-linux-x64" "7.0.2" + "@typescript/typescript-netbsd-arm64" "7.0.2" + "@typescript/typescript-netbsd-x64" "7.0.2" + "@typescript/typescript-openbsd-arm64" "7.0.2" + "@typescript/typescript-openbsd-x64" "7.0.2" + "@typescript/typescript-sunos-x64" "7.0.2" + "@typescript/typescript-win32-arm64" "7.0.2" + "@typescript/typescript-win32-x64" "7.0.2" + +undici-types@~8.3.0: + version "8.3.0" + resolved "https://registry.yarnpkg.com/undici-types/-/undici-types-8.3.0.tgz#44e9fc9f3244648cdea35e4f9bb2d681e9410809" + integrity sha512-j375ScV60dom+YkPFIfTLcOiPxkN/buHz5GobjLhixFuANaNs3C9l4GmrWqejgXWJ7BbJcFYpTEUkS1Ge8bpZQ== use-callback-ref@^1.3.3: version "1.3.3" @@ -1542,16 +1693,16 @@ victory-vendor@^37.0.2: d3-time "^3.0.0" d3-timer "^3.0.1" -vite@^8.0.11: - version "8.0.11" - resolved "https://registry.yarnpkg.com/vite/-/vite-8.0.11.tgz#d128fe82a0dd24da5127d20560735f1cd7ade0a6" - integrity sha512-Jz1mxtUBR5xTT65VOdJZUUeoyLtqljmFkiUXhPTLZka3RDc9vpi/xXkyrnsdRcm2lIi3l3GPMnAidTsEGIj3Ow== +vite@^8.1.2: + version "8.1.2" + resolved "https://registry.yarnpkg.com/vite/-/vite-8.1.2.tgz#3ac29b5868ccf28c59321391be1ebe906f135ebd" + integrity sha512-6YYPbRXTxx6bRXmOn7XdnQAy5DQNHhDgtjhDHI13oe4pY93kkcdGJWxpGwOm++/Wh0QpQhDrpIoVMrmrsI5AGQ== dependencies: lightningcss "^1.32.0" picomatch "^4.0.4" - postcss "^8.5.14" - rolldown "1.0.0-rc.18" - tinyglobby "^0.2.16" + postcss "^8.5.16" + rolldown "~1.1.3" + tinyglobby "^0.2.17" optionalDependencies: fsevents "~2.3.3" diff --git a/xtask/Cargo.toml b/xtask/Cargo.toml index 2534d698fc..c86c3eee0e 100644 --- a/xtask/Cargo.toml +++ b/xtask/Cargo.toml @@ -2,7 +2,6 @@ name = "xtask" version.workspace = true edition.workspace = true -authors.workspace = true license.workspace = true homepage.workspace = true repository.workspace = true