Compare commits

...
34 Commits
Author SHA1 Message Date
mbecker20 a2d9fa931e default rate limit attempts to 5 per 15 sec 2025-12-07 16:56:22 -08:00
mbecker20 1621043a21 KL-4 must fallback to axum extracted IP for cases not using reverse proxy 2025-12-04 19:31:53 -08:00
mbecker20 6fa5acd1e3 KL-4: Align log consistency 2025-12-01 13:26:45 -08:00
mbecker20 45d6b21a61 KL-1: core_allowed_origins example config should be empty 2025-12-01 12:53:06 -08:00
mbecker20 33bd744052 KL-8 modify action state internal behavior comments 2025-12-01 12:37:16 -08:00
mbecker20 967629545f KL-7 Improve typescript safety: disable allow any 2025-12-01 12:17:22 -08:00
mbecker20 581ef4f7da KL-6 Improve error handling in startup paths 2025-12-01 12:02:41 -08:00
mbecker20 ffc125e288 KL-6 Improve alert cache error handling 2025-12-01 11:42:38 -08:00
mbecker20 23430cc3cb KL-6 Remove monitoring_interval panic 2025-12-01 11:03:41 -08:00
mbecker20 d44be15c14 KL-5 ext: Tighten the skew tolerance re https://curity.io/resources/learn/jwt-best-practices/?utm_source=chatgpt.com#9-dealing-with-time-based-claims 2025-11-30 12:57:54 -08:00
mbecker20 afeb4ac526 KL-5 JWT clock skew tolerance 2025-11-30 12:54:15 -08:00
mbecker20 3e3639231d cargo clippy and bump to rust 1.91.1 2025-11-30 01:30:49 -08:00
mbecker20 dd3fb92eb8 KL-4 ext 2: Improve rate limiting / attempt state conveyance with response 2025-11-30 01:30:24 -08:00
mbecker20 30929838d2 KL-4 ext: Remove brute-force compromising credential failure reasons to improve auth rate limiter effectiveness 2025-11-29 17:53:29 -08:00
mbecker20 8b0c2299be KL-4 Authentication rate limiting 2025-11-29 17:31:46 -08:00
mbecker20 3164cdf5fe KL-2/3 Input validation for local auth, service users, api keys, and variables 2025-11-28 19:11:22 -08:00
mbecker20 4a50b780a6 KL-1 Configurable CORS support 2025-11-28 00:37:44 -08:00
mbecker20 a3a01f1625 add schemas for swarm commands 2025-11-27 23:47:22 -08:00
mbecker20 aec8fa2bf1 task view page with logs 2025-11-23 23:45:28 -08:00
mbecker20 7ff2dba82f swarm service page with logs 2025-11-23 12:48:23 -08:00
mbecker20 9c86b2d239 swarm service logs api 2025-11-23 00:43:59 -08:00
mbecker20 b1fec7c663 docker swarm stack read apis 2025-11-22 16:05:51 -08:00
mbecker20 8341c6b802 sward resources use list items 2025-11-22 02:41:49 -08:00
mbecker20 73285c4374 move swarm to list item + inspect on pages 2025-11-21 14:34:20 -08:00
mbecker20 32d48cdb02 swarm configs view 2025-11-21 13:21:14 -08:00
mbecker20 8e081fd09c swarm config apis 2025-11-21 12:55:37 -08:00
mbecker20 04531f1dea Explore swarm info 2025-11-20 13:00:29 -08:00
mbecker20 80f439d472 basic swarm 2025-11-19 18:02:50 -08:00
mbecker20 d5e03d6d16 prog on swarm 2025-11-19 01:48:43 -08:00
mbecker20 a9f55bb8e6 fix Terminals permissions when no perm on Server 2025-11-17 15:44:14 -08:00
mbecker20 9e765f93f5 bump deps 2025-11-17 15:43:56 -08:00
mbecker20 b3aa8e906f add context to onboarding login error and error messages sent over the communication websocket in general 2025-11-17 13:23:44 -08:00
mbecker20 03fe442aa0 in prog 2025-11-17 12:59:08 -08:00
ChanningHeandGitHub d268009a6a validate compose_cmd_wrapper for required placeholder and add interpolation support (#977) 2025-11-12 10:09:15 -08:00
174 changed files with 14927 additions and 2667 deletions
Generated
+103 -80
View File
@@ -144,9 +144,9 @@ checksum = "69f7f8c3906b62b754cd5326047894316021dcfe5a194c8ea52bdd94934a3457"
[[package]]
name = "async-compression"
version = "0.4.33"
version = "0.4.34"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "93c1f86859c1af3d514fa19e8323147ff10ea98684e6c7b307912509f50e67b2"
checksum = "0e86f6d3dc9dc4352edeea6b8e499e13e3f5dc3b964d7ca5fd411415a3498473"
dependencies = [
"compression-codecs",
"compression-core",
@@ -193,9 +193,9 @@ checksum = "c08606f8c3cbf4ce6ec8e28fb0014a2c086708fe954eaa885384a6165172e7e8"
[[package]]
name = "aws-config"
version = "1.8.10"
version = "1.8.11"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1856b1b48b65f71a4dd940b1c0931f9a7b646d4a924b9828ffefc1454714668a"
checksum = "a0149602eeaf915158e14029ba0c78dedb8c08d554b024d54c8f239aab46511d"
dependencies = [
"aws-credential-types",
"aws-runtime",
@@ -223,9 +223,9 @@ dependencies = [
[[package]]
name = "aws-credential-types"
version = "1.2.9"
version = "1.2.10"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "86590e57ea40121d47d3f2e131bfd873dea15d78dc2f4604f4734537ad9e56c4"
checksum = "b01c9521fa01558f750d183c8c68c81b0155b9d193a4ba7f84c36bd1b6d04a06"
dependencies = [
"aws-smithy-async",
"aws-smithy-runtime-api",
@@ -260,9 +260,9 @@ dependencies = [
[[package]]
name = "aws-runtime"
version = "1.5.14"
version = "1.5.16"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8fe0fd441565b0b318c76e7206c8d1d0b0166b3e986cf30e890b61feb6192045"
checksum = "7ce527fb7e53ba9626fc47824f25e256250556c40d8f81d27dd92aa38239d632"
dependencies = [
"aws-credential-types",
"aws-sigv4",
@@ -284,9 +284,9 @@ dependencies = [
[[package]]
name = "aws-sdk-ec2"
version = "1.184.0"
version = "1.194.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f78a1031a701f3f82e3e9d7087cd09e7ef69bd7e592719521c873d8f02c464d6"
checksum = "f55f9073b102788b24a8382fdadd27f5bf34d5ff4869eb88cdf251a455599e63"
dependencies = [
"aws-credential-types",
"aws-runtime",
@@ -307,9 +307,9 @@ dependencies = [
[[package]]
name = "aws-sdk-sso"
version = "1.89.0"
version = "1.90.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a9c1b1af02288f729e95b72bd17988c009aa72e26dcb59b3200f86d7aea726c9"
checksum = "4f18e53542c522459e757f81e274783a78f8c81acdfc8d1522ee8a18b5fb1c66"
dependencies = [
"aws-credential-types",
"aws-runtime",
@@ -329,9 +329,9 @@ dependencies = [
[[package]]
name = "aws-sdk-ssooidc"
version = "1.91.0"
version = "1.92.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "4e8122301558dc7c6c68e878af918880b82ff41897a60c8c4e18e4dc4d93e9f1"
checksum = "532f4d866012ffa724a4385c82e8dd0e59f0ca0e600f3f22d4c03b6824b34e4a"
dependencies = [
"aws-credential-types",
"aws-runtime",
@@ -351,9 +351,9 @@ dependencies = [
[[package]]
name = "aws-sdk-sts"
version = "1.92.0"
version = "1.94.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a0c7808adcff8333eaa76a849e6de926c6ac1a1268b9fd6afe32de9c29ef29d2"
checksum = "1be6fbbfa1a57724788853a623378223fe828fc4c09b146c992f0c95b6256174"
dependencies = [
"aws-credential-types",
"aws-runtime",
@@ -576,9 +576,9 @@ dependencies = [
[[package]]
name = "axum"
version = "0.8.6"
version = "0.8.7"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8a18ed336352031311f4e0b4dd2ff392d4fbb370777c9d18d7fc9d7359f73871"
checksum = "5b098575ebe77cb6d14fc7f32749631a6e44edbef6b796f89b020e99ba20d425"
dependencies = [
"axum-core",
"axum-macros",
@@ -632,9 +632,9 @@ dependencies = [
[[package]]
name = "axum-extra"
version = "0.12.1"
version = "0.12.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5136e6c5e7e7978fe23e9876fb924af2c0f84c72127ac6ac17e7c46f457d362c"
checksum = "dbfe9f610fe4e99cf0cfcd03ccf8c63c28c616fe714d80475ef731f3b13dd21b"
dependencies = [
"axum",
"axum-core",
@@ -665,9 +665,9 @@ dependencies = [
[[package]]
name = "axum-server"
version = "0.7.2"
version = "0.7.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "495c05f60d6df0093e8fb6e74aa5846a0ad06abaf96d76166283720bf740f8ab"
checksum = "c1ab4a3ec9ea8a657c72d99a03a824af695bd0fb5ec639ccbd9cd3543b41a5f9"
dependencies = [
"arc-swap",
"bytes",
@@ -867,7 +867,7 @@ dependencies = [
"getrandom 0.2.16",
"getrandom 0.3.3",
"hex",
"indexmap 2.12.0",
"indexmap 2.12.1",
"js-sys",
"once_cell",
"rand 0.9.2",
@@ -892,9 +892,9 @@ checksum = "1fd0f2584146f6f2ef48085050886acf353beff7305ebd1ae69500e27c67f64b"
[[package]]
name = "bytes"
version = "1.10.1"
version = "1.11.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d71b6127be86fdcfddb610f7182ac57211d4b18a3e9c82eb2d17662f2227ad6a"
checksum = "b35204fbdc0b3f4446b89fc1ac2cf84a8a68971995d0bf2e925ec7cd960f9cb3"
[[package]]
name = "bytes-utils"
@@ -908,7 +908,7 @@ dependencies = [
[[package]]
name = "cache"
version = "2.0.0-dev-90"
version = "2.0.0-dev-91"
dependencies = [
"anyhow",
"tokio",
@@ -1025,9 +1025,9 @@ dependencies = [
[[package]]
name = "clap"
version = "4.5.51"
version = "4.5.53"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "4c26d721170e0295f191a69bd9a1f93efcdb0aff38684b61ab5750468972e5f5"
checksum = "c9e340e012a1bf4935f5282ed1436d1489548e8f72308207ea5df0e23d2d03f8"
dependencies = [
"clap_builder",
"clap_derive",
@@ -1035,9 +1035,9 @@ dependencies = [
[[package]]
name = "clap_builder"
version = "4.5.51"
version = "4.5.53"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "75835f0c7bf681bfd05abe44e965760fea999a5286c6eb2d59883634fd02011a"
checksum = "d76b5d13eaa18c901fd2f7fca939fefe3a0727a953561fefdf3b2922b8569d00"
dependencies = [
"anstream",
"anstyle",
@@ -1100,7 +1100,7 @@ dependencies = [
[[package]]
name = "command"
version = "2.0.0-dev-90"
version = "2.0.0-dev-91"
dependencies = [
"komodo_client",
"shlex",
@@ -1110,9 +1110,9 @@ dependencies = [
[[package]]
name = "compression-codecs"
version = "0.4.32"
version = "0.4.33"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "680dc087785c5230f8e8843e2e57ac7c1c90488b6a91b88caa265410568f441b"
checksum = "302266479cb963552d11bd042013a58ef1adc56768016c8b82b4199488f2d4ad"
dependencies = [
"compression-core",
"flate2",
@@ -1121,16 +1121,16 @@ dependencies = [
[[package]]
name = "compression-core"
version = "0.4.30"
version = "0.4.31"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3a9b614a5787ef0c8802a55766480563cb3a93b435898c422ed2a359cf811582"
checksum = "75984efb6ed102a0d42db99afb6c1948f0380d1d91808d5529916e6c08b49d8d"
[[package]]
name = "config"
version = "2.0.0-dev-90"
version = "2.0.0-dev-91"
dependencies = [
"colored",
"indexmap 2.12.0",
"indexmap 2.12.1",
"regex",
"serde",
"serde_json",
@@ -1449,7 +1449,7 @@ checksum = "2a2330da5de22e8a3cb63252ce2abb30116bf5265e89c0e01bc17015ce30a476"
[[package]]
name = "database"
version = "2.0.0-dev-90"
version = "2.0.0-dev-91"
dependencies = [
"anyhow",
"async-compression",
@@ -1748,7 +1748,7 @@ dependencies = [
[[package]]
name = "encoding"
version = "2.0.0-dev-90"
version = "2.0.0-dev-91"
dependencies = [
"anyhow",
"bytes",
@@ -1769,9 +1769,9 @@ dependencies = [
[[package]]
name = "english-to-cron"
version = "0.1.6"
version = "0.1.7"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e26fb7377cbec9a94f60428e6e6afbe10c699a14639b4d3d4b67b25c0bbe0806"
checksum = "3c3d16f6dc9dc43a9a2fd5bce09b6cf8df250dcf77cffdaa66be21c527e2d05c"
dependencies = [
"regex",
]
@@ -1790,7 +1790,7 @@ dependencies = [
[[package]]
name = "environment"
version = "2.0.0-dev-90"
version = "2.0.0-dev-91"
dependencies = [
"anyhow",
"formatting",
@@ -1800,7 +1800,7 @@ dependencies = [
[[package]]
name = "environment_file"
version = "2.0.0-dev-90"
version = "2.0.0-dev-91"
dependencies = [
"thiserror 2.0.17",
]
@@ -1902,7 +1902,7 @@ dependencies = [
[[package]]
name = "formatting"
version = "2.0.0-dev-90"
version = "2.0.0-dev-91"
dependencies = [
"serror",
]
@@ -2068,7 +2068,7 @@ dependencies = [
[[package]]
name = "git"
version = "2.0.0-dev-90"
version = "2.0.0-dev-91"
dependencies = [
"anyhow",
"cache",
@@ -2107,7 +2107,7 @@ dependencies = [
"futures-sink",
"futures-util",
"http 0.2.12",
"indexmap 2.12.0",
"indexmap 2.12.1",
"slab",
"tokio",
"tokio-util",
@@ -2126,7 +2126,7 @@ dependencies = [
"futures-core",
"futures-sink",
"http 1.3.1",
"indexmap 2.12.0",
"indexmap 2.12.1",
"slab",
"tokio",
"tokio-util",
@@ -2158,9 +2158,9 @@ dependencies = [
[[package]]
name = "hashbrown"
version = "0.16.0"
version = "0.16.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5419bdc4f6a9207fbeba6d11b604d481addf78ecd10c11ad51e76c2f6482748d"
checksum = "841d1cc9bed7f9236f321df977030373f4a4163ae1a7dbfe1a51a2c1a51d9100"
[[package]]
name = "headers"
@@ -2681,12 +2681,12 @@ dependencies = [
[[package]]
name = "indexmap"
version = "2.12.0"
version = "2.12.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6717a8d2a5a929a1a2eb43a12812498ed141a0bcfb7e8f7844fbdbe4303bba9f"
checksum = "0ad4bb2b565bca0645f4d68c5c9af97fba094e9791da685bf83cb5f3ce74acf2"
dependencies = [
"equivalent",
"hashbrown 0.16.0",
"hashbrown 0.16.1",
"serde",
"serde_core",
]
@@ -2702,7 +2702,7 @@ dependencies = [
[[package]]
name = "interpolate"
version = "2.0.0-dev-90"
version = "2.0.0-dev-91"
dependencies = [
"anyhow",
"komodo_client",
@@ -2824,7 +2824,7 @@ dependencies = [
[[package]]
name = "komodo_cli"
version = "2.0.0-dev-90"
version = "2.0.0-dev-91"
dependencies = [
"anyhow",
"chrono",
@@ -2852,7 +2852,7 @@ dependencies = [
[[package]]
name = "komodo_client"
version = "2.0.0-dev-90"
version = "2.0.0-dev-91"
dependencies = [
"anyhow",
"async_timing_util",
@@ -2865,7 +2865,7 @@ dependencies = [
"derive_variants",
"envy",
"futures-util",
"indexmap 2.12.0",
"indexmap 2.12.1",
"ipnetwork",
"mongo_indexed",
"partial_derive2",
@@ -2888,7 +2888,7 @@ dependencies = [
[[package]]
name = "komodo_core"
version = "2.0.0-dev-90"
version = "2.0.0-dev-91"
dependencies = [
"anyhow",
"arc-swap",
@@ -2922,7 +2922,7 @@ dependencies = [
"git",
"hex",
"hmac",
"indexmap 2.12.0",
"indexmap 2.12.1",
"interpolate",
"jsonwebtoken",
"komodo_client",
@@ -2931,6 +2931,7 @@ dependencies = [
"openidconnect",
"partial_derive2",
"periphery_client",
"rate_limit",
"regex",
"reqwest",
"resolver_api",
@@ -2957,12 +2958,13 @@ dependencies = [
"url",
"urlencoding",
"uuid",
"validations",
"wildcard",
]
[[package]]
name = "komodo_periphery"
version = "2.0.0-dev-90"
version = "2.0.0-dev-91"
dependencies = [
"anyhow",
"arc-swap",
@@ -3083,7 +3085,7 @@ checksum = "34080505efa8e45a4b816c349525ebe327ceaa8559756f0356cba97ef3bf7432"
[[package]]
name = "logger"
version = "2.0.0-dev-90"
version = "2.0.0-dev-91"
dependencies = [
"anyhow",
"komodo_client",
@@ -3375,7 +3377,7 @@ dependencies = [
[[package]]
name = "noise"
version = "2.0.0-dev-90"
version = "2.0.0-dev-91"
dependencies = [
"anyhow",
"arc-swap",
@@ -3799,7 +3801,7 @@ checksum = "9b4f627cb1b25917193a259e49bdad08f671f8d9708acfd5fe0a8c1455d87220"
[[package]]
name = "periphery_client"
version = "2.0.0-dev-90"
version = "2.0.0-dev-91"
dependencies = [
"anyhow",
"derive_variants",
@@ -4142,6 +4144,17 @@ dependencies = [
"getrandom 0.3.3",
]
[[package]]
name = "rate_limit"
version = "2.0.0-dev-91"
dependencies = [
"anyhow",
"axum",
"cache",
"serror",
"tokio",
]
[[package]]
name = "redox_syscall"
version = "0.5.18"
@@ -4280,7 +4293,7 @@ dependencies = [
[[package]]
name = "response"
version = "2.0.0-dev-90"
version = "2.0.0-dev-91"
dependencies = [
"anyhow",
"axum",
@@ -4550,7 +4563,7 @@ dependencies = [
[[package]]
name = "secret_file"
version = "2.0.0-dev-90"
version = "2.0.0-dev-91"
dependencies = [
"tokio",
]
@@ -4653,7 +4666,7 @@ version = "1.0.145"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "402a6f66d8c709116cf22f558eab210f5a50187f702eb4d7e5ef38d9a7f1c79c"
dependencies = [
"indexmap 2.12.0",
"indexmap 2.12.1",
"itoa",
"memchr",
"ryu",
@@ -4734,7 +4747,7 @@ dependencies = [
"chrono",
"hex",
"indexmap 1.9.3",
"indexmap 2.12.0",
"indexmap 2.12.1",
"schemars 0.9.0",
"schemars 1.0.4",
"serde_core",
@@ -4761,7 +4774,7 @@ version = "0.10.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7b4db627b98b36d4203a7b458cf3573730f2bb591b28871d916dfa9efabfd41f"
dependencies = [
"indexmap 2.12.0",
"indexmap 2.12.1",
"itoa",
"ryu",
"serde",
@@ -5348,7 +5361,7 @@ version = "0.9.8"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f0dc8b1fb61449e27716ec0e1bdf0f6b8f3e8f6b05391e8497b8b6d7804ea6d8"
dependencies = [
"indexmap 2.12.0",
"indexmap 2.12.1",
"serde_core",
"serde_spanned",
"toml_datetime",
@@ -5440,7 +5453,7 @@ checksum = "d039ad9159c98b70ecfd540b2573b97f7f52c3e8d9f8ad57a24b916a536975f9"
dependencies = [
"futures-core",
"futures-util",
"indexmap 2.12.0",
"indexmap 2.12.1",
"pin-project-lite",
"slab",
"sync_wrapper",
@@ -5453,9 +5466,9 @@ dependencies = [
[[package]]
name = "tower-http"
version = "0.6.6"
version = "0.6.7"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "adc82fd73de2a9722ac5da747f12383d2bfdb93591ee6c58486e0097890f05f2"
checksum = "9cf146f99d442e8e68e585f5d798ccd3cad9a7835b917e09728880a862706456"
dependencies = [
"bitflags 2.9.4",
"bytes",
@@ -5493,9 +5506,9 @@ checksum = "8df9b6e13f2d32c91b9bd719c00d1958837bc7dec474d94952798cc8e69eeec3"
[[package]]
name = "tracing"
version = "0.1.41"
version = "0.1.43"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "784e0ac535deb450455cbfa28a6f0df145ea1bb7ae51b821cf5e7927fdcfbdd0"
checksum = "2d15d90a0b5c19378952d479dc858407149d7bb45a14de0142f6c534b16fc647"
dependencies = [
"log",
"pin-project-lite",
@@ -5505,9 +5518,9 @@ dependencies = [
[[package]]
name = "tracing-attributes"
version = "0.1.30"
version = "0.1.31"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "81383ab64e72a7a8b8e13130c49e3dab29def6d0c7d76a03087b3cf71c5c6903"
checksum = "7490cfa5ec963746568740651ac6781f701c9c5ea257c58e057f3ba8cf69e8da"
dependencies = [
"proc-macro2",
"quote",
@@ -5516,9 +5529,9 @@ dependencies = [
[[package]]
name = "tracing-core"
version = "0.1.34"
version = "0.1.35"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b9d12581f227e93f094d3af2ae690a574abb8a2b9b7a96e7cfe9647b2b617678"
checksum = "7a04e24fab5c89c6a36eb8558c9656f30d81de51dfa4d3b45f26b21d61fa0a6c"
dependencies = [
"once_cell",
"valuable",
@@ -5566,9 +5579,9 @@ dependencies = [
[[package]]
name = "tracing-subscriber"
version = "0.3.20"
version = "0.3.22"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "2054a14f5307d601f88daf0553e1cbf472acc4f2c51afab632431cdcd72124d5"
checksum = "2f30143827ddab0d256fd843b7a66d164e9f271cfa0dde49142c5ca0ca291f1e"
dependencies = [
"nu-ansi-term",
"serde",
@@ -5583,7 +5596,7 @@ dependencies = [
[[package]]
name = "transport"
version = "2.0.0-dev-90"
version = "2.0.0-dev-91"
dependencies = [
"anyhow",
"axum",
@@ -5804,6 +5817,16 @@ dependencies = [
"wasm-bindgen",
]
[[package]]
name = "validations"
version = "2.0.0-dev-91"
dependencies = [
"anyhow",
"bson",
"regex",
"url",
]
[[package]]
name = "valuable"
version = "0.1.1"
+18 -16
View File
@@ -8,7 +8,7 @@ members = [
]
[workspace.package]
version = "2.0.0-dev-90"
version = "2.0.0-dev-91"
edition = "2024"
authors = ["mbecker20 <becker.maxh@gmail.com>"]
license = "GPL-3.0-or-later"
@@ -26,7 +26,9 @@ environment_file = { path = "lib/environment_file" }
environment = { path = "lib/environment" }
interpolate = { path = "lib/interpolate" }
secret_file = { path = "lib/secret_file" }
validations = { path = "lib/validations" }
formatting = { path = "lib/formatting" }
rate_limit = { path = "lib/rate_limit" }
transport = { path = "lib/transport" }
database = { path = "lib/database" }
encoding = { path = "lib/encoding" }
@@ -63,14 +65,14 @@ arc-swap = "1.7.1"
# SERVER
tokio-tungstenite = { version = "0.28.0", features = ["rustls-tls-native-roots"] }
axum-extra = { version = "0.12.1", features = ["typed-header"] }
tower-http = { version = "0.6.6", features = ["fs", "cors"] }
axum-server = { version = "0.7.2", features = ["tls-rustls"] }
axum = { version = "0.8.6", features = ["ws", "json", "macros"] }
axum-extra = { version = "0.12.2", features = ["typed-header"] }
tower-http = { version = "0.6.7", features = ["fs", "cors", "set-header"] }
axum-server = { version = "0.7.3", features = ["tls-rustls"] }
axum = { version = "0.8.7", features = ["ws", "json", "macros"] }
# SER/DE
ipnetwork = { version = "0.21.1", features = ["serde"] }
indexmap = { version = "2.12.0", features = ["serde"] }
indexmap = { version = "2.12.1", features = ["serde"] }
serde = { version = "1.0.227", features = ["derive"] }
strum = { version = "0.27.2", features = ["derive"] }
bson = { version = "2.15.0" } # must keep in sync with mongodb version
@@ -87,23 +89,23 @@ thiserror = "2.0.17"
# LOGGING
opentelemetry-otlp = { version = "0.31.0", features = ["tls-roots", "reqwest-rustls"] }
opentelemetry_sdk = { version = "0.31.0", features = ["rt-tokio"] }
tracing-subscriber = { version = "0.3.20", features = ["json"] }
tracing-subscriber = { version = "0.3.22", features = ["json"] }
opentelemetry-semantic-conventions = "0.31.0"
tracing-opentelemetry = "0.32.0"
opentelemetry = "0.31.0"
tracing = "0.1.41"
tracing = "0.1.43"
# CONFIG
clap = { version = "4.5.51", features = ["derive"] }
clap = { version = "4.5.53", features = ["derive"] }
dotenvy = "0.15.7"
envy = "0.4.2"
# CRYPTO / AUTH
openidconnect = { version = "4.0.1", features = ["accept-rfc3339-timestamps"] }
uuid = { version = "1.18.1", features = ["v4", "fast-rng", "serde"] }
jsonwebtoken = { version = "10.2.0", features = ["aws_lc_rs"] } # locked back with octorust
rustls = { version = "0.23.35", features = ["aws-lc-rs"] }
pem-rfc7468 = { version = "1.0.0", features = ["alloc"] }
openidconnect = "4.0.1"
urlencoding = "2.1.3"
bcrypt = "0.17.1"
base64 = "0.22.1"
@@ -127,23 +129,23 @@ sysinfo = "0.37.1"
shlex = "1.3.0"
# CLOUD
aws-config = "1.8.10"
aws-sdk-ec2 = "1.184.0"
aws-credential-types = "1.2.9"
aws-config = "1.8.11"
aws-sdk-ec2 = "1.194.0"
aws-credential-types = "1.2.10"
## CRON
english-to-cron = "0.1.6"
english-to-cron = "0.1.7"
chrono-tz = "0.10.4"
chrono = "0.4.42"
croner = "3.0.1"
# MISC
async-compression = { version = "0.4.33", features = ["tokio", "gzip"] }
async-compression = { version = "0.4.34", features = ["tokio", "gzip"] }
derive_builder = "0.20.2"
comfy-table = "7.2.1"
typeshare = "1.0.4"
dashmap = "6.1.0"
wildcard = "0.3.0"
colored = "3.0.0"
bytes = "1.10.1"
bytes = "1.11.0"
regex = "1.12.2"
+1 -1
View File
@@ -1,7 +1,7 @@
## Builds the Komodo Core, Periphery, and Util binaries
## for a specific architecture.
FROM rust:1.90.0-bullseye AS builder
FROM rust:1.91.1-bullseye AS builder
RUN cargo install cargo-strip
WORKDIR /builder
+1 -1
View File
@@ -1,4 +1,4 @@
FROM rust:1.90.0-bullseye AS builder
FROM rust:1.91.1-bullseye AS builder
RUN cargo install cargo-strip
WORKDIR /builder
+2
View File
@@ -20,7 +20,9 @@ periphery_client.workspace = true
environment_file.workspace = true
interpolate.workspace = true
secret_file.workspace = true
validations.workspace = true
formatting.workspace = true
rate_limit.workspace = true
transport.workspace = true
database.workspace = true
encoding.workspace = true
+1 -1
View File
@@ -1,7 +1,7 @@
## All in one, multi stage compile + runtime Docker build for your architecture.
# Build Core
FROM rust:1.90.0-trixie AS core-builder
FROM rust:1.91.1-trixie AS core-builder
RUN cargo install cargo-strip
WORKDIR /builder
+2 -2
View File
@@ -232,9 +232,9 @@ pub async fn send_alert(
format!(
"{level} | {message}{}",
if details.is_empty() {
format_args!("")
String::new()
} else {
format_args!("\n{details}")
format!("\n{details}")
}
)
}
+2 -2
View File
@@ -466,9 +466,9 @@ fn standard_alert_content(alert: &Alert) -> String {
format!(
"{level} | {message}{}",
if details.is_empty() {
format_args!("")
String::new()
} else {
format_args!("\n{details}")
format!("\n{details}")
}
)
}
+46 -15
View File
@@ -1,8 +1,18 @@
use std::{sync::OnceLock, time::Instant};
use std::{
net::{IpAddr, SocketAddr},
sync::OnceLock,
time::Instant,
};
use axum::{Router, extract::Path, http::HeaderMap, routing::post};
use axum::{
Router,
extract::{ConnectInfo, Path},
http::HeaderMap,
routing::post,
};
use derive_variants::{EnumVariants, ExtractVariant};
use komodo_client::{api::auth::*, entities::user::User};
use rate_limit::WithFailureRateLimit;
use reqwest::StatusCode;
use resolver_api::Resolve;
use response::Response;
@@ -21,14 +31,16 @@ use crate::{
},
config::core_config,
helpers::query::get_user,
state::jwt_client,
state::{auth_rate_limiter, jwt_client},
};
use super::Variant;
#[derive(Default)]
pub struct AuthArgs {
pub headers: HeaderMap,
/// Prefer extracting IP from headers.
/// This IP will be the IP of reverse proxy itself.
pub ip: IpAddr,
}
#[typeshare]
@@ -78,6 +90,7 @@ pub fn router() -> Router {
async fn variant_handler(
headers: HeaderMap,
info: ConnectInfo<SocketAddr>,
Path(Variant { variant }): Path<Variant>,
Json(params): Json<serde_json::Value>,
) -> serror::Result<axum::response::Response> {
@@ -85,11 +98,12 @@ async fn variant_handler(
"type": variant,
"params": params,
}))?;
handler(headers, Json(req)).await
handler(headers, info, Json(req)).await
}
async fn handler(
headers: HeaderMap,
ConnectInfo(info): ConnectInfo<SocketAddr>,
Json(request): Json<AuthRequest>,
) -> serror::Result<axum::response::Response> {
let timer = Instant::now();
@@ -98,7 +112,12 @@ async fn handler(
"/auth request {req_id} | METHOD: {:?}",
request.extract_variant()
);
let res = request.resolve(&AuthArgs { headers }).await;
let res = request
.resolve(&AuthArgs {
headers,
ip: info.ip(),
})
.await;
if let Err(e) = &res {
debug!("/auth request {req_id} | error: {:#}", e.error);
}
@@ -135,25 +154,37 @@ impl Resolve<AuthArgs> for GetLoginOptions {
impl Resolve<AuthArgs> for ExchangeForJwt {
async fn resolve(
self,
_: &AuthArgs,
AuthArgs { headers, ip }: &AuthArgs,
) -> serror::Result<ExchangeForJwtResponse> {
jwt_client()
.redeem_exchange_token(&self.token)
.with_failure_rate_limit_using_headers(
auth_rate_limiter(),
headers,
Some(*ip),
)
.await
.map_err(Into::into)
}
}
impl Resolve<AuthArgs> for GetUser {
async fn resolve(
self,
AuthArgs { headers }: &AuthArgs,
AuthArgs { headers, ip }: &AuthArgs,
) -> serror::Result<User> {
let user_id = get_user_id_from_headers(headers)
.await
.status_code(StatusCode::UNAUTHORIZED)?;
get_user(&user_id)
.await
.status_code(StatusCode::UNAUTHORIZED)
async {
let user_id = get_user_id_from_headers(headers)
.await
.status_code(StatusCode::UNAUTHORIZED)?;
get_user(&user_id)
.await
.status_code(StatusCode::UNAUTHORIZED)
}
.with_failure_rate_limit_using_headers(
auth_rate_limiter(),
headers,
Some(*ip),
)
.await
}
}
+3 -3
View File
@@ -142,13 +142,13 @@ pub enum ExecuteRequest {
RunAction(RunAction),
BatchRunAction(BatchRunAction),
// ==== SYNC ====
RunSync(RunSync),
// ==== ALERTER ====
TestAlerter(TestAlerter),
SendAlert(SendAlert),
// ==== SYNC ====
RunSync(RunSync),
// ==== MAINTENANCE ====
ClearRepoCache(ClearRepoCache),
BackupCoreDatabase(BackupCoreDatabase),
+31 -27
View File
@@ -136,34 +136,10 @@ impl Resolve<ExecuteArgs> for RunSync {
};
match ObjectId::from_str(&name_or_id) {
Ok(_) => match resource_type {
ResourceTargetVariant::Alerter => all_resources
.alerters
ResourceTargetVariant::Swarm => all_resources
.swarms
.get(&name_or_id)
.map(|a| a.name.clone()),
ResourceTargetVariant::Build => all_resources
.builds
.get(&name_or_id)
.map(|b| b.name.clone()),
ResourceTargetVariant::Builder => all_resources
.builders
.get(&name_or_id)
.map(|b| b.name.clone()),
ResourceTargetVariant::Deployment => all_resources
.deployments
.get(&name_or_id)
.map(|d| d.name.clone()),
ResourceTargetVariant::Procedure => all_resources
.procedures
.get(&name_or_id)
.map(|p| p.name.clone()),
ResourceTargetVariant::Action => all_resources
.actions
.get(&name_or_id)
.map(|p| p.name.clone()),
ResourceTargetVariant::Repo => all_resources
.repos
.get(&name_or_id)
.map(|r| r.name.clone()),
.map(|s| s.name.clone()),
ResourceTargetVariant::Server => all_resources
.servers
.get(&name_or_id)
@@ -172,10 +148,38 @@ impl Resolve<ExecuteArgs> for RunSync {
.stacks
.get(&name_or_id)
.map(|s| s.name.clone()),
ResourceTargetVariant::Deployment => all_resources
.deployments
.get(&name_or_id)
.map(|d| d.name.clone()),
ResourceTargetVariant::Build => all_resources
.builds
.get(&name_or_id)
.map(|b| b.name.clone()),
ResourceTargetVariant::Repo => all_resources
.repos
.get(&name_or_id)
.map(|r| r.name.clone()),
ResourceTargetVariant::Procedure => all_resources
.procedures
.get(&name_or_id)
.map(|p| p.name.clone()),
ResourceTargetVariant::Action => all_resources
.actions
.get(&name_or_id)
.map(|p| p.name.clone()),
ResourceTargetVariant::ResourceSync => all_resources
.syncs
.get(&name_or_id)
.map(|s| s.name.clone()),
ResourceTargetVariant::Builder => all_resources
.builders
.get(&name_or_id)
.map(|b| b.name.clone()),
ResourceTargetVariant::Alerter => all_resources
.alerters
.get(&name_or_id)
.map(|a| a.name.clone()),
ResourceTargetVariant::System => None,
},
Err(_) => Some(name_or_id),
@@ -5,10 +5,9 @@ use hmac::{Hmac, Mac};
use serde::Deserialize;
use sha2::Sha256;
use crate::{
config::core_config,
listener::{ExtractBranch, VerifySecret},
};
use crate::config::core_config;
use super::{ExtractBranch, VerifySecret};
type HmacSha256 = Hmac<Sha256>;
@@ -18,7 +17,7 @@ pub struct Github;
impl VerifySecret for Github {
#[instrument("VerifyGithubSecret", skip_all)]
fn verify_secret(
headers: HeaderMap,
headers: &HeaderMap,
body: &str,
custom_secret: &str,
) -> anyhow::Result<()> {
@@ -1,10 +1,10 @@
use anyhow::{Context, anyhow};
use axum::http::HeaderMap;
use serde::Deserialize;
use crate::{
config::core_config,
listener::{ExtractBranch, VerifySecret},
};
use crate::config::core_config;
use super::{ExtractBranch, VerifySecret};
/// Listener implementation for Gitlab type API
pub struct Gitlab;
@@ -12,7 +12,7 @@ pub struct Gitlab;
impl VerifySecret for Gitlab {
#[instrument("VerifyGitlabSecret", skip_all)]
fn verify_secret(
headers: axum::http::HeaderMap,
headers: &HeaderMap,
_body: &str,
custom_secret: &str,
) -> anyhow::Result<()> {
@@ -0,0 +1,4 @@
pub mod github;
pub mod gitlab;
use super::{ExtractBranch, VerifySecret};
@@ -32,7 +32,7 @@ trait CustomSecret: KomodoResource {
/// Implemented on the integration struct, eg [integrations::github::Github]
trait VerifySecret {
fn verify_secret(
headers: HeaderMap,
headers: &HeaderMap,
body: &str,
custom_secret: &str,
) -> anyhow::Result<()>;
@@ -1,14 +1,22 @@
use axum::{Router, extract::Path, http::HeaderMap, routing::post};
use std::net::{IpAddr, SocketAddr};
use axum::{
Router,
extract::{ConnectInfo, Path},
http::HeaderMap,
routing::post,
};
use komodo_client::entities::{
action::Action, build::Build, procedure::Procedure, repo::Repo,
resource::Resource, stack::Stack, sync::ResourceSync,
};
use rate_limit::WithFailureRateLimit;
use reqwest::StatusCode;
use serde::Deserialize;
use serror::AddStatusCode;
use tracing::Instrument;
use crate::resource::KomodoResource;
use crate::{resource::KomodoResource, state::auth_rate_limiter};
use super::{
CustomSecret, ExtractBranch, VerifySecret,
@@ -47,9 +55,9 @@ pub fn router<P: VerifySecret + ExtractBranch>() -> Router {
.route(
"/build/{id}",
post(
|Path(Id { id }), headers: HeaderMap, body: String| async move {
|Path(Id { id }), headers: HeaderMap, ConnectInfo(info): ConnectInfo<SocketAddr>, body: String| async move {
let build =
auth_webhook::<P, Build>(&id, headers, &body).await?;
auth_webhook::<P, Build>(&id, &headers, info.ip(), &body).await?;
tokio::spawn(async move {
let span = info_span!("BuildWebhook", id);
async {
@@ -73,9 +81,9 @@ pub fn router<P: VerifySecret + ExtractBranch>() -> Router {
.route(
"/repo/{id}/{option}",
post(
|Path(IdAndOption::<RepoWebhookOption> { id, option }), headers: HeaderMap, body: String| async move {
|Path(IdAndOption::<RepoWebhookOption> { id, option }), headers: HeaderMap, ConnectInfo(info): ConnectInfo<SocketAddr>, body: String| async move {
let repo =
auth_webhook::<P, Repo>(&id, headers, &body).await?;
auth_webhook::<P, Repo>(&id, &headers, info.ip(), &body).await?;
tokio::spawn(async move {
let span = info_span!("RepoWebhook", id);
async {
@@ -99,9 +107,9 @@ pub fn router<P: VerifySecret + ExtractBranch>() -> Router {
.route(
"/stack/{id}/{option}",
post(
|Path(IdAndOption::<StackWebhookOption> { id, option }), headers: HeaderMap, body: String| async move {
|Path(IdAndOption::<StackWebhookOption> { id, option }), headers: HeaderMap, ConnectInfo(info): ConnectInfo<SocketAddr>, body: String| async move {
let stack =
auth_webhook::<P, Stack>(&id, headers, &body).await?;
auth_webhook::<P, Stack>(&id, &headers, info.ip(), &body).await?;
tokio::spawn(async move {
let span = info_span!("StackWebhook", id);
async {
@@ -125,9 +133,9 @@ pub fn router<P: VerifySecret + ExtractBranch>() -> Router {
.route(
"/sync/{id}/{option}",
post(
|Path(IdAndOption::<SyncWebhookOption> { id, option }), headers: HeaderMap, body: String| async move {
|Path(IdAndOption::<SyncWebhookOption> { id, option }), headers: HeaderMap, ConnectInfo(info): ConnectInfo<SocketAddr>, body: String| async move {
let sync =
auth_webhook::<P, ResourceSync>(&id, headers, &body).await?;
auth_webhook::<P, ResourceSync>(&id, &headers, info.ip(), &body).await?;
tokio::spawn(async move {
let span = info_span!("ResourceSyncWebhook", id);
async {
@@ -151,9 +159,9 @@ pub fn router<P: VerifySecret + ExtractBranch>() -> Router {
.route(
"/procedure/{id}/{branch}",
post(
|Path(IdAndBranch { id, branch }), headers: HeaderMap, body: String| async move {
|Path(IdAndBranch { id, branch }), headers: HeaderMap, ConnectInfo(info): ConnectInfo<SocketAddr>, body: String| async move {
let procedure =
auth_webhook::<P, Procedure>(&id, headers, &body).await?;
auth_webhook::<P, Procedure>(&id, &headers, info.ip(), &body).await?;
tokio::spawn(async move {
let span = info_span!("ProcedureWebhook", id);
async {
@@ -177,9 +185,9 @@ pub fn router<P: VerifySecret + ExtractBranch>() -> Router {
.route(
"/action/{id}/{branch}",
post(
|Path(IdAndBranch { id, branch }), headers: HeaderMap, body: String| async move {
|Path(IdAndBranch { id, branch }), headers: HeaderMap, ConnectInfo(info): ConnectInfo<SocketAddr>, body: String| async move {
let action =
auth_webhook::<P, Action>(&id, headers, &body).await?;
auth_webhook::<P, Action>(&id, &headers, info.ip(), &body).await?;
tokio::spawn(async move {
let span = info_span!("ActionWebhook", id);
async {
@@ -204,17 +212,26 @@ pub fn router<P: VerifySecret + ExtractBranch>() -> Router {
async fn auth_webhook<P, R>(
id: &str,
headers: HeaderMap,
headers: &HeaderMap,
ip: IpAddr,
body: &str,
) -> serror::Result<Resource<R::Config, R::Info>>
where
P: VerifySecret,
R: KomodoResource + CustomSecret,
{
let resource = crate::resource::get::<R>(id)
.await
.status_code(StatusCode::BAD_REQUEST)?;
P::verify_secret(headers, body, R::custom_secret(&resource))
.status_code(StatusCode::UNAUTHORIZED)?;
Ok(resource)
async {
let resource = crate::resource::get::<R>(id)
.await
.status_code(StatusCode::BAD_REQUEST)?;
P::verify_secret(headers, body, R::custom_secret(&resource))
.status_code(StatusCode::UNAUTHORIZED)?;
serror::Result::Ok(resource)
}
.with_failure_rate_limit_using_headers(
auth_rate_limiter(),
headers,
Some(ip),
)
.await
}
+60 -1
View File
@@ -1,11 +1,70 @@
use axum::{
Router,
http::{HeaderName, HeaderValue},
routing::get,
};
use tower_http::{
services::{ServeDir, ServeFile},
set_header::SetResponseHeaderLayer,
};
use crate::{
config::{core_config, cors_layer},
ts_client,
};
pub mod auth;
pub mod execute;
pub mod read;
pub mod terminal;
pub mod user;
pub mod write;
mod listener;
mod terminal;
mod ws;
#[derive(serde::Deserialize)]
struct Variant {
variant: String,
}
pub fn app() -> Router {
let config = core_config();
// Setup static frontend services
let frontend_path = &config.frontend_path;
let frontend_index =
ServeFile::new(format!("{frontend_path}/index.html"));
let serve_frontend = ServeDir::new(frontend_path)
.not_found_service(frontend_index.clone());
Router::new()
.route("/version", get(|| async { env!("CARGO_PKG_VERSION") }))
.nest("/auth", auth::router())
.nest("/user", user::router())
.nest("/read", read::router())
.nest("/write", write::router())
.nest("/execute", execute::router())
.nest("/terminal", terminal::router())
.nest("/listener", listener::router())
.nest("/ws", ws::router())
.nest("/client", ts_client::router())
.fallback_service(serve_frontend)
.layer(cors_layer())
.layer(SetResponseHeaderLayer::overriding(
HeaderName::from_static("x-content-type-options"),
HeaderValue::from_static("nosniff"),
))
.layer(SetResponseHeaderLayer::overriding(
HeaderName::from_static("x-frame-options"),
HeaderValue::from_static("DENY"),
))
.layer(SetResponseHeaderLayer::overriding(
HeaderName::from_static("x-xss-protection"),
HeaderValue::from_static("1; mode=block"),
))
.layer(SetResponseHeaderLayer::overriding(
HeaderName::from_static("referrer-policy"),
HeaderValue::from_static("strict-origin-when-cross-origin"),
))
}
+54 -31
View File
@@ -49,6 +49,7 @@ mod repo;
mod schedule;
mod server;
mod stack;
mod swarm;
mod sync;
mod tag;
mod terminal;
@@ -75,36 +76,27 @@ enum ReadRequest {
ListGitProvidersFromConfig(ListGitProvidersFromConfig),
ListDockerRegistriesFromConfig(ListDockerRegistriesFromConfig),
// ==== USER ====
GetUsername(GetUsername),
GetPermission(GetPermission),
FindUser(FindUser),
ListUsers(ListUsers),
ListApiKeys(ListApiKeys),
ListApiKeysForServiceUser(ListApiKeysForServiceUser),
ListPermissions(ListPermissions),
ListUserTargetPermissions(ListUserTargetPermissions),
// ==== USER GROUP ====
GetUserGroup(GetUserGroup),
ListUserGroups(ListUserGroups),
// ==== PROCEDURE ====
GetProceduresSummary(GetProceduresSummary),
GetProcedure(GetProcedure),
GetProcedureActionState(GetProcedureActionState),
ListProcedures(ListProcedures),
ListFullProcedures(ListFullProcedures),
// ==== ACTION ====
GetActionsSummary(GetActionsSummary),
GetAction(GetAction),
GetActionActionState(GetActionActionState),
ListActions(ListActions),
ListFullActions(ListFullActions),
// ==== SCHEDULE ====
ListSchedules(ListSchedules),
// ==== SWARM ====
GetSwarmsSummary(GetSwarmsSummary),
GetSwarm(GetSwarm),
GetSwarmActionState(GetSwarmActionState),
ListSwarms(ListSwarms),
InspectSwarm(InspectSwarm),
ListFullSwarms(ListFullSwarms),
ListSwarmNodes(ListSwarmNodes),
InspectSwarmNode(InspectSwarmNode),
ListSwarmConfigs(ListSwarmConfigs),
InspectSwarmConfig(InspectSwarmConfig),
ListSwarmSecrets(ListSwarmSecrets),
InspectSwarmSecret(InspectSwarmSecret),
ListSwarmStacks(ListSwarmStacks),
InspectSwarmStack(InspectSwarmStack),
ListSwarmTasks(ListSwarmTasks),
InspectSwarmTask(InspectSwarmTask),
ListSwarmServices(ListSwarmServices),
InspectSwarmService(InspectSwarmService),
GetSwarmServiceLog(GetSwarmServiceLog),
SearchSwarmServiceLog(SearchSwarmServiceLog),
// ==== SERVER ====
GetServersSummary(GetServersSummary),
@@ -112,7 +104,6 @@ enum ReadRequest {
GetServerState(GetServerState),
GetPeripheryInformation(GetPeripheryInformation),
GetServerActionState(GetServerActionState),
GetHistoricalServerStats(GetHistoricalServerStats),
ListServers(ListServers),
ListFullServers(ListFullServers),
@@ -139,6 +130,7 @@ enum ReadRequest {
// ==== SERVER STATS ====
GetSystemInformation(GetSystemInformation),
GetSystemStats(GetSystemStats),
GetHistoricalServerStats(GetHistoricalServerStats),
ListSystemProcesses(ListSystemProcesses),
// ==== STACK ====
@@ -184,6 +176,23 @@ enum ReadRequest {
ListRepos(ListRepos),
ListFullRepos(ListFullRepos),
// ==== PROCEDURE ====
GetProceduresSummary(GetProceduresSummary),
GetProcedure(GetProcedure),
GetProcedureActionState(GetProcedureActionState),
ListProcedures(ListProcedures),
ListFullProcedures(ListFullProcedures),
// ==== ACTION ====
GetActionsSummary(GetActionsSummary),
GetAction(GetAction),
GetActionActionState(GetActionActionState),
ListActions(ListActions),
ListFullActions(ListFullActions),
// ==== SCHEDULE ====
ListSchedules(ListSchedules),
// ==== SYNC ====
GetResourceSyncsSummary(GetResourceSyncsSummary),
GetResourceSync(GetResourceSync),
@@ -211,6 +220,20 @@ enum ReadRequest {
GetTag(GetTag),
ListTags(ListTags),
// ==== USER ====
GetUsername(GetUsername),
GetPermission(GetPermission),
FindUser(FindUser),
ListUsers(ListUsers),
ListApiKeys(ListApiKeys),
ListApiKeysForServiceUser(ListApiKeysForServiceUser),
ListPermissions(ListPermissions),
ListUserTargetPermissions(ListUserTargetPermissions),
// ==== USER GROUP ====
GetUserGroup(GetUserGroup),
ListUserGroups(ListUserGroups),
// ==== UPDATE ====
GetUpdate(GetUpdate),
ListUpdates(ListUpdates),
+15 -14
View File
@@ -383,8 +383,8 @@ impl Resolve<ReadArgs> for ListDockerContainers {
let cache = server_status_cache()
.get_or_insert_default(&server.id)
.await;
if let Some(containers) = &cache.containers {
Ok(containers.clone())
if let Some(docker) = &cache.docker {
Ok(docker.containers.clone())
} else {
Ok(Vec::new())
}
@@ -410,10 +410,11 @@ impl Resolve<ReadArgs> for ListAllDockerContainers {
let cache = server_status_cache()
.get_or_insert_default(&server.id)
.await;
let Some(more) = &cache.containers else {
let Some(docker) = &cache.docker else {
continue;
};
let more = more
let more = docker
.containers
.iter()
.filter(|container| {
self.containers.is_empty()
@@ -448,8 +449,8 @@ impl Resolve<ReadArgs> for GetDockerContainersSummary {
.get_or_insert_default(&server.id)
.await;
if let Some(containers) = &cache.containers {
for container in containers {
if let Some(docker) = &cache.docker {
for container in &docker.containers {
res.total += 1;
match container.state {
ContainerStateStatusEnum::Created
@@ -641,8 +642,8 @@ impl Resolve<ReadArgs> for ListDockerNetworks {
let cache = server_status_cache()
.get_or_insert_default(&server.id)
.await;
if let Some(networks) = &cache.networks {
Ok(networks.clone())
if let Some(docker) = &cache.docker {
Ok(docker.networks.clone())
} else {
Ok(Vec::new())
}
@@ -694,8 +695,8 @@ impl Resolve<ReadArgs> for ListDockerImages {
let cache = server_status_cache()
.get_or_insert_default(&server.id)
.await;
if let Some(images) = &cache.images {
Ok(images.clone())
if let Some(docker) = &cache.docker {
Ok(docker.images.clone())
} else {
Ok(Vec::new())
}
@@ -775,8 +776,8 @@ impl Resolve<ReadArgs> for ListDockerVolumes {
let cache = server_status_cache()
.get_or_insert_default(&server.id)
.await;
if let Some(volumes) = &cache.volumes {
Ok(volumes.clone())
if let Some(docker) = &cache.docker {
Ok(docker.volumes.clone())
} else {
Ok(Vec::new())
}
@@ -825,8 +826,8 @@ impl Resolve<ReadArgs> for ListComposeProjects {
let cache = server_status_cache()
.get_or_insert_default(&server.id)
.await;
if let Some(projects) = &cache.projects {
Ok(projects.clone())
if let Some(docker) = &cache.docker {
Ok(docker.projects.clone())
} else {
Ok(Vec::new())
}
+478
View File
@@ -0,0 +1,478 @@
use anyhow::Context;
use komodo_client::{
api::read::*,
entities::{
permission::PermissionLevel,
swarm::{Swarm, SwarmActionState, SwarmListItem, SwarmState},
},
};
use resolver_api::Resolve;
use crate::{
helpers::{query::get_all_tags, swarm::swarm_request},
permission::get_check_permissions,
resource,
state::{action_states, swarm_status_cache},
};
use super::ReadArgs;
impl Resolve<ReadArgs> for GetSwarm {
async fn resolve(
self,
ReadArgs { user }: &ReadArgs,
) -> serror::Result<Swarm> {
Ok(
get_check_permissions::<Swarm>(
&self.swarm,
user,
PermissionLevel::Read.into(),
)
.await?,
)
}
}
impl Resolve<ReadArgs> for ListSwarms {
async fn resolve(
self,
ReadArgs { user }: &ReadArgs,
) -> serror::Result<Vec<SwarmListItem>> {
let all_tags = if self.query.tags.is_empty() {
vec![]
} else {
get_all_tags(None).await?
};
Ok(
resource::list_for_user::<Swarm>(
self.query,
user,
PermissionLevel::Read.into(),
&all_tags,
)
.await?,
)
}
}
impl Resolve<ReadArgs> for ListFullSwarms {
async fn resolve(
self,
ReadArgs { user }: &ReadArgs,
) -> serror::Result<ListFullSwarmsResponse> {
let all_tags = if self.query.tags.is_empty() {
vec![]
} else {
get_all_tags(None).await?
};
Ok(
resource::list_full_for_user::<Swarm>(
self.query,
user,
PermissionLevel::Read.into(),
&all_tags,
)
.await?,
)
}
}
impl Resolve<ReadArgs> for GetSwarmActionState {
async fn resolve(
self,
ReadArgs { user }: &ReadArgs,
) -> serror::Result<SwarmActionState> {
let swarm = get_check_permissions::<Swarm>(
&self.swarm,
user,
PermissionLevel::Read.into(),
)
.await?;
let action_state = action_states()
.swarm
.get(&swarm.id)
.await
.unwrap_or_default()
.get()?;
Ok(action_state)
}
}
impl Resolve<ReadArgs> for GetSwarmsSummary {
async fn resolve(
self,
ReadArgs { user }: &ReadArgs,
) -> serror::Result<GetSwarmsSummaryResponse> {
let swarms = resource::list_full_for_user::<Swarm>(
Default::default(),
user,
PermissionLevel::Read.into(),
&[],
)
.await
.context("failed to get swarms from db")?;
let mut res = GetSwarmsSummaryResponse::default();
let cache = swarm_status_cache();
for swarm in swarms {
res.total += 1;
match cache
.get(&swarm.id)
.await
.map(|status| status.state)
.unwrap_or_default()
{
SwarmState::Unknown => {
res.unknown += 1;
}
SwarmState::Healthy => {
res.healthy += 1;
}
SwarmState::Unhealthy => {
res.unhealthy += 1;
}
}
}
Ok(res)
}
}
impl Resolve<ReadArgs> for InspectSwarm {
async fn resolve(
self,
ReadArgs { user }: &ReadArgs,
) -> serror::Result<InspectSwarmResponse> {
let swarm = get_check_permissions::<Swarm>(
&self.swarm,
user,
PermissionLevel::Read.into(),
)
.await?;
let cache =
swarm_status_cache().get_or_insert_default(&swarm.id).await;
let inspect = cache
.inspect
.as_ref()
.cloned()
.context("SwarmInspectInfo not available")?;
Ok(inspect)
}
}
impl Resolve<ReadArgs> for ListSwarmNodes {
async fn resolve(
self,
ReadArgs { user }: &ReadArgs,
) -> serror::Result<ListSwarmNodesResponse> {
let swarm = get_check_permissions::<Swarm>(
&self.swarm,
user,
PermissionLevel::Read.into(),
)
.await?;
let cache =
swarm_status_cache().get_or_insert_default(&swarm.id).await;
if let Some(lists) = &cache.lists {
Ok(lists.nodes.clone())
} else {
Ok(Vec::new())
}
}
}
impl Resolve<ReadArgs> for InspectSwarmNode {
async fn resolve(
self,
ReadArgs { user }: &ReadArgs,
) -> serror::Result<InspectSwarmNodeResponse> {
let swarm = get_check_permissions::<Swarm>(
&self.swarm,
user,
PermissionLevel::Read.into(),
)
.await?;
swarm_request(
&swarm.config.server_ids,
periphery_client::api::swarm::InspectSwarmNode {
node: self.node,
},
)
.await
.map_err(Into::into)
}
}
impl Resolve<ReadArgs> for ListSwarmServices {
async fn resolve(
self,
ReadArgs { user }: &ReadArgs,
) -> serror::Result<ListSwarmServicesResponse> {
let swarm = get_check_permissions::<Swarm>(
&self.swarm,
user,
PermissionLevel::Read.into(),
)
.await?;
let cache =
swarm_status_cache().get_or_insert_default(&swarm.id).await;
if let Some(lists) = &cache.lists {
Ok(lists.services.clone())
} else {
Ok(Vec::new())
}
}
}
impl Resolve<ReadArgs> for InspectSwarmService {
async fn resolve(
self,
ReadArgs { user }: &ReadArgs,
) -> serror::Result<InspectSwarmServiceResponse> {
let swarm = get_check_permissions::<Swarm>(
&self.swarm,
user,
PermissionLevel::Read.into(),
)
.await?;
swarm_request(
&swarm.config.server_ids,
periphery_client::api::swarm::InspectSwarmService {
service: self.service,
},
)
.await
.map_err(Into::into)
}
}
impl Resolve<ReadArgs> for GetSwarmServiceLog {
async fn resolve(
self,
ReadArgs { user }: &ReadArgs,
) -> serror::Result<GetSwarmServiceLogResponse> {
let swarm = get_check_permissions::<Swarm>(
&self.swarm,
user,
PermissionLevel::Read.logs(),
)
.await?;
swarm_request(
&swarm.config.server_ids,
periphery_client::api::swarm::GetSwarmServiceLog {
service: self.service,
tail: self.tail,
timestamps: self.timestamps,
no_task_ids: self.no_task_ids,
no_resolve: self.no_resolve,
details: self.details,
},
)
.await
.map_err(Into::into)
}
}
impl Resolve<ReadArgs> for SearchSwarmServiceLog {
async fn resolve(
self,
ReadArgs { user }: &ReadArgs,
) -> serror::Result<SearchSwarmServiceLogResponse> {
let swarm = get_check_permissions::<Swarm>(
&self.swarm,
user,
PermissionLevel::Read.logs(),
)
.await?;
swarm_request(
&swarm.config.server_ids,
periphery_client::api::swarm::GetSwarmServiceLogSearch {
service: self.service,
terms: self.terms,
combinator: self.combinator,
invert: self.invert,
timestamps: self.timestamps,
no_task_ids: self.no_task_ids,
no_resolve: self.no_resolve,
details: self.details,
},
)
.await
.map_err(Into::into)
}
}
impl Resolve<ReadArgs> for ListSwarmTasks {
async fn resolve(
self,
ReadArgs { user }: &ReadArgs,
) -> serror::Result<ListSwarmTasksResponse> {
let swarm = get_check_permissions::<Swarm>(
&self.swarm,
user,
PermissionLevel::Read.into(),
)
.await?;
let cache =
swarm_status_cache().get_or_insert_default(&swarm.id).await;
if let Some(lists) = &cache.lists {
Ok(lists.tasks.clone())
} else {
Ok(Vec::new())
}
}
}
impl Resolve<ReadArgs> for InspectSwarmTask {
async fn resolve(
self,
ReadArgs { user }: &ReadArgs,
) -> serror::Result<InspectSwarmTaskResponse> {
let swarm = get_check_permissions::<Swarm>(
&self.swarm,
user,
PermissionLevel::Read.into(),
)
.await?;
swarm_request(
&swarm.config.server_ids,
periphery_client::api::swarm::InspectSwarmTask {
task: self.task,
},
)
.await
.map_err(Into::into)
}
}
impl Resolve<ReadArgs> for ListSwarmSecrets {
async fn resolve(
self,
ReadArgs { user }: &ReadArgs,
) -> serror::Result<ListSwarmSecretsResponse> {
let swarm = get_check_permissions::<Swarm>(
&self.swarm,
user,
PermissionLevel::Read.into(),
)
.await?;
let cache =
swarm_status_cache().get_or_insert_default(&swarm.id).await;
if let Some(lists) = &cache.lists {
Ok(lists.secrets.clone())
} else {
Ok(Vec::new())
}
}
}
impl Resolve<ReadArgs> for InspectSwarmSecret {
async fn resolve(
self,
ReadArgs { user }: &ReadArgs,
) -> serror::Result<InspectSwarmSecretResponse> {
let swarm = get_check_permissions::<Swarm>(
&self.swarm,
user,
PermissionLevel::Read.into(),
)
.await?;
swarm_request(
&swarm.config.server_ids,
periphery_client::api::swarm::InspectSwarmSecret {
secret: self.secret,
},
)
.await
.map_err(Into::into)
}
}
impl Resolve<ReadArgs> for ListSwarmConfigs {
async fn resolve(
self,
ReadArgs { user }: &ReadArgs,
) -> serror::Result<ListSwarmConfigsResponse> {
let swarm = get_check_permissions::<Swarm>(
&self.swarm,
user,
PermissionLevel::Read.into(),
)
.await?;
let cache =
swarm_status_cache().get_or_insert_default(&swarm.id).await;
if let Some(lists) = &cache.lists {
Ok(lists.configs.clone())
} else {
Ok(Vec::new())
}
}
}
impl Resolve<ReadArgs> for InspectSwarmConfig {
async fn resolve(
self,
ReadArgs { user }: &ReadArgs,
) -> serror::Result<InspectSwarmConfigResponse> {
let swarm = get_check_permissions::<Swarm>(
&self.swarm,
user,
PermissionLevel::Read.into(),
)
.await?;
swarm_request(
&swarm.config.server_ids,
periphery_client::api::swarm::InspectSwarmConfig {
config: self.config,
},
)
.await
.map_err(Into::into)
}
}
impl Resolve<ReadArgs> for ListSwarmStacks {
async fn resolve(
self,
ReadArgs { user }: &ReadArgs,
) -> serror::Result<ListSwarmStacksResponse> {
let swarm = get_check_permissions::<Swarm>(
&self.swarm,
user,
PermissionLevel::Read.into(),
)
.await?;
let cache =
swarm_status_cache().get_or_insert_default(&swarm.id).await;
if let Some(lists) = &cache.lists {
Ok(lists.stacks.clone())
} else {
Ok(Vec::new())
}
}
}
impl Resolve<ReadArgs> for InspectSwarmStack {
async fn resolve(
self,
ReadArgs { user }: &ReadArgs,
) -> serror::Result<InspectSwarmStackResponse> {
let swarm = get_check_permissions::<Swarm>(
&self.swarm,
user,
PermissionLevel::Read.into(),
)
.await?;
swarm_request(
&swarm.config.server_ids,
periphery_client::api::swarm::InspectSwarmStack {
stack: self.stack,
},
)
.await
.map_err(Into::into)
}
}
+78 -62
View File
@@ -11,7 +11,8 @@ use komodo_client::{
builder::Builder, deployment::Deployment,
permission::PermissionLevel, procedure::Procedure, repo::Repo,
resource::ResourceQuery, server::Server, stack::Stack,
sync::ResourceSync, toml::ResourcesToml, user::User,
swarm::Swarm, sync::ResourceSync, toml::ResourcesToml,
user::User,
},
};
use resolver_api::Resolve;
@@ -207,42 +208,21 @@ impl Resolve<ReadArgs> for ExportResourcesToToml {
let ReadArgs { user } = args;
for target in targets {
match target {
ResourceTarget::Alerter(id) => {
let mut alerter = get_check_permissions::<Alerter>(
ResourceTarget::Swarm(id) => {
let mut swarm = get_check_permissions::<Swarm>(
&id,
user,
PermissionLevel::Read.into(),
)
.await?;
Alerter::replace_ids(&mut alerter);
res.alerters.push(convert_resource::<Alerter>(
alerter,
Swarm::replace_ids(&mut swarm);
res.swarms.push(convert_resource::<Swarm>(
swarm,
false,
vec![],
&id_to_tags,
))
}
ResourceTarget::ResourceSync(id) => {
let mut sync = get_check_permissions::<ResourceSync>(
&id,
user,
PermissionLevel::Read.into(),
)
.await?;
if sync.config.file_contents.is_empty()
&& (sync.config.files_on_host
|| !sync.config.repo.is_empty()
|| !sync.config.linked_repo.is_empty())
{
ResourceSync::replace_ids(&mut sync);
res.resource_syncs.push(convert_resource::<ResourceSync>(
sync,
false,
vec![],
&id_to_tags,
))
}
}
ResourceTarget::Server(id) => {
let mut server = get_check_permissions::<Server>(
&id,
@@ -258,31 +238,16 @@ impl Resolve<ReadArgs> for ExportResourcesToToml {
&id_to_tags,
))
}
ResourceTarget::Builder(id) => {
let mut builder = get_check_permissions::<Builder>(
ResourceTarget::Stack(id) => {
let mut stack = get_check_permissions::<Stack>(
&id,
user,
PermissionLevel::Read.into(),
)
.await?;
Builder::replace_ids(&mut builder);
res.builders.push(convert_resource::<Builder>(
builder,
false,
vec![],
&id_to_tags,
))
}
ResourceTarget::Build(id) => {
let mut build = get_check_permissions::<Build>(
&id,
user,
PermissionLevel::Read.into(),
)
.await?;
Build::replace_ids(&mut build);
res.builds.push(convert_resource::<Build>(
build,
Stack::replace_ids(&mut stack);
res.stacks.push(convert_resource::<Stack>(
stack,
false,
vec![],
&id_to_tags,
@@ -303,6 +268,21 @@ impl Resolve<ReadArgs> for ExportResourcesToToml {
&id_to_tags,
))
}
ResourceTarget::Build(id) => {
let mut build = get_check_permissions::<Build>(
&id,
user,
PermissionLevel::Read.into(),
)
.await?;
Build::replace_ids(&mut build);
res.builds.push(convert_resource::<Build>(
build,
false,
vec![],
&id_to_tags,
))
}
ResourceTarget::Repo(id) => {
let mut repo = get_check_permissions::<Repo>(
&id,
@@ -318,21 +298,6 @@ impl Resolve<ReadArgs> for ExportResourcesToToml {
&id_to_tags,
))
}
ResourceTarget::Stack(id) => {
let mut stack = get_check_permissions::<Stack>(
&id,
user,
PermissionLevel::Read.into(),
)
.await?;
Stack::replace_ids(&mut stack);
res.stacks.push(convert_resource::<Stack>(
stack,
false,
vec![],
&id_to_tags,
))
}
ResourceTarget::Procedure(id) => {
let mut procedure = get_check_permissions::<Procedure>(
&id,
@@ -363,6 +328,57 @@ impl Resolve<ReadArgs> for ExportResourcesToToml {
&id_to_tags,
));
}
ResourceTarget::ResourceSync(id) => {
let mut sync = get_check_permissions::<ResourceSync>(
&id,
user,
PermissionLevel::Read.into(),
)
.await?;
if sync.config.file_contents.is_empty()
&& (sync.config.files_on_host
|| !sync.config.repo.is_empty()
|| !sync.config.linked_repo.is_empty())
{
ResourceSync::replace_ids(&mut sync);
res.resource_syncs.push(convert_resource::<ResourceSync>(
sync,
false,
vec![],
&id_to_tags,
))
}
}
ResourceTarget::Builder(id) => {
let mut builder = get_check_permissions::<Builder>(
&id,
user,
PermissionLevel::Read.into(),
)
.await?;
Builder::replace_ids(&mut builder);
res.builders.push(convert_resource::<Builder>(
builder,
false,
vec![],
&id_to_tags,
))
}
ResourceTarget::Alerter(id) => {
let mut alerter = get_check_permissions::<Alerter>(
&id,
user,
PermissionLevel::Read.into(),
)
.await?;
Alerter::replace_ids(&mut alerter);
res.alerters.push(convert_resource::<Alerter>(
alerter,
false,
vec![],
&id_to_tags,
))
}
ResourceTarget::System(_) => continue,
};
}
+27 -18
View File
@@ -20,6 +20,7 @@ use komodo_client::{
repo::Repo,
server::Server,
stack::Stack,
swarm::Swarm,
sync::ResourceSync,
update::{Update, UpdateListItem},
user::User,
@@ -266,6 +267,14 @@ impl Resolve<ReadArgs> for GetUpdate {
anyhow!("user must be admin to view system updates").into(),
);
}
ResourceTarget::Swarm(id) => {
get_check_permissions::<Swarm>(
id,
user,
PermissionLevel::Read.into(),
)
.await?;
}
ResourceTarget::Server(id) => {
get_check_permissions::<Server>(
id,
@@ -274,6 +283,14 @@ impl Resolve<ReadArgs> for GetUpdate {
)
.await?;
}
ResourceTarget::Stack(id) => {
get_check_permissions::<Stack>(
id,
user,
PermissionLevel::Read.into(),
)
.await?;
}
ResourceTarget::Deployment(id) => {
get_check_permissions::<Deployment>(
id,
@@ -298,22 +315,6 @@ impl Resolve<ReadArgs> for GetUpdate {
)
.await?;
}
ResourceTarget::Builder(id) => {
get_check_permissions::<Builder>(
id,
user,
PermissionLevel::Read.into(),
)
.await?;
}
ResourceTarget::Alerter(id) => {
get_check_permissions::<Alerter>(
id,
user,
PermissionLevel::Read.into(),
)
.await?;
}
ResourceTarget::Procedure(id) => {
get_check_permissions::<Procedure>(
id,
@@ -338,8 +339,16 @@ impl Resolve<ReadArgs> for GetUpdate {
)
.await?;
}
ResourceTarget::Stack(id) => {
get_check_permissions::<Stack>(
ResourceTarget::Builder(id) => {
get_check_permissions::<Builder>(
id,
user,
PermissionLevel::Read.into(),
)
.await?;
}
ResourceTarget::Alerter(id) => {
get_check_permissions::<Alerter>(
id,
user,
PermissionLevel::Read.into(),
+32 -12
View File
@@ -14,13 +14,16 @@ use komodo_client::{
api::user::*,
entities::{api_key::ApiKey, komodo_timestamp, user::User},
};
use reqwest::StatusCode;
use resolver_api::Resolve;
use response::Response;
use serde::{Deserialize, Serialize};
use serde_json::json;
use serror::{AddStatusCode, AddStatusCodeError};
use typeshare::typeshare;
use uuid::Uuid;
use crate::helpers::validations::validate_api_key_name;
use crate::{
auth::auth_request, helpers::query::get_user, state::db_client,
};
@@ -94,6 +97,9 @@ impl Resolve<UserArgs> for PushRecentlyViewed {
let user = get_user(&user.id).await?;
let (resource_type, id) = self.resource.extract_variant_id();
let field = format!("recents.{resource_type}");
let update = match user.recents.get(&resource_type) {
Some(recents) => {
let mut recents = recents
@@ -101,13 +107,16 @@ impl Resolve<UserArgs> for PushRecentlyViewed {
.filter(|_id| !id.eq(*_id))
.take(RECENTLY_VIEWED_MAX - 1)
.collect::<VecDeque<_>>();
recents.push_front(id);
doc! { format!("recents.{resource_type}"): to_bson(&recents)? }
doc! { &field: to_bson(&recents)? }
}
None => {
doc! { format!("recents.{resource_type}"): [id] }
doc! { &field: [id] }
}
};
update_one_by_id(
&db_client().users,
&user.id,
@@ -115,9 +124,7 @@ impl Resolve<UserArgs> for PushRecentlyViewed {
None,
)
.await
.with_context(|| {
format!("failed to update recents.{resource_type}")
})?;
.with_context(|| format!("Failed to update user '{field}'"))?;
Ok(PushRecentlyViewedResponse {})
}
@@ -137,7 +144,8 @@ impl Resolve<UserArgs> for SetLastSeenUpdate {
None,
)
.await
.context("failed to update user last_update_view")?;
.context("Failed to update user 'last_update_view'")?;
Ok(SetLastSeenUpdateResponse {})
}
}
@@ -157,10 +165,13 @@ impl Resolve<UserArgs> for CreateApiKey {
) -> serror::Result<CreateApiKeyResponse> {
let user = get_user(&user.id).await?;
validate_api_key_name(&self.name)
.status_code(StatusCode::BAD_REQUEST)?;
let key = format!("K-{}", random_string(SECRET_LENGTH));
let secret = format!("S-{}", random_string(SECRET_LENGTH));
let secret_hash = bcrypt::hash(&secret, BCRYPT_COST)
.context("failed at hashing secret string")?;
.context("Failed at hashing secret string")?;
let api_key = ApiKey {
name: self.name,
@@ -170,11 +181,13 @@ impl Resolve<UserArgs> for CreateApiKey {
created_at: komodo_timestamp(),
expires: self.expires,
};
db_client()
.api_keys
.insert_one(api_key)
.await
.context("failed to create api key on db")?;
.context("Failed to create api key on database")?;
Ok(CreateApiKeyResponse { key, secret })
}
}
@@ -190,20 +203,27 @@ impl Resolve<UserArgs> for DeleteApiKey {
UserArgs { user }: &UserArgs,
) -> serror::Result<DeleteApiKeyResponse> {
let client = db_client();
let key = client
.api_keys
.find_one(doc! { "key": &self.key })
.await
.context("failed at db query")?
.context("no api key with key found")?;
.context("Failed at database query")?
.context("No api key with key found")?;
if user.id != key.user_id {
return Err(anyhow!("api key does not belong to user").into());
return Err(
anyhow!("Api key does not belong to user")
.status_code(StatusCode::FORBIDDEN),
);
}
client
.api_keys
.delete_one(doc! { "key": key.key })
.await
.context("failed to delete api key from db")?;
.context("Failed to delete api key from database")?;
Ok(DeleteApiKeyResponse {})
}
}
+59 -51
View File
@@ -32,6 +32,7 @@ mod resource;
mod server;
mod service_user;
mod stack;
mod swarm;
mod sync;
mod tag;
mod terminal;
@@ -53,36 +54,16 @@ pub struct WriteArgs {
#[error(serror::Error)]
#[serde(tag = "type", content = "params")]
pub enum WriteRequest {
// ==== USER ====
CreateLocalUser(CreateLocalUser),
UpdateUserUsername(UpdateUserUsername),
UpdateUserPassword(UpdateUserPassword),
DeleteUser(DeleteUser),
// ==== SERVICE USER ====
CreateServiceUser(CreateServiceUser),
UpdateServiceUserDescription(UpdateServiceUserDescription),
CreateApiKeyForServiceUser(CreateApiKeyForServiceUser),
DeleteApiKeyForServiceUser(DeleteApiKeyForServiceUser),
// ==== USER GROUP ====
CreateUserGroup(CreateUserGroup),
RenameUserGroup(RenameUserGroup),
DeleteUserGroup(DeleteUserGroup),
AddUserToUserGroup(AddUserToUserGroup),
RemoveUserFromUserGroup(RemoveUserFromUserGroup),
SetUsersInUserGroup(SetUsersInUserGroup),
SetEveryoneUserGroup(SetEveryoneUserGroup),
// ==== PERMISSIONS ====
UpdateUserAdmin(UpdateUserAdmin),
UpdateUserBasePermissions(UpdateUserBasePermissions),
UpdatePermissionOnResourceType(UpdatePermissionOnResourceType),
UpdatePermissionOnTarget(UpdatePermissionOnTarget),
// ==== RESOURCE ====
UpdateResourceMeta(UpdateResourceMeta),
// ==== SWARM ====
CreateSwarm(CreateSwarm),
CopySwarm(CopySwarm),
DeleteSwarm(DeleteSwarm),
UpdateSwarm(UpdateSwarm),
RenameSwarm(RenameSwarm),
// ==== SERVER ====
CreateServer(CreateServer),
CopyServer(CopyServer),
@@ -93,6 +74,12 @@ pub enum WriteRequest {
UpdateServerPublicKey(UpdateServerPublicKey),
RotateServerKeys(RotateServerKeys),
// ==== TERMINAL ====
CreateTerminal(CreateTerminal),
DeleteTerminal(DeleteTerminal),
DeleteAllTerminals(DeleteAllTerminals),
BatchDeleteAllTerminals(BatchDeleteAllTerminals),
// ==== STACK ====
CreateStack(CreateStack),
CopyStack(CopyStack),
@@ -119,13 +106,6 @@ pub enum WriteRequest {
WriteBuildFileContents(WriteBuildFileContents),
RefreshBuildCache(RefreshBuildCache),
// ==== BUILDER ====
CreateBuilder(CreateBuilder),
CopyBuilder(CopyBuilder),
DeleteBuilder(DeleteBuilder),
UpdateBuilder(UpdateBuilder),
RenameBuilder(RenameBuilder),
// ==== REPO ====
CreateRepo(CreateRepo),
CopyRepo(CopyRepo),
@@ -134,13 +114,6 @@ pub enum WriteRequest {
RenameRepo(RenameRepo),
RefreshRepoCache(RefreshRepoCache),
// ==== ALERTER ====
CreateAlerter(CreateAlerter),
CopyAlerter(CopyAlerter),
DeleteAlerter(DeleteAlerter),
UpdateAlerter(UpdateAlerter),
RenameAlerter(RenameAlerter),
// ==== PROCEDURE ====
CreateProcedure(CreateProcedure),
CopyProcedure(CopyProcedure),
@@ -165,11 +138,51 @@ pub enum WriteRequest {
CommitSync(CommitSync),
RefreshResourceSyncPending(RefreshResourceSyncPending),
// ==== TERMINAL ====
CreateTerminal(CreateTerminal),
DeleteTerminal(DeleteTerminal),
DeleteAllTerminals(DeleteAllTerminals),
BatchDeleteAllTerminals(BatchDeleteAllTerminals),
// ==== BUILDER ====
CreateBuilder(CreateBuilder),
CopyBuilder(CopyBuilder),
DeleteBuilder(DeleteBuilder),
UpdateBuilder(UpdateBuilder),
RenameBuilder(RenameBuilder),
// ==== ALERTER ====
CreateAlerter(CreateAlerter),
CopyAlerter(CopyAlerter),
DeleteAlerter(DeleteAlerter),
UpdateAlerter(UpdateAlerter),
RenameAlerter(RenameAlerter),
// ==== ONBOARDING KEY ====
CreateOnboardingKey(CreateOnboardingKey),
UpdateOnboardingKey(UpdateOnboardingKey),
DeleteOnboardingKey(DeleteOnboardingKey),
// ==== USER ====
CreateLocalUser(CreateLocalUser),
UpdateUserUsername(UpdateUserUsername),
UpdateUserPassword(UpdateUserPassword),
DeleteUser(DeleteUser),
// ==== SERVICE USER ====
CreateServiceUser(CreateServiceUser),
UpdateServiceUserDescription(UpdateServiceUserDescription),
CreateApiKeyForServiceUser(CreateApiKeyForServiceUser),
DeleteApiKeyForServiceUser(DeleteApiKeyForServiceUser),
// ==== USER GROUP ====
CreateUserGroup(CreateUserGroup),
RenameUserGroup(RenameUserGroup),
DeleteUserGroup(DeleteUserGroup),
AddUserToUserGroup(AddUserToUserGroup),
RemoveUserFromUserGroup(RemoveUserFromUserGroup),
SetUsersInUserGroup(SetUsersInUserGroup),
SetEveryoneUserGroup(SetEveryoneUserGroup),
// ==== PERMISSIONS ====
UpdateUserAdmin(UpdateUserAdmin),
UpdateUserBasePermissions(UpdateUserBasePermissions),
UpdatePermissionOnResourceType(UpdatePermissionOnResourceType),
UpdatePermissionOnTarget(UpdatePermissionOnTarget),
// ==== TAG ====
CreateTag(CreateTag),
@@ -192,11 +205,6 @@ pub enum WriteRequest {
UpdateDockerRegistryAccount(UpdateDockerRegistryAccount),
DeleteDockerRegistryAccount(DeleteDockerRegistryAccount),
// ==== ONBOARDING KEY ====
CreateOnboardingKey(CreateOnboardingKey),
UpdateOnboardingKey(UpdateOnboardingKey),
DeleteOnboardingKey(DeleteOnboardingKey),
// ==== ALERT ====
CloseAlert(CloseAlert),
}
+80 -66
View File
@@ -307,8 +307,8 @@ async fn extract_user_target_with_validation(
.users
.find_one(filter)
.await
.context("failed to query db for users")?
.context("no matching user found")?
.context("Failed to query db for users")?
.context("No matching user found")?
.id;
Ok((UserTargetVariant::User, id))
}
@@ -321,8 +321,8 @@ async fn extract_user_target_with_validation(
.user_groups
.find_one(filter)
.await
.context("failed to query db for user_groups")?
.context("no matching user_group found")?
.context("Failed to query db for user_groups")?
.context("No matching user_group found")?
.id;
Ok((UserTargetVariant::UserGroup, id))
}
@@ -338,47 +338,19 @@ async fn extract_resource_target_with_validation(
let res = resource_target.extract_variant_id();
Ok((res.0, res.1.clone()))
}
ResourceTarget::Build(ident) => {
ResourceTarget::Swarm(ident) => {
let filter = match ObjectId::from_str(ident) {
Ok(id) => doc! { "_id": id },
Err(_) => doc! { "name": ident },
};
let id = db_client()
.builds
.swarms
.find_one(filter)
.await
.context("failed to query db for builds")?
.context("no matching build found")?
.context("Failed to query db for swarms")?
.context("No matching server found")?
.id;
Ok((ResourceTargetVariant::Build, id))
}
ResourceTarget::Builder(ident) => {
let filter = match ObjectId::from_str(ident) {
Ok(id) => doc! { "_id": id },
Err(_) => doc! { "name": ident },
};
let id = db_client()
.builders
.find_one(filter)
.await
.context("failed to query db for builders")?
.context("no matching builder found")?
.id;
Ok((ResourceTargetVariant::Builder, id))
}
ResourceTarget::Deployment(ident) => {
let filter = match ObjectId::from_str(ident) {
Ok(id) => doc! { "_id": id },
Err(_) => doc! { "name": ident },
};
let id = db_client()
.deployments
.find_one(filter)
.await
.context("failed to query db for deployments")?
.context("no matching deployment found")?
.id;
Ok((ResourceTargetVariant::Deployment, id))
Ok((ResourceTargetVariant::Server, id))
}
ResourceTarget::Server(ident) => {
let filter = match ObjectId::from_str(ident) {
@@ -389,11 +361,53 @@ async fn extract_resource_target_with_validation(
.servers
.find_one(filter)
.await
.context("failed to query db for servers")?
.context("no matching server found")?
.context("Failed to query db for servers")?
.context("No matching server found")?
.id;
Ok((ResourceTargetVariant::Server, id))
}
ResourceTarget::Stack(ident) => {
let filter = match ObjectId::from_str(ident) {
Ok(id) => doc! { "_id": id },
Err(_) => doc! { "name": ident },
};
let id = db_client()
.stacks
.find_one(filter)
.await
.context("Failed to query db for stacks")?
.context("No matching stack found")?
.id;
Ok((ResourceTargetVariant::Stack, id))
}
ResourceTarget::Deployment(ident) => {
let filter = match ObjectId::from_str(ident) {
Ok(id) => doc! { "_id": id },
Err(_) => doc! { "name": ident },
};
let id = db_client()
.deployments
.find_one(filter)
.await
.context("Failed to query db for deployments")?
.context("No matching deployment found")?
.id;
Ok((ResourceTargetVariant::Deployment, id))
}
ResourceTarget::Build(ident) => {
let filter = match ObjectId::from_str(ident) {
Ok(id) => doc! { "_id": id },
Err(_) => doc! { "name": ident },
};
let id = db_client()
.builds
.find_one(filter)
.await
.context("Failed to query db for builds")?
.context("No matching build found")?
.id;
Ok((ResourceTargetVariant::Build, id))
}
ResourceTarget::Repo(ident) => {
let filter = match ObjectId::from_str(ident) {
Ok(id) => doc! { "_id": id },
@@ -403,25 +417,11 @@ async fn extract_resource_target_with_validation(
.repos
.find_one(filter)
.await
.context("failed to query db for repos")?
.context("no matching repo found")?
.context("Failed to query db for repos")?
.context("No matching repo found")?
.id;
Ok((ResourceTargetVariant::Repo, id))
}
ResourceTarget::Alerter(ident) => {
let filter = match ObjectId::from_str(ident) {
Ok(id) => doc! { "_id": id },
Err(_) => doc! { "name": ident },
};
let id = db_client()
.alerters
.find_one(filter)
.await
.context("failed to query db for alerters")?
.context("no matching alerter found")?
.id;
Ok((ResourceTargetVariant::Alerter, id))
}
ResourceTarget::Procedure(ident) => {
let filter = match ObjectId::from_str(ident) {
Ok(id) => doc! { "_id": id },
@@ -431,8 +431,8 @@ async fn extract_resource_target_with_validation(
.procedures
.find_one(filter)
.await
.context("failed to query db for procedures")?
.context("no matching procedure found")?
.context("Failed to query db for procedures")?
.context("No matching procedure found")?
.id;
Ok((ResourceTargetVariant::Procedure, id))
}
@@ -445,8 +445,8 @@ async fn extract_resource_target_with_validation(
.actions
.find_one(filter)
.await
.context("failed to query db for actions")?
.context("no matching action found")?
.context("Failed to query db for actions")?
.context("No matching action found")?
.id;
Ok((ResourceTargetVariant::Action, id))
}
@@ -459,24 +459,38 @@ async fn extract_resource_target_with_validation(
.resource_syncs
.find_one(filter)
.await
.context("failed to query db for resource syncs")?
.context("no matching resource sync found")?
.context("Failed to query db for resource syncs")?
.context("No matching resource sync found")?
.id;
Ok((ResourceTargetVariant::ResourceSync, id))
}
ResourceTarget::Stack(ident) => {
ResourceTarget::Builder(ident) => {
let filter = match ObjectId::from_str(ident) {
Ok(id) => doc! { "_id": id },
Err(_) => doc! { "name": ident },
};
let id = db_client()
.stacks
.builders
.find_one(filter)
.await
.context("failed to query db for stacks")?
.context("no matching stack found")?
.context("Failed to query db for builders")?
.context("No matching builder found")?
.id;
Ok((ResourceTargetVariant::Stack, id))
Ok((ResourceTargetVariant::Builder, id))
}
ResourceTarget::Alerter(ident) => {
let filter = match ObjectId::from_str(ident) {
Ok(id) => doc! { "_id": id },
Err(_) => doc! { "name": ident },
};
let id = db_client()
.alerters
.find_one(filter)
.await
.context("Failed to query db for alerters")?
.context("No matching alerter found")?
.id;
Ok((ResourceTargetVariant::Alerter, id))
}
}
}
+13 -9
View File
@@ -5,7 +5,8 @@ use komodo_client::{
entities::{
ResourceTarget, action::Action, alerter::Alerter, build::Build,
builder::Builder, deployment::Deployment, procedure::Procedure,
repo::Repo, server::Server, stack::Stack, sync::ResourceSync,
repo::Repo, server::Server, stack::Stack, swarm::Swarm,
sync::ResourceSync,
},
};
use reqwest::StatusCode;
@@ -45,9 +46,15 @@ impl Resolve<WriteArgs> for UpdateResourceMeta {
.status_code(StatusCode::BAD_REQUEST),
);
}
ResourceTarget::Swarm(id) => {
resource::update_meta::<Swarm>(&id, meta, args).await?;
}
ResourceTarget::Server(id) => {
resource::update_meta::<Server>(&id, meta, args).await?;
}
ResourceTarget::Stack(id) => {
resource::update_meta::<Stack>(&id, meta, args).await?;
}
ResourceTarget::Deployment(id) => {
resource::update_meta::<Deployment>(&id, meta, args).await?;
}
@@ -57,12 +64,6 @@ impl Resolve<WriteArgs> for UpdateResourceMeta {
ResourceTarget::Repo(id) => {
resource::update_meta::<Repo>(&id, meta, args).await?;
}
ResourceTarget::Builder(id) => {
resource::update_meta::<Builder>(&id, meta, args).await?;
}
ResourceTarget::Alerter(id) => {
resource::update_meta::<Alerter>(&id, meta, args).await?;
}
ResourceTarget::Procedure(id) => {
resource::update_meta::<Procedure>(&id, meta, args).await?;
}
@@ -73,8 +74,11 @@ impl Resolve<WriteArgs> for UpdateResourceMeta {
resource::update_meta::<ResourceSync>(&id, meta, args)
.await?;
}
ResourceTarget::Stack(id) => {
resource::update_meta::<Stack>(&id, meta, args).await?;
ResourceTarget::Builder(id) => {
resource::update_meta::<Builder>(&id, meta, args).await?;
}
ResourceTarget::Alerter(id) => {
resource::update_meta::<Alerter>(&id, meta, args).await?;
}
}
Ok(UpdateResourceMetaResponse {})
+64 -23
View File
@@ -1,10 +1,5 @@
use std::str::FromStr;
use anyhow::{Context, anyhow};
use database::mungos::{
by_id::find_one_by_id,
mongodb::bson::{doc, oid::ObjectId},
};
use database::mungos::{by_id::find_one_by_id, mongodb::bson::doc};
use komodo_client::{
api::{user::CreateApiKey, write::*},
entities::{
@@ -12,9 +7,15 @@ use komodo_client::{
user::{User, UserConfig},
},
};
use reqwest::StatusCode;
use resolver_api::Resolve;
use serror::{AddStatusCode as _, AddStatusCodeError as _};
use crate::{api::user::UserArgs, state::db_client};
use crate::{
api::user::UserArgs,
helpers::validations::{validate_api_key_name, validate_username},
state::db_client,
};
use super::WriteArgs;
@@ -33,16 +34,19 @@ impl Resolve<WriteArgs> for CreateServiceUser {
WriteArgs { user }: &WriteArgs,
) -> serror::Result<CreateServiceUserResponse> {
if !user.admin {
return Err(anyhow!("user not admin").into());
}
if ObjectId::from_str(&self.username).is_ok() {
return Err(
anyhow!("username cannot be valid ObjectId").into(),
anyhow!("Only Admins can manage Service Users")
.status_code(StatusCode::FORBIDDEN),
);
}
validate_username(&self.username)
.status_code(StatusCode::BAD_REQUEST)?;
let config = UserConfig::Service {
description: self.description,
};
let mut user = User {
id: Default::default(),
username: self.username,
@@ -57,6 +61,7 @@ impl Resolve<WriteArgs> for CreateServiceUser {
all: Default::default(),
updated_at: komodo_timestamp(),
};
user.id = db_client()
.users
.insert_one(&user)
@@ -66,6 +71,7 @@ impl Resolve<WriteArgs> for CreateServiceUser {
.as_object_id()
.context("inserted id is not object id")?
.to_string();
Ok(user)
}
}
@@ -85,18 +91,28 @@ impl Resolve<WriteArgs> for UpdateServiceUserDescription {
WriteArgs { user }: &WriteArgs,
) -> serror::Result<UpdateServiceUserDescriptionResponse> {
if !user.admin {
return Err(anyhow!("user not admin").into());
return Err(
anyhow!("Only Admins can manage Service Users")
.status_code(StatusCode::FORBIDDEN),
);
}
let db = db_client();
let service_user = db
.users
.find_one(doc! { "username": &self.username })
.await
.context("failed to query db for user")?
.context("no user with given username")?;
.context("Failed to query db for user")?
.context("No user with given username")?;
let UserConfig::Service { .. } = &service_user.config else {
return Err(anyhow!("user is not service user").into());
return Err(
anyhow!("Target user is not Service User")
.status_code(StatusCode::FORBIDDEN),
);
};
db.users
.update_one(
doc! { "username": &self.username },
@@ -104,13 +120,15 @@ impl Resolve<WriteArgs> for UpdateServiceUserDescription {
)
.await
.context("failed to update user on db")?;
let res = db
let service_user = db
.users
.find_one(doc! { "username": &self.username })
.await
.context("failed to query db for user")?
.context("user with username not found")?;
Ok(res)
Ok(service_user)
}
}
@@ -130,16 +148,28 @@ impl Resolve<WriteArgs> for CreateApiKeyForServiceUser {
WriteArgs { user }: &WriteArgs,
) -> serror::Result<CreateApiKeyForServiceUserResponse> {
if !user.admin {
return Err(anyhow!("user not admin").into());
return Err(
anyhow!("Only Admins can manage Service Users")
.status_code(StatusCode::FORBIDDEN),
);
}
validate_api_key_name(&self.name)
.status_code(StatusCode::BAD_REQUEST)?;
let service_user =
find_one_by_id(&db_client().users, &self.user_id)
.await
.context("failed to query db for user")?
.context("no user found with id")?;
.context("Failed to query db for user")?
.context("No user found with id")?;
let UserConfig::Service { .. } = &service_user.config else {
return Err(anyhow!("user is not service user").into());
return Err(
anyhow!("Target user is not Service User")
.status_code(StatusCode::FORBIDDEN),
);
};
CreateApiKey {
name: self.name,
expires: self.expires,
@@ -163,23 +193,34 @@ impl Resolve<WriteArgs> for DeleteApiKeyForServiceUser {
WriteArgs { user }: &WriteArgs,
) -> serror::Result<DeleteApiKeyForServiceUserResponse> {
if !user.admin {
return Err(anyhow!("user not admin").into());
return Err(
anyhow!("Only Admins can manage Service Users")
.status_code(StatusCode::FORBIDDEN),
);
}
let db = db_client();
let api_key = db
.api_keys
.find_one(doc! { "key": &self.key })
.await
.context("failed to query db for api key")?
.context("did not find matching api key")?;
let service_user =
find_one_by_id(&db_client().users, &api_key.user_id)
.await
.context("failed to query db for user")?
.context("no user found with id")?;
let UserConfig::Service { .. } = &service_user.config else {
return Err(anyhow!("user is not service user").into());
return Err(
anyhow!("Target user is not Service User")
.status_code(StatusCode::FORBIDDEN),
);
};
db.api_keys
.delete_one(doc! { "key": self.key })
.await
+108
View File
@@ -0,0 +1,108 @@
use komodo_client::{
api::write::*,
entities::{
permission::PermissionLevel, swarm::Swarm, update::Update,
},
};
use resolver_api::Resolve;
use crate::{permission::get_check_permissions, resource};
use super::WriteArgs;
impl Resolve<WriteArgs> for CreateSwarm {
#[instrument(
"CreateSwarm",
skip_all,
fields(
operator = user.id,
swarm = self.name,
config = serde_json::to_string(&self.config).unwrap(),
)
)]
async fn resolve(
self,
WriteArgs { user }: &WriteArgs,
) -> serror::Result<Swarm> {
resource::create::<Swarm>(&self.name, self.config, None, user)
.await
}
}
impl Resolve<WriteArgs> for CopySwarm {
#[instrument(
"CopySwarm",
skip_all,
fields(
operator = user.id,
swarm = self.name,
copy_swarm = self.id,
)
)]
async fn resolve(
self,
WriteArgs { user }: &WriteArgs,
) -> serror::Result<Swarm> {
let Swarm { config, .. } = get_check_permissions::<Swarm>(
&self.id,
user,
PermissionLevel::Read.into(),
)
.await?;
resource::create::<Swarm>(&self.name, config.into(), None, user)
.await
}
}
impl Resolve<WriteArgs> for DeleteSwarm {
#[instrument(
"DeleteSwarm",
skip_all,
fields(
operator = user.id,
swarm = self.id,
)
)]
async fn resolve(
self,
WriteArgs { user }: &WriteArgs,
) -> serror::Result<Swarm> {
Ok(resource::delete::<Swarm>(&self.id, user).await?)
}
}
impl Resolve<WriteArgs> for UpdateSwarm {
#[instrument(
"UpdateSwarm",
skip_all,
fields(
operator = user.id,
swarm = self.id,
update = serde_json::to_string(&self.config).unwrap()
)
)]
async fn resolve(
self,
WriteArgs { user }: &WriteArgs,
) -> serror::Result<Swarm> {
Ok(resource::update::<Swarm>(&self.id, self.config, user).await?)
}
}
impl Resolve<WriteArgs> for RenameSwarm {
#[instrument(
"RenameSwarm",
skip_all,
fields(
operator = user.id,
swarm = self.id,
new_name = self.name
)
)]
async fn resolve(
self,
WriteArgs { user }: &WriteArgs,
) -> serror::Result<Update> {
Ok(resource::rename::<Swarm>(&self.id, &self.name, user).await?)
}
}
+30 -24
View File
@@ -15,9 +15,13 @@ use komodo_client::{
};
use reqwest::StatusCode;
use resolver_api::Resolve;
use serror::AddStatusCodeError;
use serror::{AddStatusCode as _, AddStatusCodeError};
use crate::{config::core_config, state::db_client};
use crate::{
config::core_config,
helpers::validations::{validate_password, validate_username},
state::db_client,
};
use super::WriteArgs;
@@ -38,24 +42,15 @@ impl Resolve<WriteArgs> for CreateLocalUser {
) -> serror::Result<CreateLocalUserResponse> {
if !admin.admin {
return Err(
anyhow!("This method is admin-only.")
anyhow!("This method is Admin Only.")
.status_code(StatusCode::FORBIDDEN),
);
}
if self.username.is_empty() {
return Err(anyhow!("Username cannot be empty.").into());
}
if ObjectId::from_str(&self.username).is_ok() {
return Err(
anyhow!("Username cannot be valid ObjectId").into(),
);
}
if self.password.is_empty() {
return Err(anyhow!("Password cannot be empty.").into());
}
validate_username(&self.username)
.status_code(StatusCode::BAD_REQUEST)?;
validate_password(&self.password)
.status_code(StatusCode::BAD_REQUEST)?;
let db = db_client();
@@ -130,17 +125,11 @@ impl Resolve<WriteArgs> for UpdateUserUsername {
);
}
}
if self.username.is_empty() {
return Err(anyhow!("Username cannot be empty.").into());
}
if ObjectId::from_str(&self.username).is_ok() {
return Err(
anyhow!("Username cannot be valid ObjectId").into(),
);
}
validate_username(&self.username)?;
let db = db_client();
if db
.users
.find_one(doc! { "username": &self.username })
@@ -150,8 +139,10 @@ impl Resolve<WriteArgs> for UpdateUserUsername {
{
return Err(anyhow!("Username already taken.").into());
}
let id = ObjectId::from_str(&user.id)
.context("User id not valid ObjectId.")?;
db.users
.update_one(
doc! { "_id": id },
@@ -159,6 +150,7 @@ impl Resolve<WriteArgs> for UpdateUserUsername {
)
.await
.context("Failed to update user username on database.")?;
Ok(NoData {})
}
}
@@ -185,7 +177,12 @@ impl Resolve<WriteArgs> for UpdateUserPassword {
);
}
}
validate_password(&self.password)
.status_code(StatusCode::BAD_REQUEST)?;
db_client().set_user_password(user, &self.password).await?;
Ok(NoData {})
}
}
@@ -211,15 +208,19 @@ impl Resolve<WriteArgs> for DeleteUser {
.status_code(StatusCode::FORBIDDEN),
);
}
if admin.username == self.user || admin.id == self.user {
return Err(anyhow!("User cannot delete themselves.").into());
}
let query = if let Ok(id) = ObjectId::from_str(&self.user) {
doc! { "_id": id }
} else {
doc! { "username": self.user }
};
let db = db_client();
let Some(user) = db
.users
.find_one(query.clone())
@@ -230,21 +231,25 @@ impl Resolve<WriteArgs> for DeleteUser {
anyhow!("No user found with given id / username").into(),
);
};
if user.super_admin {
return Err(
anyhow!("Cannot delete a super admin user.").into(),
);
}
if user.admin && !admin.super_admin {
return Err(
anyhow!("Only a Super Admin can delete an admin user.")
.into(),
);
}
db.users
.delete_one(query)
.await
.context("Failed to delete user from database")?;
// Also remove user id from all user groups
if let Err(e) = db
.user_groups
@@ -253,6 +258,7 @@ impl Resolve<WriteArgs> for DeleteUser {
{
warn!("Failed to remove deleted user from user groups | {e:?}");
};
Ok(user)
}
}
+28 -10
View File
@@ -6,12 +6,13 @@ use komodo_client::{
};
use reqwest::StatusCode;
use resolver_api::Resolve;
use serror::AddStatusCodeError;
use serror::{AddStatusCode as _, AddStatusCodeError};
use crate::{
helpers::{
query::get_variable,
update::{add_update, make_update},
validations::{validate_variable_name, validate_variable_value},
},
state::db_client,
};
@@ -35,7 +36,7 @@ impl Resolve<WriteArgs> for CreateVariable {
) -> serror::Result<CreateVariableResponse> {
if !user.admin {
return Err(
anyhow!("Only admins can create variables")
anyhow!("Only Admins can create Variables")
.status_code(StatusCode::FORBIDDEN),
);
}
@@ -47,6 +48,11 @@ impl Resolve<WriteArgs> for CreateVariable {
is_secret,
} = self;
validate_variable_name(&name)
.status_code(StatusCode::BAD_REQUEST)?;
validate_variable_value(&value)
.status_code(StatusCode::BAD_REQUEST)?;
let variable = Variable {
name,
value,
@@ -58,7 +64,7 @@ impl Resolve<WriteArgs> for CreateVariable {
.variables
.insert_one(&variable)
.await
.context("Failed to create variable on db")?;
.context("Failed to create Variable on db")?;
let mut update = make_update(
ResourceTarget::system(),
@@ -67,7 +73,8 @@ impl Resolve<WriteArgs> for CreateVariable {
);
update
.push_simple_log("create variable", format!("{variable:#?}"));
.push_simple_log("Create Variable", format!("{variable:#?}"));
update.finalize();
add_update(update).await?;
@@ -91,13 +98,18 @@ impl Resolve<WriteArgs> for UpdateVariableValue {
) -> serror::Result<UpdateVariableValueResponse> {
if !user.admin {
return Err(
anyhow!("Only admins can update variables")
anyhow!("Only Admins can update Variables")
.status_code(StatusCode::FORBIDDEN),
);
}
let UpdateVariableValue { name, value } = self;
validate_variable_name(&name)
.status_code(StatusCode::BAD_REQUEST)?;
validate_variable_value(&value)
.status_code(StatusCode::BAD_REQUEST)?;
let variable = get_variable(&name).await?;
if value == variable.value {
@@ -156,10 +168,11 @@ impl Resolve<WriteArgs> for UpdateVariableDescription {
) -> serror::Result<UpdateVariableDescriptionResponse> {
if !user.admin {
return Err(
anyhow!("Only admins can update variables")
anyhow!("Only Admins can update Variables")
.status_code(StatusCode::FORBIDDEN),
);
}
db_client()
.variables
.update_one(
@@ -168,6 +181,7 @@ impl Resolve<WriteArgs> for UpdateVariableDescription {
)
.await
.context("Failed to update variable description on db")?;
Ok(get_variable(&self.name).await?)
}
}
@@ -188,10 +202,11 @@ impl Resolve<WriteArgs> for UpdateVariableIsSecret {
) -> serror::Result<UpdateVariableIsSecretResponse> {
if !user.admin {
return Err(
anyhow!("Only admins can update variables")
anyhow!("Only Admins can update Variables")
.status_code(StatusCode::FORBIDDEN),
);
}
db_client()
.variables
.update_one(
@@ -199,7 +214,8 @@ impl Resolve<WriteArgs> for UpdateVariableIsSecret {
doc! { "$set": { "is_secret": self.is_secret } },
)
.await
.context("Failed to update variable is secret on db")?;
.context("Failed to update Variable 'is_secret' on db")?;
Ok(get_variable(&self.name).await?)
}
}
@@ -219,16 +235,18 @@ impl Resolve<WriteArgs> for DeleteVariable {
) -> serror::Result<DeleteVariableResponse> {
if !user.admin {
return Err(
anyhow!("Only admins can delete variables")
anyhow!("Only Admins can delete Variables")
.status_code(StatusCode::FORBIDDEN),
);
}
let variable = get_variable(&self.name).await?;
db_client()
.variables
.delete_one(doc! { "name": &self.name })
.await
.context("Failed to delete variable on db")?;
.context("Failed to delete Variable on db")?;
let mut update = make_update(
ResourceTarget::system(),
+101
View File
@@ -0,0 +1,101 @@
use std::net::IpAddr;
use crate::{
auth::{auth_api_key_check_enabled, auth_jwt_check_enabled},
helpers::query::get_user,
state::auth_rate_limiter,
};
use anyhow::{Context, anyhow};
use axum::{
Router,
extract::ws::{self, WebSocket},
http::HeaderMap,
routing::get,
};
use komodo_client::{entities::user::User, ws::WsLoginMessage};
use rate_limit::WithFailureRateLimit;
use reqwest::StatusCode;
use serror::{AddStatusCode, AddStatusCodeError};
mod terminal;
mod update;
pub fn router() -> Router {
Router::new()
// Periphery facing
.route("/periphery", get(crate::connection::server::handler))
// User facing
.route("/update", get(update::handler))
.route("/terminal", get(terminal::handler))
}
async fn user_ws_login(
mut socket: WebSocket,
headers: &HeaderMap,
fallback_ip: IpAddr,
) -> Option<(WebSocket, User)> {
let res = async {
let message = match socket
.recv()
.await
.context("Failed to receive message over socket: Closed")
.status_code(StatusCode::BAD_REQUEST)?
.context("Failed to recieve message over socket: Error")
.status_code(StatusCode::BAD_REQUEST)?
{
ws::Message::Text(utf8_bytes) => utf8_bytes.to_string(),
ws::Message::Binary(bytes) => String::from_utf8(bytes.into())
.context("Received invalid message bytes: Not UTF-8")
.status_code(StatusCode::BAD_REQUEST)?,
message => {
return Err(
anyhow!("Received invalid message: {message:?}")
.status_code(StatusCode::BAD_REQUEST),
);
}
};
match WsLoginMessage::from_json_str(&message)
.context("Invalid login message")
.status_code(StatusCode::BAD_REQUEST)?
{
WsLoginMessage::Jwt { jwt } => auth_jwt_check_enabled(&jwt)
.await
.status_code(StatusCode::UNAUTHORIZED),
WsLoginMessage::ApiKeys { key, secret } => {
auth_api_key_check_enabled(&key, &secret)
.await
.status_code(StatusCode::UNAUTHORIZED)
}
}
}
.with_failure_rate_limit_using_headers(
auth_rate_limiter(),
headers,
Some(fallback_ip),
)
.await;
match res {
Ok(user) => {
let _ = socket.send(ws::Message::text("LOGGED_IN")).await;
Some((socket, user))
}
Err(e) => {
let _ = socket
.send(ws::Message::text(format!(
"[{}]: {:#}",
e.status, e.error
)))
.await;
None
}
}
}
async fn check_user_valid(user_id: &str) -> anyhow::Result<User> {
let user = get_user(user_id).await?;
if !user.enabled {
return Err(anyhow!("User not enabled"));
}
Ok(user)
}
@@ -1,7 +1,9 @@
use std::net::SocketAddr;
use anyhow::anyhow;
use axum::{
extract::{FromRequestParts, WebSocketUpgrade, ws},
http::request,
extract::{ConnectInfo, FromRequestParts, WebSocketUpgrade, ws},
http::{HeaderMap, request},
response::IntoResponse,
};
use bytes::Bytes;
@@ -22,11 +24,14 @@ use crate::{
#[instrument("ConnectTerminal", skip(ws))]
pub async fn handler(
Qs(query): Qs<ConnectTerminalQuery>,
ConnectInfo(info): ConnectInfo<SocketAddr>,
headers: HeaderMap,
ws: WebSocketUpgrade,
) -> impl IntoResponse {
ws.on_upgrade(|socket| async move {
let ip = info.ip();
ws.on_upgrade(move |socket| async move {
let Some((mut client_socket, user)) =
super::user_ws_login(socket).await
super::user_ws_login(socket, &headers, ip).await
else {
return;
};
@@ -1,6 +1,9 @@
use std::net::SocketAddr;
use anyhow::anyhow;
use axum::{
extract::{WebSocketUpgrade, ws::Message},
extract::{ConnectInfo, WebSocketUpgrade, ws::Message},
http::HeaderMap,
response::IntoResponse,
};
use futures_util::{SinkExt, StreamExt};
@@ -16,17 +19,24 @@ use crate::helpers::{
channel::update_channel, query::get_user_permission_on_target,
};
pub async fn handler(ws: WebSocketUpgrade) -> impl IntoResponse {
pub async fn handler(
headers: HeaderMap,
ConnectInfo(info): ConnectInfo<SocketAddr>,
ws: WebSocketUpgrade,
) -> impl IntoResponse {
// get a reveiver for internal update messages.
let mut receiver = update_channel().receiver.resubscribe();
let ip = info.ip();
// handle http -> ws updgrade
ws.on_upgrade(|socket| async move {
let Some((socket, user)) = super::user_ws_login(socket).await else {
return
ws.on_upgrade(move |socket| async move {
let Some((client_socket, user)) =
super::user_ws_login(socket, &headers, ip).await
else {
return;
};
let (mut ws_sender, mut ws_reciever) = socket.split();
let (mut ws_sender, mut ws_reciever) = client_socket.split();
let cancel = CancellationToken::new();
let cancel_clone = cancel.clone();
+32 -14
View File
@@ -1,20 +1,28 @@
use std::net::SocketAddr;
use anyhow::{Context, anyhow};
use axum::{
Router, extract::Query, response::Redirect, routing::get,
Router,
extract::{ConnectInfo, Query},
http::HeaderMap,
response::Redirect,
routing::get,
};
use database::mongo_indexed::Document;
use database::mungos::mongodb::bson::doc;
use futures_util::TryFutureExt;
use komodo_client::entities::{
komodo_timestamp, random_string,
user::{User, UserConfig},
};
use rate_limit::WithFailureRateLimit;
use reqwest::StatusCode;
use serde::Deserialize;
use serror::AddStatusCode;
use serror::{AddStatusCode, AddStatusCodeError as _};
use crate::{
config::core_config,
state::{db_client, jwt_client},
state::{auth_rate_limiter, db_client, jwt_client},
};
use self::client::github_oauth_client;
@@ -28,21 +36,31 @@ pub fn router() -> Router {
.route(
"/login",
get(|Query(query): Query<RedirectQuery>| async {
Redirect::to(
&github_oauth_client()
.as_ref()
// OK: the router is only mounted in case that the client is populated
.unwrap()
.get_login_redirect_url(query.redirect)
.await,
)
let uri = github_oauth_client()
.as_ref()
.context("Github Oauth not configured")
.status_code(StatusCode::UNAUTHORIZED)?
.get_login_redirect_url(query.redirect)
.await;
serror::Result::Ok(Redirect::to(&uri))
}),
)
.route(
"/callback",
get(|query| async {
callback(query).await.status_code(StatusCode::UNAUTHORIZED)
}),
get(
|query,
headers: HeaderMap,
ConnectInfo(info): ConnectInfo<SocketAddr>| async move {
callback(query)
.map_err(|e| e.status_code(StatusCode::UNAUTHORIZED))
.with_failure_rate_limit_using_headers(
auth_rate_limiter(),
&headers,
Some(info.ip()),
)
.await
},
),
)
}
+32 -14
View File
@@ -1,21 +1,29 @@
use std::net::SocketAddr;
use anyhow::{Context, anyhow};
use async_timing_util::unix_timestamp_ms;
use axum::{
Router, extract::Query, response::Redirect, routing::get,
Router,
extract::{ConnectInfo, Query},
http::HeaderMap,
response::Redirect,
routing::get,
};
use database::mongo_indexed::Document;
use database::mungos::mongodb::bson::doc;
use futures_util::TryFutureExt;
use komodo_client::entities::{
random_string,
user::{User, UserConfig},
};
use rate_limit::WithFailureRateLimit;
use reqwest::StatusCode;
use serde::Deserialize;
use serror::AddStatusCode;
use serror::{AddStatusCode, AddStatusCodeError as _};
use crate::{
config::core_config,
state::{db_client, jwt_client},
state::{auth_rate_limiter, db_client, jwt_client},
};
use self::client::google_oauth_client;
@@ -29,21 +37,31 @@ pub fn router() -> Router {
.route(
"/login",
get(|Query(query): Query<RedirectQuery>| async move {
Redirect::to(
&google_oauth_client()
.as_ref()
// OK: its not mounted unless the client is populated
.unwrap()
.get_login_redirect_url(query.redirect)
.await,
)
let uri = google_oauth_client()
.as_ref()
.context("Google Oauth not configured")
.status_code(StatusCode::UNAUTHORIZED)?
.get_login_redirect_url(query.redirect)
.await;
serror::Result::Ok(Redirect::to(&uri))
}),
)
.route(
"/callback",
get(|query| async {
callback(query).await.status_code(StatusCode::UNAUTHORIZED)
}),
get(
|query,
headers: HeaderMap,
ConnectInfo(info): ConnectInfo<SocketAddr>| async move {
callback(query)
.map_err(|e| e.status_code(StatusCode::UNAUTHORIZED))
.with_failure_rate_limit_using_headers(
auth_rate_limiter(),
&headers,
Some(info.ip()),
)
.await
},
),
)
}
+19 -6
View File
@@ -12,9 +12,13 @@ use komodo_client::{
api::auth::JwtResponse,
entities::{config::core::CoreConfig, random_string},
};
use reqwest::StatusCode;
use serde::{Deserialize, Serialize};
use serror::{AddStatusCode as _, AddStatusCodeError as _};
use tokio::sync::Mutex;
use crate::auth::EXCHANGE_TOKEN_CLOCK_SKEW_TOLERANCE_MS;
type ExchangeTokenMap = Mutex<HashMap<String, (JwtResponse, u128)>>;
#[derive(Serialize, Deserialize, Clone)]
@@ -64,14 +68,14 @@ impl JwtClient {
exp,
};
let jwt = encode(&self.header, &claims, &self.encoding_key)
.context("failed at signing claim")?;
.context("Failed at signing claim")?;
Ok(JwtResponse { user_id, jwt })
}
pub fn decode(&self, jwt: &str) -> anyhow::Result<JwtClaims> {
decode::<JwtClaims>(jwt, &self.decoding_key, &self.validation)
.map(|res| res.claims)
.context("failed to decode token claims")
.context("Failed to decode token claims")
}
pub async fn create_exchange_token(
@@ -93,17 +97,26 @@ impl JwtClient {
pub async fn redeem_exchange_token(
&self,
exchange_token: &str,
) -> anyhow::Result<JwtResponse> {
) -> serror::Result<JwtResponse> {
let (jwt, valid_until) = self
.exchange_tokens
.lock()
.await
.remove(exchange_token)
.context("invalid exchange token: unrecognized")?;
if unix_timestamp_ms() < valid_until {
.context("Invalid exchange token")
.status_code(StatusCode::UNAUTHORIZED)?;
// Apply clock skew tolerance.
// Token is valid if expiration is greater than (now - tolerance)
if valid_until
> unix_timestamp_ms()
.saturating_sub(EXCHANGE_TOKEN_CLOCK_SKEW_TOLERANCE_MS)
{
Ok(jwt)
} else {
Err(anyhow!("invalid exchange token: expired"))
Err(
anyhow!("Invalid exchange token")
.status_code(StatusCode::UNAUTHORIZED),
)
}
}
}
+166 -112
View File
@@ -1,10 +1,10 @@
use std::str::FromStr;
use std::sync::{Arc, OnceLock};
use anyhow::{Context, anyhow};
use async_timing_util::unix_timestamp_ms;
use database::{
hash_password,
mungos::mongodb::bson::{Document, doc, oid::ObjectId},
mungos::mongodb::bson::{Document, doc},
};
use komodo_client::{
api::auth::{
@@ -13,136 +13,190 @@ use komodo_client::{
},
entities::user::{User, UserConfig},
};
use rate_limit::{RateLimiter, WithFailureRateLimit};
use reqwest::StatusCode;
use resolver_api::Resolve;
use serror::{AddStatusCode as _, AddStatusCodeError};
use crate::{
api::auth::AuthArgs,
config::core_config,
state::{db_client, jwt_client},
helpers::validations::{validate_password, validate_username},
state::{auth_rate_limiter, db_client, jwt_client},
};
impl Resolve<AuthArgs> for SignUpLocalUser {
#[instrument("SignUpLocalUser", skip(self))]
async fn resolve(
self,
_: &AuthArgs,
AuthArgs { headers, ip }: &AuthArgs,
) -> serror::Result<SignUpLocalUserResponse> {
let core_config = core_config();
if !core_config.local_auth {
return Err(anyhow!("Local auth is not enabled").into());
}
if self.username.is_empty() {
return Err(anyhow!("Username cannot be empty string").into());
}
if ObjectId::from_str(&self.username).is_ok() {
return Err(
anyhow!("Username cannot be valid ObjectId").into(),
);
}
if self.password.is_empty() {
return Err(anyhow!("Password cannot be empty string").into());
}
let db = db_client();
let no_users_exist =
db.users.find_one(Document::new()).await?.is_none();
if !no_users_exist && core_config.disable_user_registration {
return Err(anyhow!("User registration is disabled").into());
}
if db
.users
.find_one(doc! { "username": &self.username })
sign_up_local_user(self)
.with_failure_rate_limit_using_headers(
auth_rate_limiter(),
headers,
Some(*ip),
)
.await
.context("Failed to query for existing users")?
.is_some()
{
return Err(anyhow!("Username already taken.").into());
}
let ts = unix_timestamp_ms() as i64;
let hashed_password = hash_password(self.password)?;
let user = User {
id: Default::default(),
username: self.username,
enabled: no_users_exist || core_config.enable_new_users,
admin: no_users_exist,
super_admin: no_users_exist,
create_server_permissions: no_users_exist,
create_build_permissions: no_users_exist,
updated_at: ts,
last_update_view: 0,
recents: Default::default(),
all: Default::default(),
config: UserConfig::Local {
password: hashed_password,
},
};
let user_id = db_client()
.users
.insert_one(user)
.await
.context("failed to create user")?
.inserted_id
.as_object_id()
.context("inserted_id is not ObjectId")?
.to_string();
jwt_client()
.encode(user_id.clone())
.context("failed to generate jwt for user")
.map_err(Into::into)
}
}
async fn sign_up_local_user(
req: SignUpLocalUser,
) -> serror::Result<SignUpLocalUserResponse> {
let config = core_config();
if !config.local_auth {
return Err(anyhow!("Local auth is not enabled").into());
}
validate_username(&req.username)
.status_code(StatusCode::BAD_REQUEST)?;
validate_password(&req.password)
.status_code(StatusCode::BAD_REQUEST)?;
let db = db_client();
let no_users_exist =
db.users.find_one(Document::new()).await?.is_none();
if !no_users_exist && config.disable_user_registration {
return Err(
anyhow!("User registration is disabled")
.status_code(StatusCode::UNAUTHORIZED),
);
}
if db
.users
.find_one(doc! { "username": &req.username })
.await
.context("Failed to query for existing users")?
.is_some()
{
// When user registration is enabled, there is no way around allowing
// potential attackers to gain some insight about which usernames exist
// if they are allowed to register accounts. Since this can be easily inferred,
// might as well be clear. The auth rate limiter is critical here.
return Err(
anyhow!("Username already taken.")
.status_code(StatusCode::BAD_REQUEST),
);
}
let ts = unix_timestamp_ms() as i64;
let hashed_password = hash_password(req.password)?;
let user = User {
id: Default::default(),
username: req.username,
enabled: no_users_exist || config.enable_new_users,
admin: no_users_exist,
super_admin: no_users_exist,
create_server_permissions: no_users_exist,
create_build_permissions: no_users_exist,
updated_at: ts,
last_update_view: 0,
recents: Default::default(),
all: Default::default(),
config: UserConfig::Local {
password: hashed_password,
},
};
let user_id = db_client()
.users
.insert_one(user)
.await
.context("Failed to create user on database")?
.inserted_id
.as_object_id()
.context("The 'inserted_id' is not ObjectId")?
.to_string();
jwt_client()
.encode(user_id)
.context("Failed to generate JWT for user")
.map_err(Into::into)
}
/// Local login method has a dedicated rate limiter
/// so the UI background calls using existing JWT do
/// not influence the number of attempts user has
/// to log in.
fn login_local_user_rate_limiter() -> &'static RateLimiter {
static LOGIN_LOCAL_USER_RATE_LIMITER: OnceLock<Arc<RateLimiter>> =
OnceLock::new();
LOGIN_LOCAL_USER_RATE_LIMITER.get_or_init(|| {
let config = core_config();
RateLimiter::new(
config.auth_rate_limit_disabled,
config.auth_rate_limit_max_attempts as usize,
config.auth_rate_limit_window_seconds,
)
})
}
impl Resolve<AuthArgs> for LoginLocalUser {
async fn resolve(
self,
_: &AuthArgs,
AuthArgs { headers, ip }: &AuthArgs,
) -> serror::Result<LoginLocalUserResponse> {
if !core_config().local_auth {
return Err(anyhow!("local auth is not enabled").into());
}
let user = db_client()
.users
.find_one(doc! { "username": &self.username })
login_local_user(self)
.with_failure_rate_limit_using_headers(
login_local_user_rate_limiter(),
headers,
Some(*ip),
)
.await
.context("failed at db query for users")?
.with_context(|| {
format!("did not find user with username {}", self.username)
})?;
let UserConfig::Local {
password: user_pw_hash,
} = user.config
else {
return Err(
anyhow!(
"non-local auth users can not log in with a password"
)
.into(),
);
};
let verified = bcrypt::verify(self.password, &user_pw_hash)
.context("failed at verify password")?;
if !verified {
return Err(anyhow!("invalid credentials").into());
}
jwt_client()
.encode(user.id.clone())
.context("failed at generating jwt for user")
.map_err(Into::into)
}
}
async fn login_local_user(
req: LoginLocalUser,
) -> serror::Result<LoginLocalUserResponse> {
if !core_config().local_auth {
return Err(
anyhow!("Local auth is not enabled")
.status_code(StatusCode::UNAUTHORIZED),
);
}
validate_username(&req.username)
.status_code(StatusCode::BAD_REQUEST)?;
let user = db_client()
.users
.find_one(doc! { "username": &req.username })
.await
.context("Failed at db query for users")?
.context("Invalid login credentials")
.status_code(StatusCode::UNAUTHORIZED)?;
let UserConfig::Local {
password: user_pw_hash,
} = user.config
else {
return Err(
anyhow!("Invalid login credentials")
.status_code(StatusCode::UNAUTHORIZED),
);
};
let verified = bcrypt::verify(req.password, &user_pw_hash)
.context("Invalid login credentials")
.status_code(StatusCode::UNAUTHORIZED)?;
if !verified {
return Err(
anyhow!("Invalid login credentials")
.status_code(StatusCode::UNAUTHORIZED),
);
}
jwt_client()
.encode(user.id)
// This is in internal error (500), not auth error
.context("Failed to generate JWT for user")
.map_err(Into::into)
}
+61 -27
View File
@@ -1,18 +1,24 @@
use std::net::SocketAddr;
use anyhow::{Context, anyhow};
use async_timing_util::unix_timestamp_ms;
use axum::{
extract::Request, http::HeaderMap, middleware::Next,
extract::{ConnectInfo, Request},
http::HeaderMap,
middleware::Next,
response::Response,
};
use database::mungos::mongodb::bson::doc;
use futures_util::TryFutureExt;
use komodo_client::entities::{komodo_timestamp, user::User};
use rate_limit::WithFailureRateLimit;
use reqwest::StatusCode;
use serde::Deserialize;
use serror::AddStatusCode;
use serror::AddStatusCodeError as _;
use crate::{
helpers::query::get_user,
state::{db_client, jwt_client},
state::{auth_rate_limiter, db_client, jwt_client},
};
use self::jwt::JwtClaims;
@@ -24,7 +30,14 @@ pub mod oidc;
mod local;
/// Length of random token in Oauth / OIDC 'state'
const STATE_PREFIX_LENGTH: usize = 20;
/// JWT Clock skew tolerance in milliseconds (10 seconds for JWTs)
const JWT_CLOCK_SKEW_TOLERANCE_MS: u128 = 10 * 1000;
/// Exchange Token Clock skew tolerance in milliseconds (5 seconds for Exchange tokens)
const EXCHANGE_TOKEN_CLOCK_SKEW_TOLERANCE_MS: u128 = 5 * 1000;
/// Api Key Clock skew tolerance in milliseconds (5 minutes for Api Keys)
const API_KEY_CLOCK_SKEW_TOLERANCE_MS: i64 = 5 * 60 * 1000;
#[derive(Debug, Deserialize)]
struct RedirectQuery {
@@ -36,9 +49,18 @@ pub async fn auth_request(
mut req: Request,
next: Next,
) -> serror::Result<Response> {
let fallback = req
.extensions()
.get::<ConnectInfo<SocketAddr>>()
.map(|addr| addr.ip());
let user = authenticate_check_enabled(&headers)
.await
.status_code(StatusCode::UNAUTHORIZED)?;
.map_err(|e| e.status_code(StatusCode::UNAUTHORIZED))
.with_failure_rate_limit_using_headers(
auth_rate_limiter(),
&headers,
fallback,
)
.await?;
req.extensions_mut().insert(user);
Ok(next.run(req).await)
}
@@ -53,23 +75,21 @@ pub async fn get_user_id_from_headers(
) {
(Some(jwt), _, _) => {
// USE JWT
let jwt = jwt.to_str().context("jwt is not str")?;
auth_jwt_get_user_id(jwt)
.await
.context("failed to authenticate jwt")
let jwt = jwt.to_str().context("JWT is not valid UTF-8")?;
auth_jwt_get_user_id(jwt).await
}
(None, Some(key), Some(secret)) => {
// USE API KEY / SECRET
let key = key.to_str().context("key is not str")?;
let secret = secret.to_str().context("secret is not str")?;
auth_api_key_get_user_id(key, secret)
.await
.context("failed to authenticate api key")
let key =
key.to_str().context("X-API-KEY is not valid UTF-8")?;
let secret =
secret.to_str().context("X-API-SECRET is not valid UTF-8")?;
auth_api_key_get_user_id(key, secret).await
}
_ => {
// AUTH FAIL
Err(anyhow!(
"must attach either AUTHORIZATION header with jwt OR pass X-API-KEY and X-API-SECRET"
"Must attach either AUTHORIZATION header with jwt OR pass X-API-KEY and X-API-SECRET"
))
}
}
@@ -79,22 +99,30 @@ pub async fn authenticate_check_enabled(
headers: &HeaderMap,
) -> anyhow::Result<User> {
let user_id = get_user_id_from_headers(headers).await?;
let user = get_user(&user_id).await?;
let user = get_user(&user_id)
.await
.map_err(|_| anyhow!("Invalid user credentials"))?;
if user.enabled {
Ok(user)
} else {
Err(anyhow!("user not enabled"))
Err(anyhow!("Invalid user credentials"))
}
}
pub async fn auth_jwt_get_user_id(
jwt: &str,
) -> anyhow::Result<String> {
let claims: JwtClaims = jwt_client().decode(jwt)?;
if claims.exp > unix_timestamp_ms() {
let claims: JwtClaims = jwt_client()
.decode(jwt)
.map_err(|_| anyhow!("Invalid user credentials"))?;
// Apply clock skew tolerance.
// Token is valid if expiration is greater than (now - tolerance)
if claims.exp
> unix_timestamp_ms().saturating_sub(JWT_CLOCK_SKEW_TOLERANCE_MS)
{
Ok(claims.id)
} else {
Err(anyhow!("token has expired"))
Err(anyhow!("Invalid user credentials"))
}
}
@@ -113,19 +141,25 @@ pub async fn auth_api_key_get_user_id(
.api_keys
.find_one(doc! { "key": key })
.await
.context("failed to query db")?
.context("no api key matching key")?;
if key.expires != 0 && key.expires < komodo_timestamp() {
return Err(anyhow!("api key expired"));
.context("Failed to query db")?
.context("Invalid user credentials")?;
// Apply clock skew tolerance.
// Token is invalid if expiration is less than (now - tolerance)
if key.expires != 0
&& key.expires
< komodo_timestamp()
.saturating_sub(API_KEY_CLOCK_SKEW_TOLERANCE_MS)
{
return Err(anyhow!("Invalid user credentials"));
}
if bcrypt::verify(secret, &key.secret)
.context("failed to verify secret hash")?
.map_err(|_| anyhow!("Invalid user credentials"))?
{
// secret matches
Ok(key.user_id)
} else {
// secret mismatch
Err(anyhow!("invalid api secret"))
Err(anyhow!("Invalid user credentials"))
}
}
@@ -142,6 +176,6 @@ async fn check_enabled(user_id: String) -> anyhow::Result<User> {
if user.enabled {
Ok(user)
} else {
Err(anyhow!("user not enabled"))
Err(anyhow!("Invalid user credentials"))
}
}
+24 -7
View File
@@ -1,12 +1,17 @@
use std::sync::OnceLock;
use std::{net::SocketAddr, sync::OnceLock};
use anyhow::{Context, anyhow};
use axum::{
Router, extract::Query, response::Redirect, routing::get,
Router,
extract::{ConnectInfo, Query},
http::HeaderMap,
response::Redirect,
routing::get,
};
use client::oidc_client;
use dashmap::DashMap;
use database::mungos::mongodb::bson::{Document, doc};
use futures_util::TryFutureExt;
use komodo_client::entities::{
komodo_timestamp, random_string,
user::{User, UserConfig},
@@ -17,13 +22,14 @@ use openidconnect::{
PkceCodeChallenge, PkceCodeVerifier, Scope, TokenResponse,
core::{CoreAuthenticationFlow, CoreGenderClaim},
};
use rate_limit::WithFailureRateLimit;
use reqwest::StatusCode;
use serde::Deserialize;
use serror::AddStatusCode;
use serror::{AddStatusCode as _, AddStatusCodeError};
use crate::{
config::core_config,
state::{db_client, jwt_client},
state::{auth_rate_limiter, db_client, jwt_client},
};
use super::RedirectQuery;
@@ -68,9 +74,20 @@ pub fn router() -> Router {
)
.route(
"/callback",
get(|query| async {
callback(query).await.status_code(StatusCode::UNAUTHORIZED)
}),
get(
|query,
headers: HeaderMap,
ConnectInfo(info): ConnectInfo<SocketAddr>| async move {
callback(query)
.map_err(|e| e.status_code(StatusCode::UNAUTHORIZED))
.with_failure_rate_limit_using_headers(
auth_rate_limiter(),
&headers,
Some(info.ip()),
)
.await
},
),
)
}
+66
View File
@@ -1,6 +1,7 @@
use std::{path::PathBuf, sync::OnceLock};
use anyhow::Context;
use axum::http::HeaderValue;
use colored::Colorize;
use config::ConfigLoader;
use environment_file::{
@@ -14,6 +15,7 @@ use komodo_client::entities::{
logger::LogConfig,
};
use noise::key::{RotatableKeyPair, SpkiPublicKey};
use tower_http::cors::CorsLayer;
/// Should call in startup to ensure Core errors without valid private key.
pub fn core_keys() -> &'static RotatableKeyPair {
@@ -89,6 +91,52 @@ pub fn periphery_public_keys() -> Option<&'static [SpkiPublicKey]> {
.as_deref()
}
/// Creates a CORS layer based on the Core configuration.
///
/// - If `cors_allowed_origins` is empty: Allows all origins (backward compatibility)
/// - If `cors_allowed_origins` is set: Only allows the specified origins
/// - Methods and headers are always allowed (Any)
/// - Credentials are only allowed if `cors_allow_credentials` is true
pub fn cors_layer() -> CorsLayer {
let config = core_config();
let mut cors = CorsLayer::new()
.allow_methods(tower_http::cors::Any)
.allow_headers(tower_http::cors::Any)
.allow_credentials(config.cors_allow_credentials);
if config.cors_allowed_origins.is_empty() {
cors = cors.allow_origin(tower_http::cors::Any)
} else {
cors = cors.allow_origin(
config
.cors_allowed_origins
.iter()
.filter_map(|origin| {
HeaderValue::from_str(origin)
.inspect_err(|e| {
warn!("Invalid CORS allowed origin: {origin} | {e:?}")
})
.ok()
})
.collect::<Vec<_>>(),
);
};
cors
}
pub fn monitoring_interval() -> async_timing_util::Timelength {
static MONITORING_INTERVAL: OnceLock<
async_timing_util::Timelength,
> = OnceLock::new();
*MONITORING_INTERVAL.get_or_init(|| {
core_config().monitoring_interval.try_into().unwrap_or_else(
|_| {
error!("Invalid 'monitoring_interval', using default 15-sec");
async_timing_util::Timelength::FifteenSeconds
},
)
})
}
pub fn core_config() -> &'static CoreConfig {
static CORE_CONFIG: OnceLock<CoreConfig> = OnceLock::new();
CORE_CONFIG.get_or_init(|| {
@@ -281,6 +329,21 @@ pub fn core_config() -> &'static CoreConfig {
.komodo_frontend_path
.unwrap_or(config.frontend_path),
jwt_ttl: env.komodo_jwt_ttl.unwrap_or(config.jwt_ttl),
auth_rate_limit_disabled: env
.komodo_auth_rate_limit_disabled
.unwrap_or(config.auth_rate_limit_disabled),
auth_rate_limit_max_attempts: env
.komodo_auth_rate_limit_max_attempts
.unwrap_or(config.auth_rate_limit_max_attempts),
auth_rate_limit_window_seconds: env
.komodo_auth_rate_limit_window_seconds
.unwrap_or(config.auth_rate_limit_window_seconds),
cors_allowed_origins: env
.komodo_cors_allowed_origins
.unwrap_or(config.cors_allowed_origins),
cors_allow_credentials: env
.komodo_cors_allow_credentials
.unwrap_or(config.cors_allow_credentials),
sync_directory: env
.komodo_sync_directory
.unwrap_or(config.sync_directory),
@@ -336,6 +399,9 @@ pub fn core_config() -> &'static CoreConfig {
.komodo_lock_login_credentials_for
.unwrap_or(config.lock_login_credentials_for),
local_auth: env.komodo_local_auth.unwrap_or(config.local_auth),
min_password_length: env
.komodo_min_password_length
.unwrap_or(config.min_password_length),
logging: LogConfig {
level: env
.komodo_logging_level
+1 -6
View File
@@ -45,7 +45,6 @@ impl PeripheryConnectionArgs<'_> {
periphery_connections().insert(id.clone(), self).await;
let responses = connection.responses.clone();
let terminals = connection.terminals.clone();
tokio::spawn(async move {
loop {
@@ -91,11 +90,7 @@ impl PeripheryConnectionArgs<'_> {
}
});
Ok(PeripheryClient {
id,
responses,
terminals,
})
Ok(PeripheryClient { id, responses })
}
}
+65 -11
View File
@@ -1,3 +1,16 @@
//! # Action State Management
//!
//! This module provides thread-safe state management for resource exections.
//! It prevents concurrent execution of exections on the same resource using
//! a Mutex-based locking mechanism with RAII guards.
//!
//! ## Safety
//!
//! - Uses RAII pattern to ensure locks are always released
//! - Handles lock poisoning gracefully
//! - Prevents race conditions through per-resource locks
//! - No deadlock risk: each resource has independent locks
use std::sync::{Arc, Mutex};
use anyhow::anyhow;
@@ -9,12 +22,13 @@ use komodo_client::{
deployment::DeploymentActionState,
procedure::ProcedureActionState, repo::RepoActionState,
server::ServerActionState, stack::StackActionState,
sync::ResourceSyncActionState,
swarm::SwarmActionState, sync::ResourceSyncActionState,
},
};
#[derive(Default)]
pub struct ActionStates {
pub swarm: CloneCache<String, Arc<ActionState<SwarmActionState>>>,
pub server: CloneCache<String, Arc<ActionState<ServerActionState>>>,
pub stack: CloneCache<String, Arc<ActionState<StackActionState>>>,
pub deployment:
@@ -28,7 +42,16 @@ pub struct ActionStates {
CloneCache<String, Arc<ActionState<ResourceSyncActionState>>>,
}
/// Need to be able to check "busy" with write lock acquired.
/// Thread-safe state container for resource executions.
///
/// Uses a Mutex to prevent concurrent executions and provides
/// RAII-based locking through [UpdateGuard].
///
/// # Safety
///
/// - Each resource has its own ActionState instance
/// - State is reset to default when [UpdateGuard] is dropped
/// - Lock poisoning error handling is handled gracefully with anyhow::Error
#[derive(Default)]
pub struct ActionState<States: Default + Send + 'static>(
Mutex<States>,
@@ -42,7 +65,7 @@ impl<States: Default + Busy + Copy + Send + 'static>
*self
.0
.lock()
.map_err(|e| anyhow!("action state lock poisoned | {e:?}"))?,
.map_err(|e| anyhow!("Action state lock poisoned | {e:?}"))?,
)
}
@@ -51,14 +74,33 @@ impl<States: Default + Busy + Copy + Send + 'static>
self
.0
.lock()
.map_err(|e| anyhow!("action state lock poisoned | {e:?}"))?
.map_err(|e| anyhow!("Action state lock poisoned | {e:?}"))?
.busy(),
)
}
/// Will acquire lock, check busy, and if not will
/// run the provided update function on the states.
/// Returns a guard that returns the states to default (not busy) when dropped.
/// Acquires lock, checks if resource is busy, and if not,
/// runs the provided update function on the states.
///
/// Returns an `UpdateGuard` that automatically resets the state
/// to default (not busy) when dropped.
///
/// # Errors
///
/// Returns an error if:
/// - The lock is poisoned
/// - The resource is currently busy
///
/// # Example
///
/// ```rust
/// let guard = action_state.update(|state| {
/// *state = SomeNewState;
/// })?;
/// // State is locked and marked as busy
/// // ... perform work ...
/// drop(guard) // Guard is dropped, state returns to default
/// ```
pub fn update(
&self,
update_fn: impl Fn(&mut States),
@@ -91,10 +133,22 @@ impl<States: Default + Busy + Copy + Send + 'static>
}
}
/// When dropped will return the inner state to default.
/// The inner mutex guard must already be dropped BEFORE this is dropped,
/// which is guaranteed as the inner guard is dropped by all public methods before
/// user could drop UpdateGuard.
/// RAII guard that automatically resets the action state when dropped.
///
/// # Safety
///
/// The inner mutex guard is guaranteed to be dropped before this guard
/// is dropped, preventing deadlocks. This is ensured by all public methods
/// that create UpdateGuard instances.
///
/// # Behavior
///
/// When dropped, this guard will:
/// 1. Re-acquire the lock
/// 2. Call the provided return function (typically resetting to default)
/// 3. Release the lock
///
/// If the lock is poisoned, an error is logged but the drop continues.
pub struct UpdateGuard<'a, States: Default + Send + 'static>(
&'a Mutex<States>,
Box<dyn Fn(&mut States) + Send>,
+6 -1
View File
@@ -3,11 +3,12 @@ use std::collections::HashMap;
use komodo_client::entities::{
action::Action, alerter::Alerter, build::Build, builder::Builder,
deployment::Deployment, procedure::Procedure, repo::Repo,
server::Server, stack::Stack, sync::ResourceSync,
server::Server, stack::Stack, swarm::Swarm, sync::ResourceSync,
};
#[derive(Debug, Default)]
pub struct AllResourcesById {
pub swarms: HashMap<String, Swarm>,
pub servers: HashMap<String, Server>,
pub deployments: HashMap<String, Deployment>,
pub stacks: HashMap<String, Stack>,
@@ -27,6 +28,10 @@ impl AllResourcesById {
let id_to_tags = &map;
let match_tags = &[];
Ok(Self {
swarms: crate::resource::get_id_to_resource_map::<Swarm>(
id_to_tags, match_tags,
)
.await?,
servers: crate::resource::get_id_to_resource_map::<Server>(
id_to_tags, match_tags,
)
+2 -2
View File
@@ -30,10 +30,10 @@ pub mod matcher;
pub mod procedure;
pub mod prune;
pub mod query;
pub mod swarm;
pub mod terminal;
pub mod update;
// pub mod resource;
pub mod validations;
pub fn empty_or_only_spaces(word: &str) -> bool {
if word.is_empty() {
+19 -15
View File
@@ -25,6 +25,7 @@ use komodo_client::{
repo::Repo,
server::{Server, ServerState},
stack::{Stack, StackServiceNames, StackState},
swarm::Swarm,
sync::ResourceSync,
tag::Tag,
update::Update,
@@ -55,8 +56,8 @@ pub async fn get_user(user: &str) -> anyhow::Result<User> {
.users
.find_one(id_or_username_filter(user))
.await
.context("failed to query mongo for user")?
.with_context(|| format!("no user found with {user}"))
.context("Failed to query mongo for user")?
.with_context(|| format!("No user found matching '{user}'"))
}
pub async fn get_server_with_state(
@@ -287,24 +288,24 @@ pub async fn get_user_permission_on_target(
) -> anyhow::Result<PermissionLevelAndSpecifics> {
match target {
ResourceTarget::System(_) => Ok(PermissionLevel::None.into()),
ResourceTarget::Build(id) => {
get_user_permission_on_resource::<Build>(user, id).await
}
ResourceTarget::Builder(id) => {
get_user_permission_on_resource::<Builder>(user, id).await
}
ResourceTarget::Deployment(id) => {
get_user_permission_on_resource::<Deployment>(user, id).await
ResourceTarget::Swarm(id) => {
get_user_permission_on_resource::<Swarm>(user, id).await
}
ResourceTarget::Server(id) => {
get_user_permission_on_resource::<Server>(user, id).await
}
ResourceTarget::Stack(id) => {
get_user_permission_on_resource::<Stack>(user, id).await
}
ResourceTarget::Deployment(id) => {
get_user_permission_on_resource::<Deployment>(user, id).await
}
ResourceTarget::Build(id) => {
get_user_permission_on_resource::<Build>(user, id).await
}
ResourceTarget::Repo(id) => {
get_user_permission_on_resource::<Repo>(user, id).await
}
ResourceTarget::Alerter(id) => {
get_user_permission_on_resource::<Alerter>(user, id).await
}
ResourceTarget::Procedure(id) => {
get_user_permission_on_resource::<Procedure>(user, id).await
}
@@ -314,8 +315,11 @@ pub async fn get_user_permission_on_target(
ResourceTarget::ResourceSync(id) => {
get_user_permission_on_resource::<ResourceSync>(user, id).await
}
ResourceTarget::Stack(id) => {
get_user_permission_on_resource::<Stack>(user, id).await
ResourceTarget::Builder(id) => {
get_user_permission_on_resource::<Builder>(user, id).await
}
ResourceTarget::Alerter(id) => {
get_user_permission_on_resource::<Alerter>(user, id).await
}
}
}
+70
View File
@@ -0,0 +1,70 @@
use std::time::Duration;
use anyhow::anyhow;
use komodo_client::entities::server::{Server, ServerState};
use resolver_api::HasResponse;
use serde::{Serialize, de::DeserializeOwned};
use crate::{
helpers::periphery_client, resource, state::server_status_cache,
};
pub async fn swarm_request<T>(
server_ids: &[String],
request: T,
) -> anyhow::Result<T::Response>
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<T>(
server_ids: &[String],
request: T,
timeout: Duration,
) -> anyhow::Result<T::Response>
where
T: std::fmt::Debug + Clone + Serialize + HasResponse,
T::Response: DeserializeOwned,
{
let status_cache = server_status_cache();
let mut err = Option::<anyhow::Error>::None;
for server_id in server_ids {
let Some(ServerState::Ok) =
status_cache.get(server_id).await.map(|status| status.state)
else {
err = Some(anyhow!("No managers connected"));
continue;
};
let Ok(periphery) =
(match resource::get::<Server>(server_id).await {
Ok(server) => periphery_client(&server).await,
Err(e) => {
err = Some(e);
continue;
}
})
else {
continue;
};
match periphery
.request_custom_timeout(request.clone(), timeout)
.await
{
Ok(res) => return Ok(res),
Err(e) => err = Some(e),
}
}
Err(
err
.unwrap_or_else(|| anyhow!("Unknown error"))
.context("Failed to request from Swarm"),
)
}
+85
View File
@@ -0,0 +1,85 @@
//! # Input Validation Module
//!
//! This module provides validation functions for user inputs to prevent
//! invalid data from entering the system and improve security.
use anyhow::Context;
use validations::{StringValidator, StringValidatorMatches};
use crate::config::core_config;
/// Minimum length for usernames
pub const MIN_USERNAME_LENGTH: usize = 1;
/// Maximum length for usernames
pub const MAX_USERNAME_LENGTH: usize = 100;
/// Validate usernames
///
/// - Between [MIN_USERNAME_LENGTH] and [MAX_USERNAME_LENGTH] characters
/// - Matches `^[a-zA-Z0-9._@-]+$`
pub fn validate_username(username: &str) -> anyhow::Result<()> {
StringValidator::default()
.min_length(MIN_USERNAME_LENGTH)
.max_length(MAX_USERNAME_LENGTH)
.matches(StringValidatorMatches::Username)
.validate(username)
.context("Failed to validate username")
}
/// Maximum length for passwords
pub const MAX_PASSWORD_LENGTH: usize = 1000;
/// Validate passwords
///
/// - Between [CoreConfig::min_password_length][komodo_client::entities::config::core::CoreConfig::min_password_length] and [MAX_PASSWORD_LENGTH] characters
pub fn validate_password(password: &str) -> anyhow::Result<()> {
StringValidator::default()
.min_length(core_config().min_password_length as usize)
.max_length(MAX_PASSWORD_LENGTH)
.validate(password)
.context("Failed to validate password")
}
/// Maximum length for API key names
pub const MAX_API_KEY_NAME_LENGTH: usize = 200;
/// Validate api key names
///
/// - Greater than [MAX_API_KEY_NAME_LENGTH] characters
pub fn validate_api_key_name(name: &str) -> anyhow::Result<()> {
StringValidator::default()
.max_length(MAX_API_KEY_NAME_LENGTH)
.validate(name)
.context("Failed to validate api key name")
}
/// Minimum length for variable names
pub const MIN_VARIABLE_NAME_LENGTH: usize = 1;
/// Maximum length for variable names
pub const MAX_VARIABLE_NAME_LENGTH: usize = 500;
/// Validate variable names
///
/// - Between [MIN_VARIABLE_NAME_LENGTH] and [MAX_VARIABLE_NAME_LENGTH] characters
/// - Matches `^[a-zA-Z_][a-zA-Z0-9_]*$`
pub fn validate_variable_name(name: &str) -> anyhow::Result<()> {
StringValidator::default()
.min_length(MIN_VARIABLE_NAME_LENGTH)
.max_length(MAX_VARIABLE_NAME_LENGTH)
.matches(StringValidatorMatches::VariableName)
.validate(name)
.context("Failed to validate variable name")
}
/// Maximum length for variable values
pub const MAX_VARIABLE_VALUE_LENGTH: usize = 10000;
/// Validate variable values
///
/// - Less than [MAX_VARIABLE_VALUE_LENGTH] characters
pub fn validate_variable_value(value: &str) -> anyhow::Result<()> {
StringValidator::default()
.max_length(MAX_VARIABLE_VALUE_LENGTH)
.validate(value)
.context("Failed to validate variable value")
}
@@ -1,2 +0,0 @@
pub mod github;
pub mod gitlab;
+3 -34
View File
@@ -6,12 +6,7 @@ extern crate tracing;
use std::{net::SocketAddr, str::FromStr};
use anyhow::Context;
use axum::{Router, routing::get};
use axum_server::{Handle, tls_rustls::RustlsConfig};
use tower_http::{
cors::{Any, CorsLayer},
services::{ServeDir, ServeFile},
};
use tracing::Instrument;
use crate::config::{core_config, core_keys};
@@ -23,7 +18,6 @@ mod cloud;
mod config;
mod connection;
mod helpers;
mod listener;
mod monitor;
mod network;
mod periphery;
@@ -35,7 +29,6 @@ mod startup;
mod state;
mod sync;
mod ts_client;
mod ws;
async fn app() -> anyhow::Result<()> {
dotenvy::dotenv().ok();
@@ -76,7 +69,7 @@ async fn app() -> anyhow::Result<()> {
startup::on_startup().await;
// Spawn background tasks
monitor::spawn_monitor_loop();
monitor::spawn_monitoring_loops();
resource::spawn_resource_refresh_loop();
resource::spawn_all_resources_cache_refresh_loop();
resource::spawn_build_state_refresh_loop();
@@ -89,32 +82,8 @@ async fn app() -> anyhow::Result<()> {
.instrument(startup_span)
.await;
// Setup static frontend services
let frontend_path = &config.frontend_path;
let frontend_index =
ServeFile::new(format!("{frontend_path}/index.html"));
let serve_frontend = ServeDir::new(frontend_path)
.not_found_service(frontend_index.clone());
let app = Router::new()
.route("/version", get(|| async { env!("CARGO_PKG_VERSION") }))
.nest("/auth", api::auth::router())
.nest("/user", api::user::router())
.nest("/read", api::read::router())
.nest("/write", api::write::router())
.nest("/execute", api::execute::router())
.nest("/terminal", api::terminal::router())
.nest("/listener", listener::router())
.nest("/ws", ws::router())
.nest("/client", ts_client::router())
.fallback_service(serve_frontend)
.layer(
CorsLayer::new()
.allow_origin(Any)
.allow_methods(Any)
.allow_headers(Any),
)
.into_make_service();
let app =
api::app().into_make_service_with_connect_info::<SocketAddr>();
let addr =
format!("{}:{}", core_config().bind_ip, core_config().port);
+55 -74
View File
@@ -1,29 +1,73 @@
use komodo_client::entities::{
alert::SeverityLevel,
deployment::{Deployment, DeploymentState},
docker::{
container::ContainerListItem, image::ImageListItem,
network::NetworkListItem, volume::VolumeListItem,
},
docker::DockerLists,
repo::Repo,
server::{
PeripheryInformation, Server, ServerConfig, ServerHealth,
ServerHealthState, ServerState,
},
stack::{ComposeProject, Stack, StackState},
stack::{Stack, StackState},
stats::{SingleDiskUsage, SystemInformation, SystemStats},
};
use serror::Serror;
use crate::state::{
deployment_status_cache, repo_status_cache, server_status_cache,
stack_status_cache,
CachedDeploymentStatus, CachedRepoStatus, CachedServerStatus,
CachedStackStatus, History, deployment_status_cache,
repo_status_cache, server_status_cache, stack_status_cache,
};
use super::{
CachedDeploymentStatus, CachedRepoStatus, CachedServerStatus,
CachedStackStatus, History,
};
pub async fn insert_server_status(
server: &Server,
state: ServerState,
periphery_info: Option<PeripheryInformation>,
system_info: Option<SystemInformation>,
system_stats: Option<SystemStats>,
docker: Option<DockerLists>,
err: impl Into<Option<Serror>>,
) {
let health =
system_stats.as_ref().map(|s| get_server_health(server, s));
server_status_cache()
.insert(
server.id.clone(),
CachedServerStatus {
id: server.id.clone(),
state,
periphery_info,
system_info,
system_stats,
health,
docker,
err: err.into(),
}
.into(),
)
.await;
}
pub async fn insert_stacks_status_unknown(stacks: Vec<Stack>) {
let status_cache = stack_status_cache();
for stack in stacks {
let prev =
status_cache.get(&stack.id).await.map(|s| s.curr.state);
status_cache
.insert(
stack.id.clone(),
History {
curr: CachedStackStatus {
id: stack.id,
state: StackState::Unknown,
services: Vec::new(),
},
prev,
}
.into(),
)
.await;
}
}
pub async fn insert_deployments_status_unknown(
deployments: Vec<Deployment>,
@@ -66,69 +110,6 @@ pub async fn insert_repos_status_unknown(repos: Vec<Repo>) {
}
}
pub async fn insert_stacks_status_unknown(stacks: Vec<Stack>) {
let status_cache = stack_status_cache();
for stack in stacks {
let prev =
status_cache.get(&stack.id).await.map(|s| s.curr.state);
status_cache
.insert(
stack.id.clone(),
History {
curr: CachedStackStatus {
id: stack.id,
state: StackState::Unknown,
services: Vec::new(),
},
prev,
}
.into(),
)
.await;
}
}
type DockerLists = (
Option<Vec<ContainerListItem>>,
Option<Vec<NetworkListItem>>,
Option<Vec<ImageListItem>>,
Option<Vec<VolumeListItem>>,
Option<Vec<ComposeProject>>,
);
pub async fn insert_server_status(
server: &Server,
state: ServerState,
periphery_info: Option<PeripheryInformation>,
system_info: Option<SystemInformation>,
system_stats: Option<SystemStats>,
(containers, networks, images, volumes, projects): DockerLists,
err: impl Into<Option<Serror>>,
) {
let health =
system_stats.as_ref().map(|s| get_server_health(server, s));
server_status_cache()
.insert(
server.id.clone(),
CachedServerStatus {
id: server.id.clone(),
state,
periphery_info,
system_info,
system_stats,
health,
containers,
networks,
images,
volumes,
projects,
err: err.into(),
}
.into(),
)
.await;
}
const ALERT_PERCENTAGE_THRESHOLD: f32 = 5.0;
fn get_server_health(
+45 -88
View File
@@ -7,30 +7,26 @@ use futures_util::future::join_all;
use helpers::insert_stacks_status_unknown;
use komodo_client::entities::{
build::Build,
deployment::{Deployment, DeploymentState},
docker::{
container::ContainerListItem, image::ImageListItem,
network::NetworkListItem, volume::VolumeListItem,
},
deployment::Deployment,
komodo_timestamp, optional_string,
repo::Repo,
server::{PeripheryInformation, Server, ServerHealth, ServerState},
stack::{ComposeProject, Stack, StackService, StackState},
stats::{SystemInformation, SystemStats},
server::{Server, ServerState},
stack::Stack,
stats::SystemStats,
};
use periphery_client::api::{
self, PollStatusResponse, git::GetLatestCommit,
self, git::GetLatestCommit, poll::PollStatusResponse,
};
use serror::Serror;
use tokio::sync::Mutex;
use crate::{
config::core_config,
config::monitoring_interval,
helpers::periphery_client,
monitor::{alert::check_alerts, record::record_server_stats},
state::{
db_client, deployment_status_cache, periphery_connections,
repo_status_cache,
CachedRepoStatus, db_client, deployment_status_cache,
periphery_connections, repo_status_cache,
},
};
@@ -43,64 +39,21 @@ mod alert;
mod helpers;
mod record;
mod resources;
mod swarm;
#[derive(Default, Debug)]
pub struct History<Curr: Default, Prev> {
pub curr: Curr,
pub prev: Option<Prev>,
}
#[derive(Default, Clone, Debug)]
pub struct CachedServerStatus {
pub id: String,
pub state: ServerState,
pub health: Option<ServerHealth>,
pub periphery_info: Option<PeripheryInformation>,
pub system_info: Option<SystemInformation>,
pub system_stats: Option<SystemStats>,
pub containers: Option<Vec<ContainerListItem>>,
pub networks: Option<Vec<NetworkListItem>>,
pub images: Option<Vec<ImageListItem>>,
pub volumes: Option<Vec<VolumeListItem>>,
pub projects: Option<Vec<ComposeProject>>,
/// Store the error in reaching periphery
pub err: Option<serror::Serror>,
}
#[derive(Default, Clone, Debug)]
pub struct CachedDeploymentStatus {
/// The deployment id
pub id: String,
pub state: DeploymentState,
pub container: Option<ContainerListItem>,
pub update_available: bool,
}
#[derive(Default, Clone, Debug)]
pub struct CachedRepoStatus {
pub latest_hash: Option<String>,
pub latest_message: Option<String>,
}
#[derive(Default, Clone, Debug)]
pub struct CachedStackStatus {
/// The stack id
pub id: String,
/// The stack state
pub state: StackState,
/// The services connected to the stack
pub services: Vec<StackService>,
}
pub use swarm::update_cache_for_swarm;
const ADDITIONAL_MS: u128 = 500;
pub fn spawn_monitor_loop() {
let interval: async_timing_util::Timelength = core_config()
.monitoring_interval
.try_into()
.expect("Invalid monitoring interval");
pub fn spawn_monitoring_loops() {
spawn_server_monitoring_loop();
swarm::spawn_swarm_monitoring_loop();
}
fn spawn_server_monitoring_loop() {
tokio::spawn(async move {
refresh_server_cache(komodo_timestamp()).await;
let interval = monitoring_interval();
loop {
let ts = (wait_until_timelength(interval, ADDITIONAL_MS).await
- ADDITIONAL_MS) as i64;
@@ -115,7 +68,7 @@ async fn refresh_server_cache(ts: i64) {
Ok(servers) => servers,
Err(e) => {
error!(
"failed to get server list (manage status cache) | {e:#}"
"Failed to get server list (refresh server cache) | {e:#}"
);
return;
}
@@ -172,7 +125,7 @@ pub async fn update_cache_for_server(server: &Server, force: bool) {
None,
None,
None,
(None, None, None, None, None),
None,
None,
)
.await;
@@ -190,7 +143,7 @@ pub async fn update_cache_for_server(server: &Server, force: bool) {
None,
None,
None,
(None, None, None, None, None),
None,
Serror::from(&e),
)
.await;
@@ -202,14 +155,11 @@ pub async fn update_cache_for_server(server: &Server, force: bool) {
periphery_info,
system_info,
system_stats,
mut containers,
networks,
images,
volumes,
projects,
mut docker,
} = match periphery
.request(api::PollStatus {
.request(api::poll::PollStatus {
include_stats: server.config.stats_monitoring,
include_docker: true,
})
.await
{
@@ -222,7 +172,7 @@ pub async fn update_cache_for_server(server: &Server, force: bool) {
None,
None,
None,
(None, None, None, None, None),
None,
Serror::from(&e),
)
.await;
@@ -230,37 +180,44 @@ pub async fn update_cache_for_server(server: &Server, force: bool) {
}
};
containers.iter_mut().for_each(|container| {
container.server_id = Some(server.id.clone())
});
if let Some(docker) = &mut docker {
docker.containers.iter_mut().for_each(|container| {
container.server_id = Some(server.id.clone())
});
}
let containers = docker
.as_ref()
.map(|docker| docker.containers.as_slice())
.unwrap_or(&[]);
let images = docker
.as_ref()
.map(|docker| docker.images.as_slice())
.unwrap_or(&[]);
tokio::join!(
resources::update_deployment_cache(
server.name.clone(),
resources.deployments,
&containers,
&images,
containers,
images,
&resources.builds,
),
resources::update_stack_cache(
server.name.clone(),
resources.stacks,
&containers,
&images
containers,
images
),
);
insert_server_status(
server,
ServerState::Ok,
Some(periphery_info),
Some(system_info),
system_stats.map(|stats| filter_volumes(server, stats)),
(
Some(containers.clone()),
Some(networks),
Some(images),
Some(volumes),
Some(projects),
),
docker,
None,
)
.await;
+237 -210
View File
@@ -1,5 +1,6 @@
use std::{
collections::HashSet,
hash::Hash,
sync::{Mutex, OnceLock},
};
@@ -30,15 +31,213 @@ use crate::{
services::extract_services_from_stack,
},
state::{
CachedDeploymentStatus, CachedStackStatus, History,
action_states, db_client, deployment_status_cache,
stack_status_cache,
},
};
use super::{CachedDeploymentStatus, CachedStackStatus, History};
/// (StackId, Service)
fn stack_alert_sent_cache() -> &'static AlertCache<(String, String)> {
static CACHE: OnceLock<AlertCache<(String, String)>> =
OnceLock::new();
CACHE.get_or_init(Default::default)
}
fn deployment_alert_sent_cache() -> &'static Mutex<HashSet<String>> {
static CACHE: OnceLock<Mutex<HashSet<String>>> = OnceLock::new();
pub async fn update_stack_cache(
server_name: String,
stacks: Vec<Stack>,
containers: &[ContainerListItem],
images: &[ImageListItem],
) {
let stack_status_cache = stack_status_cache();
let stack_alert_sent_cache = stack_alert_sent_cache();
for stack in stacks {
let services = extract_services_from_stack(&stack);
let mut services_with_containers = services.iter().map(|StackServiceNames { service_name, container_name, image }| {
let container = containers.iter().find(|container| {
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:#}");
return false
}
}.is_match(&container.name)
}).cloned();
let image = if image.contains(':') {
image.to_string()
} else {
format!("{image}:latest")
};
let update_available = if let Some(ContainerListItem { image_id: Some(curr_image_id), .. }) = &container {
// Docker will automatically strip `docker.io` from incoming image names re #468.
// Need to strip it in order to match by image tag and find available update.
let image =
image.strip_prefix("docker.io/").unwrap_or(&image);
images
.iter()
.find(|i| i.name == image)
.map(|i| &i.id != curr_image_id)
.unwrap_or_default()
} else {
false
};
if update_available {
if !stack.config.auto_update
&& stack.config.send_alerts
&& let Some(container) = &container
&& container.state == ContainerStateStatusEnum::Running
&& !stack_alert_sent_cache
.contains(&(stack.id.clone(), service_name.clone()))
{
stack_alert_sent_cache
.insert((stack.id.clone(), service_name.clone()));
let ts = komodo_timestamp();
let alert = Alert {
id: Default::default(),
ts,
resolved: true,
resolved_ts: ts.into(),
level: SeverityLevel::Ok,
target: ResourceTarget::Stack(stack.id.clone()),
data: AlertData::StackImageUpdateAvailable {
id: stack.id.clone(),
name: stack.name.clone(),
server_name: server_name.clone(),
server_id: stack.config.server_id.clone(),
service: service_name.clone(),
image: image.clone(),
},
};
tokio::spawn(async move {
let res = db_client().alerts.insert_one(&alert).await;
if let Err(e) = res {
error!(
"Failed to record StackImageUpdateAvailable to db | {e:#}"
);
}
send_alerts(&[alert]).await;
});
}
} else {
// If it sees there is no longer update available, remove
// from the sent cache, so on next `update_available = true`
// the cache is empty and a fresh alert will be sent.
stack_alert_sent_cache
.remove(&(stack.id.clone(), service_name.clone()));
}
StackService {
service: service_name.clone(),
image: image.clone(),
container,
update_available,
}
}).collect::<Vec<_>>();
let mut images_with_update = Vec::new();
let mut services_to_update = Vec::new();
for service in services_with_containers.iter() {
if service.update_available {
images_with_update.push(service.image.clone());
// Only allow it to actually trigger an auto update deploy
// if the service is running.
if service
.container
.as_ref()
.map(|c| c.state == ContainerStateStatusEnum::Running)
.unwrap_or_default()
{
services_to_update.push(service.service.clone());
}
}
}
let state = get_stack_state_from_containers(
&stack.config.ignore_services,
&services,
containers,
);
if !services_to_update.is_empty()
&& stack.config.auto_update
&& state == StackState::Running
&& !action_states()
.stack
.get_or_insert_default(&stack.id)
.await
.busy()
.unwrap_or(true)
{
let id = stack.id.clone();
let server_name = server_name.clone();
let services = if stack.config.auto_update_all_services {
Vec::new()
} else {
services_to_update
};
tokio::spawn(async move {
match execute::inner_handler(
ExecuteRequest::DeployStack(DeployStack {
stack: stack.name.clone(),
services,
stop_time: None,
}),
auto_redeploy_user().to_owned(),
)
.await
{
Ok(_) => {
let ts = komodo_timestamp();
let alert = Alert {
id: Default::default(),
ts,
resolved: true,
resolved_ts: ts.into(),
level: SeverityLevel::Ok,
target: ResourceTarget::Stack(id.clone()),
data: AlertData::StackAutoUpdated {
id,
name: stack.name.clone(),
server_name,
server_id: stack.config.server_id,
images: images_with_update,
},
};
let res = db_client().alerts.insert_one(&alert).await;
if let Err(e) = res {
error!(
"Failed to record StackAutoUpdated to db | {e:#}"
);
}
send_alerts(&[alert]).await;
}
Err(e) => {
warn!("Failed auto update Stack {} | {e:#}", stack.name,)
}
}
});
}
services_with_containers
.sort_by(|a, b| a.service.cmp(&b.service));
let prev = stack_status_cache
.get(&stack.id)
.await
.map(|s| s.curr.state);
let status = CachedStackStatus {
id: stack.id.clone(),
state,
services: services_with_containers,
};
stack_status_cache
.insert(stack.id, History { curr: status, prev }.into())
.await;
}
}
fn deployment_alert_sent_cache() -> &'static AlertCache<String> {
static CACHE: OnceLock<AlertCache<String>> = OnceLock::new();
CACHE.get_or_init(Default::default)
}
@@ -50,6 +249,8 @@ pub async fn update_deployment_cache(
builds: &[Build],
) {
let deployment_status_cache = deployment_status_cache();
let deployment_alert_sent_cache = deployment_alert_sent_cache();
for deployment in deployments {
let container = containers
.iter()
@@ -163,16 +364,10 @@ pub async fn update_deployment_cache(
}
} else if state == DeploymentState::Running
&& deployment.config.send_alerts
&& !deployment_alert_sent_cache()
.lock()
.unwrap()
.contains(&deployment.id)
&& deployment_alert_sent_cache.contains(&deployment.id)
{
// Add that it is already sent to the cache, so another alert won't be sent.
deployment_alert_sent_cache()
.lock()
.unwrap()
.insert(deployment.id.clone());
deployment_alert_sent_cache.insert(deployment.id.clone());
let ts = komodo_timestamp();
let alert = Alert {
id: Default::default(),
@@ -201,10 +396,7 @@ pub async fn update_deployment_cache(
// If it sees there is no longer update available, remove
// from the sent cache, so on next `update_available = true`
// the cache is empty and a fresh alert will be sent.
deployment_alert_sent_cache()
.lock()
.unwrap()
.remove(&deployment.id);
deployment_alert_sent_cache.remove(&deployment.id);
}
deployment_status_cache
.insert(
@@ -224,204 +416,39 @@ pub async fn update_deployment_cache(
}
}
/// (StackId, Service)
fn stack_alert_sent_cache()
-> &'static Mutex<HashSet<(String, String)>> {
static CACHE: OnceLock<Mutex<HashSet<(String, String)>>> =
OnceLock::new();
CACHE.get_or_init(Default::default)
}
struct AlertCache<K>(Mutex<HashSet<K>>);
pub async fn update_stack_cache(
server_name: String,
stacks: Vec<Stack>,
containers: &[ContainerListItem],
images: &[ImageListItem],
) {
let stack_status_cache = stack_status_cache();
for stack in stacks {
let services = extract_services_from_stack(&stack);
let mut services_with_containers = services.iter().map(|StackServiceNames { service_name, container_name, image }| {
let container = containers.iter().find(|container| {
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:#}");
return false
}
}.is_match(&container.name)
}).cloned();
let image = if image.contains(':') {
image.to_string()
} else {
format!("{image}:latest")
};
let update_available = if let Some(ContainerListItem { image_id: Some(curr_image_id), .. }) = &container {
// Docker will automatically strip `docker.io` from incoming image names re #468.
// Need to strip it in order to match by image tag and find available update.
let image =
image.strip_prefix("docker.io/").unwrap_or(&image);
images
.iter()
.find(|i| i.name == image)
.map(|i| &i.id != curr_image_id)
.unwrap_or_default()
} else {
false
};
if update_available {
if !stack.config.auto_update
&& stack.config.send_alerts
&& container.is_some()
&& container.as_ref().unwrap().state == ContainerStateStatusEnum::Running
&& !stack_alert_sent_cache()
.lock()
.unwrap()
.contains(&(stack.id.clone(), service_name.clone()))
{
stack_alert_sent_cache()
.lock()
.unwrap()
.insert((stack.id.clone(), service_name.clone()));
let ts = komodo_timestamp();
let alert = Alert {
id: Default::default(),
ts,
resolved: true,
resolved_ts: ts.into(),
level: SeverityLevel::Ok,
target: ResourceTarget::Stack(stack.id.clone()),
data: AlertData::StackImageUpdateAvailable {
id: stack.id.clone(),
name: stack.name.clone(),
server_name: server_name.clone(),
server_id: stack.config.server_id.clone(),
service: service_name.clone(),
image: image.clone(),
},
};
tokio::spawn(async move {
let res = db_client().alerts.insert_one(&alert).await;
if let Err(e) = res {
error!(
"Failed to record StackImageUpdateAvailable to db | {e:#}"
);
}
send_alerts(&[alert]).await;
});
}
} else {
stack_alert_sent_cache()
.lock()
.unwrap()
.remove(&(stack.id.clone(), service_name.clone()));
}
StackService {
service: service_name.clone(),
image: image.clone(),
container,
update_available,
}
}).collect::<Vec<_>>();
impl<K> Default for AlertCache<K> {
fn default() -> Self {
Self(Default::default())
}
}
let mut images_with_update = Vec::new();
let mut services_to_update = Vec::new();
impl<K: Eq + Hash> AlertCache<K> {
/// Checks the cache for an existing key entry.
/// If the cache is poisoned, will always return 'true' and include error log.
fn contains(&self, key: &K) -> bool {
self
.0
.lock()
.map(|cache| cache.contains(key))
.map_err(|_| error!("Alert Cache poisoned, this blocks container state change alerts, please restart Komodo Core."))
.unwrap_or(true)
}
for service in services_with_containers.iter() {
if service.update_available {
images_with_update.push(service.image.clone());
// Only allow it to actually trigger an auto update deploy
// if the service is running.
if service
.container
.as_ref()
.map(|c| c.state == ContainerStateStatusEnum::Running)
.unwrap_or_default()
{
services_to_update.push(service.service.clone());
}
}
}
fn insert(&self, key: K) {
let _ = self
.0
.lock()
.map(|mut cache| cache.insert(key))
.map_err(|_| error!("Alert Cache poisoned, this blocks container state change alerts, please restart Komodo Core."));
}
let state = get_stack_state_from_containers(
&stack.config.ignore_services,
&services,
containers,
);
if !services_to_update.is_empty()
&& stack.config.auto_update
&& state == StackState::Running
&& !action_states()
.stack
.get_or_insert_default(&stack.id)
.await
.busy()
.unwrap_or(true)
{
let id = stack.id.clone();
let server_name = server_name.clone();
let services = if stack.config.auto_update_all_services {
Vec::new()
} else {
services_to_update
};
tokio::spawn(async move {
match execute::inner_handler(
ExecuteRequest::DeployStack(DeployStack {
stack: stack.name.clone(),
services,
stop_time: None,
}),
auto_redeploy_user().to_owned(),
)
.await
{
Ok(_) => {
let ts = komodo_timestamp();
let alert = Alert {
id: Default::default(),
ts,
resolved: true,
resolved_ts: ts.into(),
level: SeverityLevel::Ok,
target: ResourceTarget::Stack(id.clone()),
data: AlertData::StackAutoUpdated {
id,
name: stack.name.clone(),
server_name,
server_id: stack.config.server_id,
images: images_with_update,
},
};
let res = db_client().alerts.insert_one(&alert).await;
if let Err(e) = res {
error!(
"Failed to record StackAutoUpdated to db | {e:#}"
);
}
send_alerts(&[alert]).await;
}
Err(e) => {
warn!("Failed auto update Stack {} | {e:#}", stack.name,)
}
}
});
}
services_with_containers
.sort_by(|a, b| a.service.cmp(&b.service));
let prev = stack_status_cache
.get(&stack.id)
.await
.map(|s| s.curr.state);
let status = CachedStackStatus {
id: stack.id.clone(),
state,
services: services_with_containers,
};
stack_status_cache
.insert(stack.id, History { curr: status, prev }.into())
.await;
fn remove(&self, key: &K) {
let _ = self
.0
.lock()
.map(|mut cache| cache.remove(key))
.map_err(|_| error!("Alert Cache poisoned, this blocks container state change alerts, please restart Komodo Core."));
}
}
+158
View File
@@ -0,0 +1,158 @@
use std::{
sync::{Arc, OnceLock},
time::Duration,
};
use anyhow::anyhow;
use async_timing_util::wait_until_timelength;
use cache::CloneCache;
use database::mungos::find::find_collect;
use formatting::format_serror;
use futures_util::future::join_all;
use komodo_client::entities::{
docker::node::NodeState,
komodo_timestamp,
swarm::{Swarm, SwarmState},
};
use periphery_client::api::swarm::{
PollSwarmStatus, PollSwarmStatusResponse,
};
use tokio::sync::Mutex;
use crate::{
config::monitoring_interval,
helpers::swarm::swarm_request_custom_timeout,
state::{CachedSwarmStatus, db_client, swarm_status_cache},
};
const ADDITIONAL_MS: u128 = 1000;
pub fn spawn_swarm_monitoring_loop() {
tokio::spawn(async move {
refresh_swarm_cache(komodo_timestamp()).await;
let interval = monitoring_interval();
loop {
let ts = (wait_until_timelength(interval, ADDITIONAL_MS).await
- ADDITIONAL_MS) as i64;
refresh_swarm_cache(ts).await;
}
});
}
async fn refresh_swarm_cache(_ts: i64) {
let swarms =
match find_collect(&db_client().swarms, None, None).await {
Ok(swarms) => swarms,
Err(e) => {
error!(
"Failed to get swarm list (refresh swarm cache) | {e:#}"
);
return;
}
};
let futures = swarms.into_iter().map(|swarm| async move {
update_cache_for_swarm(&swarm, false).await;
});
join_all(futures).await;
// tokio::join!(check_alerts(ts), record_swarm_stats(ts));
}
/// Makes sure cache for swarm doesn't update too frequently / simultaneously.
/// If forced, will still block against simultaneous update.
fn update_cache_for_swarm_controller()
-> &'static CloneCache<String, Arc<Mutex<i64>>> {
static CACHE: OnceLock<CloneCache<String, Arc<Mutex<i64>>>> =
OnceLock::new();
CACHE.get_or_init(Default::default)
}
/// The background loop will call this with force: false,
/// which exits early if the lock is busy or it was completed too recently.
/// If force is true, it will wait on simultaneous calls, and will
/// ignore the restriction on being completed too recently.
pub async fn update_cache_for_swarm(swarm: &Swarm, force: bool) {
// Concurrency controller to ensure it isn't done too often
// when it happens in other contexts.
let controller = update_cache_for_swarm_controller()
.get_or_insert_default(&swarm.id)
.await;
let mut lock = match controller.try_lock() {
Ok(lock) => lock,
Err(_) if force => controller.lock().await,
Err(_) => return,
};
let now = komodo_timestamp();
// early return if called again sooner than 1s.
if !force && *lock > now - 1_000 {
return;
}
*lock = now;
if swarm.config.server_ids.is_empty() {
swarm_status_cache()
.insert(
swarm.id.clone(),
CachedSwarmStatus {
state: SwarmState::Unknown,
inspect: None,
lists: None,
err: Some(format_serror(
&anyhow!("No Servers configured as manager nodes").into(),
)),
}
.into(),
)
.await;
return;
}
let PollSwarmStatusResponse { inspect, lists } =
match swarm_request_custom_timeout(
&swarm.config.server_ids,
PollSwarmStatus {},
Duration::from_secs(1),
)
.await
{
Ok(info) => info,
Err(e) => {
swarm_status_cache()
.insert(
swarm.id.clone(),
CachedSwarmStatus {
state: SwarmState::Unknown,
inspect: None,
lists: None,
err: Some(format_serror(&e.into())),
}
.into(),
)
.await;
return;
}
};
let mut state = SwarmState::Healthy;
for node in &lists.nodes {
if !matches!(node.state, Some(NodeState::READY)) {
state = SwarmState::Unhealthy;
}
}
swarm_status_cache()
.insert(
swarm.id.clone(),
CachedSwarmStatus {
state,
inspect,
lists: Some(lists),
err: None,
}
.into(),
)
.await;
}
+31 -17
View File
@@ -12,7 +12,6 @@ use uuid::Uuid;
use crate::{
connection::{
PeripheryConnection, PeripheryConnectionArgs, ResponseChannels,
TerminalChannels,
},
state::periphery_connections,
};
@@ -24,7 +23,6 @@ pub struct PeripheryClient {
/// Usually the server id
pub id: String,
pub responses: Arc<ResponseChannels>,
pub terminals: Arc<TerminalChannels>,
}
impl PeripheryClient {
@@ -51,7 +49,6 @@ impl PeripheryClient {
return Ok(PeripheryClient {
id,
responses: connection.responses.clone(),
terminals: connection.terminals.clone(),
});
}
@@ -68,7 +65,6 @@ impl PeripheryClient {
Ok(PeripheryClient {
id,
responses: connection.responses.clone(),
terminals: connection.terminals.clone(),
})
} else {
// Core -> Periphery connection
@@ -89,6 +85,20 @@ impl PeripheryClient {
&self,
request: T,
) -> anyhow::Result<T::Response>
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<T>(
&self,
request: T,
timeout: Duration,
) -> anyhow::Result<T::Response>
where
T: std::fmt::Debug + Serialize + HasResponse,
T::Response: DeserializeOwned,
@@ -117,24 +127,28 @@ impl PeripheryClient {
.await
.context("Failed to send request over channel")
{
// cleanup
self.terminals.remove(&channel_id).await;
self.responses.remove(&channel_id).await;
return Err(e);
}
// Poll for the associated response
loop {
let message = response_receiever
.recv()
.with_timeout(Duration::from_secs(10))
.await?;
let res = async {
// Poll for the associated response
loop {
let message =
response_receiever.recv().with_timeout(timeout).await?;
// Still in progress, sent to avoid timeout.
let Some(message) = message.decode()? else {
continue;
};
// Still in progress, sent to avoid timeout.
let Some(message) = message.decode()? else {
continue;
};
return message.decode();
return message.decode();
}
}
.await;
self.responses.remove(&channel_id).await;
res
}
}
+11 -8
View File
@@ -60,6 +60,7 @@ mod refresh;
mod repo;
mod server;
mod stack;
mod swarm;
mod sync;
pub use action::{
@@ -647,20 +648,21 @@ pub async fn update<T: KomodoResource>(
fn resource_target<T: KomodoResource>(id: String) -> ResourceTarget {
match T::resource_type() {
ResourceTargetVariant::System => ResourceTarget::System(id),
ResourceTargetVariant::Build => ResourceTarget::Build(id),
ResourceTargetVariant::Builder => ResourceTarget::Builder(id),
ResourceTargetVariant::Swarm => ResourceTarget::Swarm(id),
ResourceTargetVariant::Server => ResourceTarget::Server(id),
ResourceTargetVariant::Stack => ResourceTarget::Stack(id),
ResourceTargetVariant::Deployment => {
ResourceTarget::Deployment(id)
}
ResourceTargetVariant::Server => ResourceTarget::Server(id),
ResourceTargetVariant::Build => ResourceTarget::Build(id),
ResourceTargetVariant::Repo => ResourceTarget::Repo(id),
ResourceTargetVariant::Alerter => ResourceTarget::Alerter(id),
ResourceTargetVariant::Procedure => ResourceTarget::Procedure(id),
ResourceTargetVariant::Action => ResourceTarget::Action(id),
ResourceTargetVariant::ResourceSync => {
ResourceTarget::ResourceSync(id)
}
ResourceTargetVariant::Stack => ResourceTarget::Stack(id),
ResourceTargetVariant::Action => ResourceTarget::Action(id),
ResourceTargetVariant::Builder => ResourceTarget::Builder(id),
ResourceTargetVariant::Alerter => ResourceTarget::Alerter(id),
}
}
@@ -929,16 +931,17 @@ where
{
let resource: ResourceTarget = resource.into();
let (recent_field, id) = match resource {
ResourceTarget::Swarm(id) => ("recents.Swarm", id),
ResourceTarget::Server(id) => ("recents.Server", id),
ResourceTarget::Stack(id) => ("recents.Stack", id),
ResourceTarget::Deployment(id) => ("recents.Deployment", id),
ResourceTarget::Build(id) => ("recents.Build", id),
ResourceTarget::Repo(id) => ("recents.Repo", id),
ResourceTarget::Procedure(id) => ("recents.Procedure", id),
ResourceTarget::Action(id) => ("recents.Action", id),
ResourceTarget::Stack(id) => ("recents.Stack", id),
ResourceTarget::ResourceSync(id) => ("recents.ResourceSync", id),
ResourceTarget::Builder(id) => ("recents.Builder", id),
ResourceTarget::Alerter(id) => ("recents.Alerter", id),
ResourceTarget::ResourceSync(id) => ("recents.ResourceSync", id),
ResourceTarget::System(_) => return,
};
if let Err(e) = db_client()
+3 -2
View File
@@ -147,8 +147,9 @@ impl super::KomodoResource for Stack {
.await
.as_ref()
{
if let Some(projects) = &status.projects {
if let Some(project) = projects
if let Some(docker) = &status.docker {
if let Some(project) = docker
.projects
.iter()
.find(|project| project.name == project_name)
{
+195
View File
@@ -0,0 +1,195 @@
use anyhow::Context;
use database::mungos::mongodb::Collection;
use komodo_client::entities::{
Operation, ResourceTarget, ResourceTargetVariant,
permission::PermissionLevel,
resource::Resource,
server::Server,
swarm::{
PartialSwarmConfig, Swarm, SwarmConfig, SwarmConfigDiff,
SwarmInfo, SwarmListItem, SwarmListItemInfo, SwarmQuerySpecifics,
},
update::Update,
user::User,
};
use crate::{
config::core_config,
monitor::update_cache_for_swarm,
state::{db_client, swarm_status_cache},
};
use super::get_check_permissions;
impl super::KomodoResource for Swarm {
type Config = SwarmConfig;
type PartialConfig = PartialSwarmConfig;
type ConfigDiff = SwarmConfigDiff;
type Info = SwarmInfo;
type ListItem = SwarmListItem;
type QuerySpecifics = SwarmQuerySpecifics;
fn resource_type() -> ResourceTargetVariant {
ResourceTargetVariant::Swarm
}
fn resource_target(id: impl Into<String>) -> ResourceTarget {
ResourceTarget::Swarm(id.into())
}
fn coll() -> &'static Collection<Resource<Self::Config, Self::Info>>
{
&db_client().swarms
}
async fn to_list_item(
swarm: Resource<Self::Config, Self::Info>,
) -> Self::ListItem {
let (state, err) = swarm_status_cache()
.get(&swarm.id)
.await
.map(|status| (status.state, status.err.clone()))
.unwrap_or_default();
SwarmListItem {
name: swarm.name,
id: swarm.id,
template: swarm.template,
tags: swarm.tags,
resource_type: ResourceTargetVariant::Swarm,
info: SwarmListItemInfo {
server_ids: swarm.config.server_ids,
state,
err,
},
}
}
async fn busy(_id: &String) -> anyhow::Result<bool> {
Ok(false)
}
// CREATE
fn create_operation() -> Operation {
Operation::CreateSwarm
}
fn user_can_create(user: &User) -> bool {
user.admin || !core_config().disable_non_admin_create
}
async fn validate_create_config(
config: &mut Self::PartialConfig,
user: &User,
) -> anyhow::Result<()> {
validate_config(config, user).await
}
async fn post_create(
created: &Self,
_update: &mut Update,
) -> anyhow::Result<()> {
update_cache_for_swarm(created, true).await;
Ok(())
}
// UPDATE
fn update_operation() -> Operation {
Operation::UpdateSwarm
}
async fn validate_update_config(
_id: &str,
config: &mut Self::PartialConfig,
user: &User,
) -> anyhow::Result<()> {
validate_config(config, user).await
}
async fn post_update(
updated: &Self,
_update: &mut Update,
) -> anyhow::Result<()> {
update_cache_for_swarm(updated, true).await;
Ok(())
}
// RENAME
fn rename_operation() -> Operation {
Operation::RenameSwarm
}
// DELETE
fn delete_operation() -> Operation {
Operation::DeleteSwarm
}
async fn pre_delete(
_swarm: &Self,
_update: &mut Update,
) -> anyhow::Result<()> {
Ok(())
}
async fn post_delete(
swarm: &Self,
_update: &mut Update,
) -> anyhow::Result<()> {
swarm_status_cache().remove(&swarm.id).await;
Ok(())
}
}
#[instrument("ValidateSwarmConfig", skip_all)]
async fn validate_config(
config: &mut PartialSwarmConfig,
user: &User,
) -> anyhow::Result<()> {
if let Some(server_ids) = &mut config.server_ids {
let mut res = Vec::with_capacity(server_ids.capacity());
for server_id in server_ids.iter_mut() {
let server = get_check_permissions::<Server>(
server_id,
user,
PermissionLevel::Read.attach(),
)
.await
.with_context(|| {
format!("Cannot attach Server {server_id} to this Swarm")
})?;
res.push(server.id);
}
*server_ids = res;
}
Ok(())
}
// pub fn spawn_swarm_state_refresh_loop() {
// tokio::spawn(async move {
// loop {
// refresh_swarm_state_cache().await;
// tokio::time::sleep(Duration::from_secs(60)).await;
// }
// });
// }
// pub async fn refresh_swarm_state_cache() {
// let _ = async {
// let swarms = find_collect(&db_client().swarms, None, None)
// .await
// .context("failed to get swarms from db")?;
// let cache = swarm_state_cache();
// for swarm in swarms {
// let state = get_swarm_state_from_db(&swarm.id).await;
// cache.insert(swarm.id, state).await;
// }
// anyhow::Ok(())
// }
// .await
// .inspect_err(|e| {
// warn!("failed to refresh swarm state cache | {e:#}")
// });
// }
+66 -20
View File
@@ -1,5 +1,9 @@
use std::str::FromStr;
use std::{
net::{IpAddr, Ipv4Addr},
str::FromStr,
};
use anyhow::Context;
use colored::Colorize;
use database::mungos::{
find::find_collect,
@@ -125,8 +129,15 @@ async fn in_progress_update_cleanup() {
"Komodo shutdown during execution. If this is a build, the builder may not have been terminated.",
),
);
// This static log won't fail to serialize, unwrap ok.
let log = to_document(&log).unwrap();
let log = match to_document(&log)
.context("Failed to serialize log to document")
{
Ok(log) => log,
Err(e) => {
error!("Failed to cleanup in progress update | {e:#}");
return;
}
};
if let Err(e) = db_client()
.updates
.update_many(
@@ -292,19 +303,37 @@ async fn ensure_init_user_and_resources() {
// Init admin user if set in config.
if let Some(username) = &config.init_admin_username {
info!("Creating init admin user...");
SignUpLocalUser {
if let Err(e) = (SignUpLocalUser {
username: username.clone(),
password: config.init_admin_password.clone(),
}
.resolve(&AuthArgs::default())
})
.resolve(&AuthArgs {
headers: Default::default(),
ip: IpAddr::V4(Ipv4Addr::UNSPECIFIED),
})
.await
.expect("Failed to initialize default admin user.");
db.users
{
error!("Failed to create init admin user | {:#}", e.error);
return;
}
match db
.users
.find_one(doc! { "username": username })
.await
.expect("Failed to query database for initial user")
.expect("Failed to find initial user after creation");
};
.context(
"Failed to query database for init admin user after creation",
) {
Ok(Some(_)) => {
info!("Successfully created init admin user.")
}
Ok(None) => {
error!("Failed to find init admin user after creation");
}
Err(e) => {
error!("{e:#}");
}
}
}
if config.disable_init_resources {
info!("System resources init {}", "DISABLED".red());
@@ -487,38 +516,55 @@ async fn clean_up_server_templates() {
let db = db_client();
tokio::join!(
async {
db.permissions
if let Err(e) = db
.permissions
.delete_many(doc! {
"resource_target.type": "ServerTemplate",
})
.await
.expect(
"Failed to clean up server template permissions on db",
{
error!(
"Failed to clean up server template permissions on database | {e:#}"
);
}
},
async {
db.updates
if let Err(e) = db
.updates
.delete_many(doc! { "target.type": "ServerTemplate" })
.await
.expect("Failed to clean up server template updates on db");
{
error!(
"Failed to clean up server template updates on database | {e:#}"
);
}
},
async {
db.users
if let Err(e) = db.users
.update_many(
Document::new(),
doc! { "$unset": { "recents.ServerTemplate": 1, "all.ServerTemplate": 1 } }
)
.await
.expect("Failed to clean up server template updates on db");
{
error!(
"Failed to clean up server template user references on database | {e:#}"
);
}
},
async {
db.user_groups
if let Err(e) = db
.user_groups
.update_many(
Document::new(),
doc! { "$unset": { "all.ServerTemplate": 1 } },
)
.await
.expect("Failed to clean up server template updates on db");
{
error!(
"Failed to clean up server template user group references on database | {e:#}"
);
}
},
);
}
+121 -20
View File
@@ -1,13 +1,24 @@
use std::sync::{Arc, OnceLock};
use anyhow::Context;
use anyhow::{Context, anyhow};
use arc_swap::ArcSwap;
use cache::CloneCache;
use komodo_client::entities::{
action::ActionState, build::BuildState,
deployment::DeploymentState, procedure::ProcedureState,
repo::RepoState, stack::StackState,
action::ActionState,
build::BuildState,
deployment::DeploymentState,
docker::{
DockerLists, SwarmLists, container::ContainerListItem,
swarm::SwarmInspectInfo,
},
procedure::ProcedureState,
repo::RepoState,
server::{PeripheryInformation, ServerHealth, ServerState},
stack::{StackService, StackState},
stats::{SystemInformation, SystemStats},
swarm::SwarmState,
};
use rate_limit::RateLimiter;
use crate::{
auth::jwt::JwtClient,
@@ -16,29 +27,39 @@ use crate::{
helpers::{
action_state::ActionStates, all_resources::AllResourcesById,
},
monitor::{
CachedDeploymentStatus, CachedRepoStatus, CachedServerStatus,
CachedStackStatus, History,
},
};
static DB_CLIENT: OnceLock<database::Client> = OnceLock::new();
pub fn db_client() -> &'static database::Client {
DB_CLIENT
.get()
.expect("db_client accessed before initialized")
DB_CLIENT.get().unwrap_or_else(|| {
error!(
"FATAL: db_client accessed before initialized | Ensure init_db_client() is called during startup | Exiting..."
);
std::process::exit(1)
})
}
/// Must be called in app startup sequence.
pub async fn init_db_client() {
let client = database::Client::new(&core_config().database)
.await
.context("failed to initialize database client")
.unwrap();
DB_CLIENT
.set(client)
.expect("db_client initialized more than once");
let init = async {
let client = database::Client::new(&core_config().database)
.await
.context("failed to initialize database client")?;
DB_CLIENT.set(client).map_err(|_| {
anyhow!(
"db_client initialized more than once - this should not happen"
)
})?;
anyhow::Ok(())
}
.await;
if let Err(e) = init {
error!(
"FATAL: Failed to initialize database::Client | {e:#} | Exiting..."
);
std::process::exit(1)
}
}
pub fn jwt_client() -> &'static JwtClient {
@@ -46,8 +67,10 @@ pub fn jwt_client() -> &'static JwtClient {
JWT_CLIENT.get_or_init(|| match JwtClient::new(core_config()) {
Ok(client) => client,
Err(e) => {
error!("failed to initialialize JwtClient | {e:#}");
panic!("Exiting");
error!(
"FATAL: Failed to initialialize JwtClient | {e:#} | Exiting..."
);
std::process::exit(1)
}
})
}
@@ -64,6 +87,43 @@ pub fn action_states() -> &'static ActionStates {
ACTION_STATES.get_or_init(ActionStates::default)
}
#[derive(Default, Debug)]
pub struct History<Curr: Default, Prev> {
pub curr: Curr,
pub prev: Option<Prev>,
}
#[derive(Default, Clone, Debug)]
pub struct CachedSwarmStatus {
pub state: SwarmState,
pub inspect: Option<SwarmInspectInfo>,
pub lists: Option<SwarmLists>,
/// Store the error in communicating with Swarm
pub err: Option<String>,
}
pub type SwarmStatusCache =
CloneCache<String, Arc<CachedSwarmStatus>>;
pub fn swarm_status_cache() -> &'static SwarmStatusCache {
static SWARM_STATUS_CACHE: OnceLock<SwarmStatusCache> =
OnceLock::new();
SWARM_STATUS_CACHE.get_or_init(Default::default)
}
#[derive(Default, Clone, Debug)]
pub struct CachedServerStatus {
pub id: String,
pub state: ServerState,
pub health: Option<ServerHealth>,
pub periphery_info: Option<PeripheryInformation>,
pub system_info: Option<SystemInformation>,
pub system_stats: Option<SystemStats>,
pub docker: Option<DockerLists>,
/// Store the error in reaching periphery
pub err: Option<serror::Serror>,
}
pub type ServerStatusCache =
CloneCache<String, Arc<CachedServerStatus>>;
@@ -73,6 +133,16 @@ pub fn server_status_cache() -> &'static ServerStatusCache {
SERVER_STATUS_CACHE.get_or_init(Default::default)
}
#[derive(Default, Clone, Debug)]
pub struct CachedStackStatus {
/// The stack id
pub id: String,
/// The stack state
pub state: StackState,
/// The services connected to the stack
pub services: Vec<StackService>,
}
pub type StackStatusCache =
CloneCache<String, Arc<History<CachedStackStatus, StackState>>>;
@@ -82,6 +152,15 @@ pub fn stack_status_cache() -> &'static StackStatusCache {
STACK_STATUS_CACHE.get_or_init(Default::default)
}
#[derive(Default, Clone, Debug)]
pub struct CachedDeploymentStatus {
/// The deployment id
pub id: String,
pub state: DeploymentState,
pub container: Option<ContainerListItem>,
pub update_available: bool,
}
/// Cache of ids to status
pub type DeploymentStatusCache = CloneCache<
String,
@@ -103,6 +182,12 @@ pub fn build_state_cache() -> &'static BuildStateCache {
BUILD_STATE_CACHE.get_or_init(Default::default)
}
#[derive(Default, Clone, Debug)]
pub struct CachedRepoStatus {
pub latest_hash: Option<String>,
pub latest_message: Option<String>,
}
pub type RepoStatusCache = CloneCache<String, Arc<CachedRepoStatus>>;
pub fn repo_status_cache() -> &'static RepoStatusCache {
@@ -139,3 +224,19 @@ pub fn all_resources_cache() -> &'static ArcSwap<AllResourcesById> {
OnceLock::new();
ALL_RESOURCES.get_or_init(Default::default)
}
pub fn auth_rate_limiter() -> &'static RateLimiter {
static AUTH_RATE_LIMITER: OnceLock<Arc<RateLimiter>> =
OnceLock::new();
AUTH_RATE_LIMITER.get_or_init(|| {
let config = core_config();
if config.auth_rate_limit_disabled {
warn!("Auth rate limiting is disabled")
}
RateLimiter::new(
config.auth_rate_limit_disabled,
config.auth_rate_limit_max_attempts as usize,
config.auth_rate_limit_window_seconds,
)
})
}
+12
View File
@@ -14,6 +14,7 @@ use komodo_client::{
repo::Repo,
server::Server,
stack::Stack,
swarm::Swarm,
sync::ResourceSync,
tag::Tag,
update::Log,
@@ -34,6 +35,17 @@ use super::{
include_resource_by_tags,
};
impl ResourceSyncTrait for Swarm {
fn get_diff(
original: Self::Config,
update: Self::PartialConfig,
) -> anyhow::Result<Self::ConfigDiff> {
Ok(original.partial_diff(update))
}
}
impl ExecuteResourceSync for Swarm {}
impl ResourceSyncTrait for Server {
fn get_diff(
original: Self::Config,
+37
View File
@@ -15,6 +15,7 @@ use komodo_client::{
resource::Resource,
server::Server,
stack::Stack,
swarm::Swarm,
sync::ResourceSync,
tag::Tag,
toml::ResourceToml,
@@ -174,6 +175,42 @@ impl ToToml for ResourceSync {
}
}
impl ToToml for Swarm {
fn replace_ids(resource: &mut Resource<Self::Config, Self::Info>) {
let all = all_resources_cache().load();
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<Self::PartialConfig>,
config: IndexMap<String, serde_json::Value>,
) -> anyhow::Result<IndexMap<String, serde_json::Value>> {
config
.into_iter()
.map(|(key, value)| {
#[allow(clippy::single_match)]
match key.as_str() {
"server_ids" => {
return Ok((String::from("servers"), value));
}
_ => {}
}
Ok((key, value))
})
.collect()
}
}
impl ToToml for Stack {
fn replace_ids(resource: &mut Resource<Self::Config, Self::Info>) {
let all = all_resources_cache().load();
+112 -86
View File
@@ -323,23 +323,9 @@ pub async fn get_updates_for_execution(
// replace the ids with names
match &mut p.resource_target {
ResourceTarget::System(_) => {}
ResourceTarget::Build(id) => {
ResourceTarget::Swarm(id) => {
*id = all_resources
.builds
.get(id)
.map(|b| b.name.clone())
.unwrap_or_default()
}
ResourceTarget::Builder(id) => {
*id = all_resources
.builders
.get(id)
.map(|b| b.name.clone())
.unwrap_or_default()
}
ResourceTarget::Deployment(id) => {
*id = all_resources
.deployments
.swarms
.get(id)
.map(|b| b.name.clone())
.unwrap_or_default()
@@ -351,16 +337,30 @@ pub async fn get_updates_for_execution(
.map(|b| b.name.clone())
.unwrap_or_default()
}
ResourceTarget::Repo(id) => {
ResourceTarget::Stack(id) => {
*id = all_resources
.repos
.stacks
.get(id)
.map(|b| b.name.clone())
.unwrap_or_default()
}
ResourceTarget::Alerter(id) => {
ResourceTarget::Deployment(id) => {
*id = all_resources
.alerters
.deployments
.get(id)
.map(|b| b.name.clone())
.unwrap_or_default()
}
ResourceTarget::Build(id) => {
*id = all_resources
.builds
.get(id)
.map(|b| b.name.clone())
.unwrap_or_default()
}
ResourceTarget::Repo(id) => {
*id = all_resources
.repos
.get(id)
.map(|b| b.name.clone())
.unwrap_or_default()
@@ -386,9 +386,16 @@ pub async fn get_updates_for_execution(
.map(|b| b.name.clone())
.unwrap_or_default()
}
ResourceTarget::Stack(id) => {
ResourceTarget::Builder(id) => {
*id = all_resources
.stacks
.builders
.get(id)
.map(|b| b.name.clone())
.unwrap_or_default()
}
ResourceTarget::Alerter(id) => {
*id = all_resources
.alerters
.get(id)
.map(|b| b.name.clone())
.unwrap_or_default()
@@ -796,37 +803,13 @@ async fn expand_user_group_permissions(
}
let matcher = Matcher::new(id)?;
match variant {
ResourceTargetVariant::Build => {
ResourceTargetVariant::Swarm => {
let permissions = all_resources
.builds
.swarms
.values()
.filter(|resource| matcher.is_match(&resource.name))
.map(|resource| PermissionToml {
target: ResourceTarget::Build(resource.name.clone()),
level: permission.level,
specific: permission.specific.clone(),
});
expanded.extend(permissions);
}
ResourceTargetVariant::Builder => {
let permissions = all_resources
.builders
.values()
.filter(|resource| matcher.is_match(&resource.name))
.map(|resource| PermissionToml {
target: ResourceTarget::Builder(resource.name.clone()),
level: permission.level,
specific: permission.specific.clone(),
});
expanded.extend(permissions);
}
ResourceTargetVariant::Deployment => {
let permissions = all_resources
.deployments
.values()
.filter(|resource| matcher.is_match(&resource.name))
.map(|resource| PermissionToml {
target: ResourceTarget::Deployment(resource.name.clone()),
target: ResourceTarget::Swarm(resource.name.clone()),
level: permission.level,
specific: permission.specific.clone(),
});
@@ -844,6 +827,42 @@ async fn expand_user_group_permissions(
});
expanded.extend(permissions);
}
ResourceTargetVariant::Stack => {
let permissions = all_resources
.stacks
.values()
.filter(|resource| matcher.is_match(&resource.name))
.map(|resource| PermissionToml {
target: ResourceTarget::Stack(resource.name.clone()),
level: permission.level,
specific: permission.specific.clone(),
});
expanded.extend(permissions);
}
ResourceTargetVariant::Deployment => {
let permissions = all_resources
.deployments
.values()
.filter(|resource| matcher.is_match(&resource.name))
.map(|resource| PermissionToml {
target: ResourceTarget::Deployment(resource.name.clone()),
level: permission.level,
specific: permission.specific.clone(),
});
expanded.extend(permissions);
}
ResourceTargetVariant::Build => {
let permissions = all_resources
.builds
.values()
.filter(|resource| matcher.is_match(&resource.name))
.map(|resource| PermissionToml {
target: ResourceTarget::Build(resource.name.clone()),
level: permission.level,
specific: permission.specific.clone(),
});
expanded.extend(permissions);
}
ResourceTargetVariant::Repo => {
let permissions = all_resources
.repos
@@ -856,18 +875,6 @@ async fn expand_user_group_permissions(
});
expanded.extend(permissions);
}
ResourceTargetVariant::Alerter => {
let permissions = all_resources
.alerters
.values()
.filter(|resource| matcher.is_match(&resource.name))
.map(|resource| PermissionToml {
target: ResourceTarget::Alerter(resource.name.clone()),
level: permission.level,
specific: permission.specific.clone(),
});
expanded.extend(permissions);
}
ResourceTargetVariant::Procedure => {
let permissions = all_resources
.procedures
@@ -906,13 +913,25 @@ async fn expand_user_group_permissions(
});
expanded.extend(permissions);
}
ResourceTargetVariant::Stack => {
ResourceTargetVariant::Builder => {
let permissions = all_resources
.stacks
.builders
.values()
.filter(|resource| matcher.is_match(&resource.name))
.map(|resource| PermissionToml {
target: ResourceTarget::Stack(resource.name.clone()),
target: ResourceTarget::Builder(resource.name.clone()),
level: permission.level,
specific: permission.specific.clone(),
});
expanded.extend(permissions);
}
ResourceTargetVariant::Alerter => {
let permissions = all_resources
.alerters
.values()
.filter(|resource| matcher.is_match(&resource.name))
.map(|resource| PermissionToml {
target: ResourceTarget::Alerter(resource.name.clone()),
level: permission.level,
specific: permission.specific.clone(),
});
@@ -1040,23 +1059,9 @@ pub async fn convert_user_groups(
.filter(|permission| permission.level > PermissionLevel::None)
.map(|mut permission| {
match &mut permission.resource_target {
ResourceTarget::Build(id) => {
ResourceTarget::Swarm(id) => {
*id = all
.builds
.get(id)
.map(|r| r.name.clone())
.unwrap_or_default()
}
ResourceTarget::Builder(id) => {
*id = all
.builders
.get(id)
.map(|r| r.name.clone())
.unwrap_or_default()
}
ResourceTarget::Deployment(id) => {
*id = all
.deployments
.swarms
.get(id)
.map(|r| r.name.clone())
.unwrap_or_default()
@@ -1068,16 +1073,30 @@ pub async fn convert_user_groups(
.map(|r| r.name.clone())
.unwrap_or_default()
}
ResourceTarget::Repo(id) => {
ResourceTarget::Stack(id) => {
*id = all
.repos
.stacks
.get(id)
.map(|r| r.name.clone())
.unwrap_or_default()
}
ResourceTarget::Alerter(id) => {
ResourceTarget::Deployment(id) => {
*id = all
.alerters
.deployments
.get(id)
.map(|r| r.name.clone())
.unwrap_or_default()
}
ResourceTarget::Build(id) => {
*id = all
.builds
.get(id)
.map(|r| r.name.clone())
.unwrap_or_default()
}
ResourceTarget::Repo(id) => {
*id = all
.repos
.get(id)
.map(|r| r.name.clone())
.unwrap_or_default()
@@ -1103,9 +1122,16 @@ pub async fn convert_user_groups(
.map(|r| r.name.clone())
.unwrap_or_default()
}
ResourceTarget::Stack(id) => {
ResourceTarget::Builder(id) => {
*id = all
.stacks
.builders
.get(id)
.map(|r| r.name.clone())
.unwrap_or_default()
}
ResourceTarget::Alerter(id) => {
*id = all
.alerters
.get(id)
.map(|r| r.name.clone())
.unwrap_or_default()
-113
View File
@@ -1,113 +0,0 @@
use crate::{
auth::{auth_api_key_check_enabled, auth_jwt_check_enabled},
helpers::query::get_user,
};
use anyhow::anyhow;
use axum::{
Router,
extract::ws::{self, WebSocket},
routing::get,
};
use futures_util::SinkExt;
use komodo_client::{entities::user::User, ws::WsLoginMessage};
mod terminal;
mod update;
pub fn router() -> Router {
Router::new()
// Periphery facing
.route("/periphery", get(crate::connection::server::handler))
// User facing
.route("/update", get(update::handler))
.route("/terminal", get(terminal::handler))
}
async fn user_ws_login(
mut socket: WebSocket,
) -> Option<(WebSocket, User)> {
let login_msg = match socket.recv().await {
Some(Ok(ws::Message::Text(login_msg))) => {
LoginMessage::Ok(login_msg.to_string())
}
Some(Ok(msg)) => {
LoginMessage::Err(format!("invalid login message: {msg:?}"))
}
Some(Err(e)) => {
LoginMessage::Err(format!("failed to get login message: {e:?}"))
}
None => {
LoginMessage::Err("failed to get login message".to_string())
}
};
let login_msg = match login_msg {
LoginMessage::Ok(login_msg) => login_msg,
LoginMessage::Err(msg) => {
let _ = socket.send(ws::Message::text(msg)).await;
let _ = socket.close().await;
return None;
}
};
match WsLoginMessage::from_json_str(&login_msg) {
// Login using a jwt
Ok(WsLoginMessage::Jwt { jwt }) => {
match auth_jwt_check_enabled(&jwt).await {
Ok(user) => {
let _ = socket.send(ws::Message::text("LOGGED_IN")).await;
Some((socket, user))
}
Err(e) => {
let _ = socket
.send(ws::Message::text(format!(
"failed to authenticate user using jwt | {e:#}"
)))
.await;
let _ = socket.close().await;
None
}
}
}
// login using api keys
Ok(WsLoginMessage::ApiKeys { key, secret }) => {
match auth_api_key_check_enabled(&key, &secret).await {
Ok(user) => {
let _ = socket.send(ws::Message::text("LOGGED_IN")).await;
Some((socket, user))
}
Err(e) => {
let _ = socket
.send(ws::Message::text(format!(
"failed to authenticate user using api keys | {e:#}"
)))
.await;
let _ = socket.close().await;
None
}
}
}
Err(e) => {
let _ = socket
.send(ws::Message::text(format!(
"failed to parse login message: {e:#}"
)))
.await;
let _ = socket.close().await;
None
}
}
}
enum LoginMessage {
/// The text message
Ok(String),
/// The err message
Err(String),
}
async fn check_user_valid(user_id: &str) -> anyhow::Result<User> {
let user = get_user(user_id).await?;
if !user.enabled {
return Err(anyhow!("user not enabled"));
}
Ok(user)
}
+1 -1
View File
@@ -1,6 +1,6 @@
## All in one, multi stage compile + runtime Docker build for your architecture.
FROM rust:1.90.0-trixie AS builder
FROM rust:1.91.1-trixie AS builder
RUN cargo install cargo-strip
WORKDIR /builder
+12 -67
View File
@@ -12,9 +12,8 @@ use komodo_client::{
entities::{
FileContents, RepoExecutionResponse, all_logs_success,
stack::{
ComposeFile, ComposeProject, ComposeService,
ComposeServiceDeploy, StackRemoteFileContents,
StackServiceNames,
ComposeFile, ComposeService, ComposeServiceDeploy,
StackRemoteFileContents, StackServiceNames,
},
to_path_compatible_name,
update::Log,
@@ -23,12 +22,12 @@ use komodo_client::{
};
use periphery_client::api::compose::*;
use resolver_api::Resolve;
use serde::{Deserialize, Serialize};
use shell_escape::unix::escape;
use tracing::Instrument;
use crate::{
config::periphery_config,
docker::compose::docker_compose,
helpers::{format_extra_args, format_log_grep},
};
@@ -37,67 +36,6 @@ mod write;
use helpers::*;
fn docker_compose() -> &'static str {
if periphery_config().legacy_compose_cli {
"docker-compose"
} else {
"docker compose"
}
}
pub async fn list_compose_projects()
-> anyhow::Result<Vec<ComposeProject>> {
let docker_compose = docker_compose();
let res = run_komodo_standard_command(
"List Projects",
None,
format!("{docker_compose} ls --all --format json"),
)
.await;
if !res.success {
return Err(anyhow!("{}", res.combined()).context(format!(
"failed to list compose projects using {docker_compose} ls"
)));
}
let res =
serde_json::from_str::<Vec<DockerComposeLsItem>>(&res.stdout)
.with_context(|| res.stdout.clone())
.with_context(|| {
format!(
"failed to parse '{docker_compose} ls' response to json"
)
})?
.into_iter()
.filter(|item| !item.name.is_empty())
.map(|item| ComposeProject {
name: item.name,
status: item.status,
compose_files: item
.config_files
.split(',')
.map(str::to_string)
.collect(),
})
.collect();
Ok(res)
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct DockerComposeLsItem {
#[serde(default, alias = "Name")]
pub name: String,
#[serde(alias = "Status")]
pub status: Option<String>,
/// Comma seperated list of paths
#[serde(default, alias = "ConfigFiles")]
pub config_files: String,
}
//
impl Resolve<super::Args> for GetComposeLog {
async fn resolve(self, _: &super::Args) -> anyhow::Result<Log> {
let GetComposeLog {
@@ -117,7 +55,7 @@ impl Resolve<super::Args> for GetComposeLog {
services.join(" ")
);
Ok(
run_komodo_standard_command("get stack log", None, command)
run_komodo_standard_command("Get Stack Log", None, command)
.await,
)
}
@@ -145,7 +83,7 @@ impl Resolve<super::Args> for GetComposeLogSearch {
services.join(" ")
);
Ok(
run_komodo_shell_command("Get stack log grep", None, command)
run_komodo_shell_command("Search Stack Log", None, command)
.await,
)
}
@@ -670,6 +608,13 @@ impl Resolve<super::Args> for ComposeUp {
let compose_cmd_wrapper =
parse_multiline_command(&stack.config.compose_cmd_wrapper);
if !compose_cmd_wrapper.is_empty() {
if !compose_cmd_wrapper.contains("[[COMPOSE_COMMAND]]") {
res.logs.push(Log::error(
"Compose Command Wrapper",
"compose_cmd_wrapper is configured but does not contain [[COMPOSE_COMMAND]] placeholder. The placeholder is required to inject the compose command.".to_string(),
));
return Ok(res);
}
command =
compose_cmd_wrapper.replace("[[COMPOSE_COMMAND]]", &command);
}
+15 -82
View File
@@ -1,28 +1,20 @@
use command::run_komodo_standard_command;
use derive_variants::EnumVariants;
use encoding::{EncodedJsonMessage, EncodedResponse};
use futures_util::FutureExt;
use komodo_client::entities::{
config::{DockerRegistry, GitProvider},
server::PeripheryInformation,
stats::SystemProcess,
update::Log,
};
use periphery_client::api::{
build::*, compose::*, container::*, docker::*, git::*, keys::*,
stats::*, terminal::*, *,
poll::*, stats::*, swarm::*, terminal::*, *,
};
use resolver_api::Resolve;
use serde::{Deserialize, Serialize};
use uuid::Uuid;
use crate::{
api::compose::list_compose_projects,
config::periphery_config,
state::{
docker_client, host_public_ip, periphery_keys, stats_client,
},
};
use crate::{config::periphery_config, state::stats_client};
pub mod terminal;
@@ -33,6 +25,8 @@ mod deploy;
mod docker;
mod git;
mod keys;
mod poll;
mod swarm;
#[derive(Debug)]
pub struct Args {
@@ -142,6 +136,17 @@ pub enum PeripheryRequest {
// All in one (Write)
PruneSystem(PruneSystem),
// Swarm
PollSwarmStatus(PollSwarmStatus),
InspectSwarmNode(InspectSwarmNode),
InspectSwarmConfig(InspectSwarmConfig),
InspectSwarmSecret(InspectSwarmSecret),
InspectSwarmStack(InspectSwarmStack),
InspectSwarmTask(InspectSwarmTask),
InspectSwarmService(InspectSwarmService),
GetSwarmServiceLog(GetSwarmServiceLog),
GetSwarmServiceLogSearch(GetSwarmServiceLogSearch),
// Terminal
ListTerminals(ListTerminals),
CreateServerTerminal(CreateServerTerminal),
@@ -184,78 +189,6 @@ impl Resolve<Args> for GetVersion {
//
impl Resolve<Args> for PollStatus {
async fn resolve(
self,
_: &Args,
) -> anyhow::Result<PollStatusResponse> {
// Docker lists
let docker_lists = async {
let client = docker_client().load();
let Some(client) = client.iter().next() else {
return Default::default();
};
let containers =
client.list_containers().await.unwrap_or_default();
// Todo: handle errors better
(
tokio::join!(
client
.list_networks(&containers)
.map(Result::unwrap_or_default),
client
.list_images(&containers)
.map(Result::unwrap_or_default),
client
.list_volumes(&containers)
.map(Result::unwrap_or_default)
),
containers,
)
};
let (
((networks, images, volumes), containers),
projects,
stats_client,
) = tokio::join!(
docker_lists,
list_compose_projects().map(Result::unwrap_or_default),
stats_client().read(),
);
let system_stats = if self.include_stats {
Some(stats_client.stats.clone())
} else {
None
};
let config = periphery_config();
Ok(PollStatusResponse {
periphery_info: PeripheryInformation {
version: env!("CARGO_PKG_VERSION").to_string(),
public_key: periphery_keys().load().public.to_string(),
terminals_disabled: config.disable_terminals,
container_terminals_disabled: config
.disable_container_terminals,
stats_polling_rate: config.stats_polling_rate,
docker_connected: docker_client().load().is_some(),
public_ip: host_public_ip().await.cloned(),
},
system_info: stats_client.info.clone(),
system_stats,
containers,
networks,
images,
volumes,
projects,
})
}
}
//
impl Resolve<Args> for GetSystemProcesses {
async fn resolve(
self,
+83
View File
@@ -0,0 +1,83 @@
use futures_util::FutureExt;
use komodo_client::entities::{
docker::DockerLists, server::PeripheryInformation,
};
use periphery_client::api::poll::{PollStatus, PollStatusResponse};
use resolver_api::Resolve;
use crate::{
config::periphery_config,
docker::{DockerClient, compose::list_compose_projects},
state::{
docker_client, host_public_ip, periphery_keys, stats_client,
},
};
impl Resolve<super::Args> for PollStatus {
async fn resolve(
self,
_: &super::Args,
) -> anyhow::Result<PollStatusResponse> {
let stats_client = stats_client().read().await;
let system_stats = if self.include_stats {
Some(stats_client.stats.clone())
} else {
None
};
let docker = if self.include_docker {
let client = docker_client().load();
if let Some(client) = client.iter().next() {
Some(docker_lists(client).await)
} else {
None
}
} else {
None
};
Ok(PollStatusResponse {
periphery_info: periphery_information().await,
system_info: stats_client.info.clone(),
system_stats,
docker,
})
}
}
async fn periphery_information() -> PeripheryInformation {
let config = periphery_config();
PeripheryInformation {
version: env!("CARGO_PKG_VERSION").to_string(),
public_key: periphery_keys().load().public.to_string(),
terminals_disabled: config.disable_terminals,
container_terminals_disabled: config.disable_container_terminals,
stats_polling_rate: config.stats_polling_rate,
docker_connected: docker_client().load().is_some(),
public_ip: host_public_ip().await.cloned(),
}
}
async fn docker_lists(client: &DockerClient) -> DockerLists {
let containers = client.list_containers().await.unwrap_or_default();
let (networks, images, volumes, projects) = tokio::join!(
client
.list_networks(&containers)
.map(Result::unwrap_or_default),
client
.list_images(&containers)
.map(Result::unwrap_or_default),
client
.list_volumes(&containers)
.map(Result::unwrap_or_default),
list_compose_projects().map(Result::unwrap_or_default),
);
DockerLists {
containers,
networks,
images,
volumes,
projects,
}
}
+245
View File
@@ -0,0 +1,245 @@
use anyhow::Context as _;
use command::{
run_komodo_shell_command, run_komodo_standard_command,
};
use komodo_client::entities::{
docker::{
SwarmLists, config::SwarmConfig, node::SwarmNode,
secret::SwarmSecret, service::SwarmService,
stack::SwarmStackLists, task::SwarmTask,
},
update::Log,
};
use periphery_client::api::swarm::*;
use resolver_api::Resolve;
use crate::{
docker::{
config::{inspect_swarm_config, list_swarm_configs},
stack::{inspect_swarm_stack, list_swarm_stacks},
},
helpers::format_log_grep,
state::docker_client,
};
impl Resolve<super::Args> for PollSwarmStatus {
async fn resolve(
self,
_: &super::Args,
) -> anyhow::Result<PollSwarmStatusResponse> {
let client = docker_client().load();
let client = client
.iter()
.next()
.context("Could not connect to docker client")?;
let (inspect, nodes, services, tasks, secrets, configs, stacks) = tokio::join!(
client.inspect_swarm(),
client.list_swarm_nodes(),
client.list_swarm_services(),
client.list_swarm_tasks(),
client.list_swarm_secrets(),
list_swarm_configs(),
list_swarm_stacks(),
);
Ok(PollSwarmStatusResponse {
inspect: inspect.ok(),
lists: SwarmLists {
nodes: nodes.unwrap_or_default(),
services: services.unwrap_or_default(),
tasks: tasks.unwrap_or_default(),
secrets: secrets.unwrap_or_default(),
configs: configs.unwrap_or_default(),
stacks: stacks.unwrap_or_default(),
},
})
}
}
// ======
// Node
// ======
impl Resolve<super::Args> for InspectSwarmNode {
async fn resolve(
self,
_: &super::Args,
) -> anyhow::Result<SwarmNode> {
let client = docker_client().load();
let client = client
.iter()
.next()
.context("Could not connect to docker client")?;
client.inspect_swarm_node(&self.node).await
}
}
// =========
// Service
// =========
impl Resolve<super::Args> for InspectSwarmService {
async fn resolve(
self,
_: &super::Args,
) -> anyhow::Result<SwarmService> {
let client = docker_client().load();
let client = client
.iter()
.next()
.context("Could not connect to docker client")?;
client.inspect_swarm_service(&self.service).await
}
}
impl Resolve<super::Args> for GetSwarmServiceLog {
async fn resolve(self, _: &super::Args) -> anyhow::Result<Log> {
let GetSwarmServiceLog {
service,
tail,
timestamps,
no_task_ids,
no_resolve,
details,
} = self;
let timestamps = if timestamps {
" --timestamps"
} else {
Default::default()
};
let no_task_ids = if no_task_ids {
" --no-task-ids"
} else {
Default::default()
};
let no_resolve = if no_resolve {
" --no-resolve"
} else {
Default::default()
};
let details = if details {
" --details"
} else {
Default::default()
};
let command = format!(
"docker service logs --tail {tail}{timestamps}{no_task_ids}{no_resolve}{details} {service}",
);
Ok(
run_komodo_standard_command(
"Get Swarm Service Log",
None,
command,
)
.await,
)
}
}
impl Resolve<super::Args> for GetSwarmServiceLogSearch {
async fn resolve(self, _: &super::Args) -> anyhow::Result<Log> {
let GetSwarmServiceLogSearch {
service,
terms,
combinator,
invert,
timestamps,
no_task_ids,
no_resolve,
details,
} = self;
let timestamps = if timestamps {
" --timestamps"
} else {
Default::default()
};
let no_task_ids = if no_task_ids {
" --no-task-ids"
} else {
Default::default()
};
let no_resolve = if no_resolve {
" --no-resolve"
} else {
Default::default()
};
let details = if details {
" --details"
} else {
Default::default()
};
let grep = format_log_grep(&terms, combinator, invert);
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,
)
.await,
)
}
}
// ======
// Task
// ======
impl Resolve<super::Args> for InspectSwarmTask {
async fn resolve(
self,
_: &super::Args,
) -> anyhow::Result<SwarmTask> {
let client = docker_client().load();
let client = client
.iter()
.next()
.context("Could not connect to docker client")?;
client.inspect_swarm_task(&self.task).await
}
}
// ========
// Secret
// ========
impl Resolve<super::Args> for InspectSwarmSecret {
async fn resolve(
self,
_: &super::Args,
) -> anyhow::Result<SwarmSecret> {
let client = docker_client().load();
let client = client
.iter()
.next()
.context("Could not connect to docker client")?;
client.inspect_swarm_secret(&self.secret).await
}
}
// ========
// Config
// ========
impl Resolve<super::Args> for InspectSwarmConfig {
async fn resolve(
self,
_: &super::Args,
) -> anyhow::Result<Vec<SwarmConfig>> {
inspect_swarm_config(&self.config).await
}
}
// =======
// Stack
// =======
impl Resolve<super::Args> for InspectSwarmStack {
async fn resolve(
self,
_: &super::Args,
) -> anyhow::Result<SwarmStackLists> {
inspect_swarm_stack(self.stack).await
}
}
+2 -1
View File
@@ -171,7 +171,8 @@ async fn handle_onboarding(
public_key_validator: core_public_keys(),
socket: &mut socket,
})
.await?;
.await
.context("Onboarding failed")?;
// Post onboarding login 1: Send public key
socket
+65
View File
@@ -0,0 +1,65 @@
use anyhow::{Context, anyhow};
use command::run_komodo_standard_command;
use komodo_client::entities::stack::ComposeProject;
use serde::{Deserialize, Serialize};
use crate::config::periphery_config;
pub fn docker_compose() -> &'static str {
if periphery_config().legacy_compose_cli {
"docker-compose"
} else {
"docker compose"
}
}
pub async fn list_compose_projects()
-> anyhow::Result<Vec<ComposeProject>> {
let docker_compose = docker_compose();
let res = run_komodo_standard_command(
"List Projects",
None,
format!("{docker_compose} ls --all --format json"),
)
.await;
if !res.success {
return Err(anyhow!("{}", res.combined()).context(format!(
"Failed to list compose projects using {docker_compose} ls"
)));
}
let res =
serde_json::from_str::<Vec<DockerComposeLsItem>>(&res.stdout)
.with_context(|| res.stdout.clone())
.with_context(|| {
format!(
"Failed to parse '{docker_compose} ls' response from json"
)
})?
.into_iter()
.filter(|item| !item.name.is_empty())
.map(|item| ComposeProject {
name: item.name,
status: item.status,
compose_files: item
.config_files
.split(',')
.map(str::to_string)
.collect(),
})
.collect();
Ok(res)
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct DockerComposeLsItem {
#[serde(default, alias = "Name")]
pub name: String,
#[serde(alias = "Status")]
pub status: Option<String>,
/// Comma seperated list of paths
#[serde(default, alias = "ConfigFiles")]
pub config_files: String,
}
+49
View File
@@ -0,0 +1,49 @@
use anyhow::{Context, anyhow};
use command::run_komodo_standard_command;
use komodo_client::entities::docker::config::{
SwarmConfig, SwarmConfigListItem,
};
pub async fn list_swarm_configs()
-> anyhow::Result<Vec<SwarmConfigListItem>> {
let res = run_komodo_standard_command(
"List Swarm Configs",
None,
"docker config ls --format json",
)
.await;
if !res.success {
return Err(anyhow!("{}", res.combined()).context(
"Failed to list swarm configs using 'docker config ls'",
));
}
// The output is in JSONL, need to convert to standard JSON vec.
serde_json::from_str(&format!(
"[{}]",
res.stdout.trim().replace('\n', ",")
))
.context("Failed to parse 'docker config ls' response from json")
}
pub async fn inspect_swarm_config(
config: &str,
) -> anyhow::Result<Vec<SwarmConfig>> {
let res = run_komodo_standard_command(
"Inspect Swarm Config",
None,
format!(r#"docker config inspect "{config}""#),
)
.await;
if !res.success {
return Err(anyhow!("{}", res.combined()).context(format!(
"Failed to inspect swarm config using 'docker config inspect {config}'"
)));
}
serde_json::from_str(&res.stdout).context(
"Failed to parse 'docker config inspect' response from json",
)
}
@@ -4,14 +4,14 @@ use anyhow::Context;
use bollard::query_parameters::{
InspectContainerOptions, ListContainersOptions,
};
use komodo_client::entities::docker::{
ContainerConfig, GraphDriverData, HealthConfig, PortBinding,
container::*,
};
use komodo_client::entities::docker::{container::*, *};
use crate::state::container_stats;
use super::DockerClient;
use super::{
DockerClient, convert_health_config, convert_mount,
convert_mount_point_type, convert_resources_ulimits,
};
impl DockerClient {
pub async fn list_containers(
@@ -257,11 +257,7 @@ impl DockerClient {
.ulimits
.unwrap_or_default()
.into_iter()
.map(|ulimit| ResourcesUlimits {
name: ulimit.name,
soft: ulimit.soft,
hard: ulimit.hard,
})
.map(convert_resources_ulimits)
.collect(),
cpu_count: config.cpu_count,
cpu_percent: config.cpu_percent,
@@ -309,49 +305,7 @@ impl DockerClient {
.mounts
.unwrap_or_default()
.into_iter()
.map(|mount| ContainerMount {
target: mount.target,
source: mount.source,
typ: mount
.typ
.map(convert_mount_type)
.unwrap_or_default(),
read_only: mount.read_only,
consistency: mount.consistency,
bind_options: mount.bind_options.map(|options| {
MountBindOptions {
propagation: options
.propagation
.map(convert_mount_propogation)
.unwrap_or_default(),
non_recursive: options.non_recursive,
create_mountpoint: options.create_mountpoint,
read_only_non_recursive: options
.read_only_non_recursive,
read_only_force_recursive: options
.read_only_force_recursive,
}
}),
volume_options: mount.volume_options.map(|options| {
MountVolumeOptions {
no_copy: options.no_copy,
labels: options.labels.unwrap_or_default(),
driver_config: options.driver_config.map(|config| {
MountVolumeOptionsDriverConfig {
name: config.name,
options: config.options.unwrap_or_default(),
}
}),
subpath: options.subpath,
}
}),
tmpfs_options: mount.tmpfs_options.map(|options| {
MountTmpfsOptions {
size_bytes: options.size_bytes,
mode: options.mode,
}
}),
})
.map(convert_mount)
.collect(),
console_size: config
.console_size
@@ -435,14 +389,7 @@ impl DockerClient {
stdin_once: config.stdin_once,
env: config.env.unwrap_or_default(),
cmd: config.cmd.unwrap_or_default(),
healthcheck: config.healthcheck.map(|health| HealthConfig {
test: health.test.unwrap_or_default(),
interval: health.interval,
timeout: health.timeout,
retries: health.retries,
start_period: health.start_period,
start_interval: health.start_interval,
}),
healthcheck: config.healthcheck.map(convert_health_config),
args_escaped: config.args_escaped,
image: config.image,
volumes: config
@@ -657,74 +604,6 @@ fn convert_restart_policy(
}
}
fn convert_mount_type(
typ: bollard::secret::MountTypeEnum,
) -> MountTypeEnum {
match typ {
bollard::secret::MountTypeEnum::EMPTY => MountTypeEnum::Empty,
bollard::secret::MountTypeEnum::BIND => MountTypeEnum::Bind,
bollard::secret::MountTypeEnum::VOLUME => MountTypeEnum::Volume,
bollard::secret::MountTypeEnum::IMAGE => MountTypeEnum::Image,
bollard::secret::MountTypeEnum::TMPFS => MountTypeEnum::Tmpfs,
bollard::secret::MountTypeEnum::NPIPE => MountTypeEnum::Npipe,
bollard::secret::MountTypeEnum::CLUSTER => MountTypeEnum::Cluster,
}
}
fn convert_mount_point_type(
typ: bollard::secret::MountPointTypeEnum,
) -> MountTypeEnum {
match typ {
bollard::secret::MountPointTypeEnum::EMPTY => {
MountTypeEnum::Empty
}
bollard::secret::MountPointTypeEnum::BIND => MountTypeEnum::Bind,
bollard::secret::MountPointTypeEnum::VOLUME => {
MountTypeEnum::Volume
}
bollard::secret::MountPointTypeEnum::IMAGE => {
MountTypeEnum::Image
}
bollard::secret::MountPointTypeEnum::TMPFS => {
MountTypeEnum::Tmpfs
}
bollard::secret::MountPointTypeEnum::NPIPE => {
MountTypeEnum::Npipe
}
bollard::secret::MountPointTypeEnum::CLUSTER => {
MountTypeEnum::Cluster
}
}
}
fn convert_mount_propogation(
propogation: bollard::secret::MountBindOptionsPropagationEnum,
) -> MountBindOptionsPropagationEnum {
match propogation {
bollard::secret::MountBindOptionsPropagationEnum::EMPTY => {
MountBindOptionsPropagationEnum::Empty
}
bollard::secret::MountBindOptionsPropagationEnum::PRIVATE => {
MountBindOptionsPropagationEnum::Private
}
bollard::secret::MountBindOptionsPropagationEnum::RPRIVATE => {
MountBindOptionsPropagationEnum::Rprivate
}
bollard::secret::MountBindOptionsPropagationEnum::SHARED => {
MountBindOptionsPropagationEnum::Shared
}
bollard::secret::MountBindOptionsPropagationEnum::RSHARED => {
MountBindOptionsPropagationEnum::Rshared
}
bollard::secret::MountBindOptionsPropagationEnum::SLAVE => {
MountBindOptionsPropagationEnum::Slave
}
bollard::secret::MountBindOptionsPropagationEnum::RSLAVE => {
MountBindOptionsPropagationEnum::Rslave
}
}
}
fn convert_cgroupns_mode(
mode: bollard::secret::HostConfigCgroupnsModeEnum,
) -> HostConfigCgroupnsModeEnum {
+433 -5
View File
@@ -1,14 +1,26 @@
use anyhow::{Context, anyhow};
use bollard::Docker;
use command::{run_komodo_standard_command, run_shell_command};
use komodo_client::entities::{TerminationSignal, update::Log};
use komodo_client::entities::{
TerminationSignal,
docker::{task::*, *},
update::Log,
};
pub mod compose;
pub mod config;
pub mod stack;
pub mod stats;
mod containers;
mod images;
mod networks;
mod volumes;
mod container;
mod image;
mod network;
mod node;
mod secret;
mod service;
mod swarm;
mod task;
mod volume;
pub struct DockerClient {
docker: Docker,
@@ -78,3 +90,419 @@ pub fn stop_container_command(
.unwrap_or_default();
format!("docker stop{signal}{time} {container_name}")
}
fn convert_object_version(
version: bollard::models::ObjectVersion,
) -> ObjectVersion {
ObjectVersion {
index: version.index,
}
}
fn convert_driver(driver: bollard::models::Driver) -> Driver {
Driver {
name: driver.name,
options: driver.options,
}
}
fn convert_mount(mount: bollard::models::Mount) -> Mount {
Mount {
target: mount.target,
source: mount.source,
typ: mount.typ.map(convert_mount_type).unwrap_or_default(),
read_only: mount.read_only,
consistency: mount.consistency,
bind_options: mount.bind_options.map(|options| {
MountBindOptions {
propagation: options
.propagation
.map(convert_mount_propogation)
.unwrap_or_default(),
non_recursive: options.non_recursive,
create_mountpoint: options.create_mountpoint,
read_only_non_recursive: options.read_only_non_recursive,
read_only_force_recursive: options.read_only_force_recursive,
}
}),
volume_options: mount.volume_options.map(|options| {
MountVolumeOptions {
no_copy: options.no_copy,
labels: options.labels.unwrap_or_default(),
driver_config: options.driver_config.map(|config| {
MountVolumeOptionsDriverConfig {
name: config.name,
options: config.options.unwrap_or_default(),
}
}),
subpath: options.subpath,
}
}),
tmpfs_options: mount.tmpfs_options.map(|options| {
MountTmpfsOptions {
size_bytes: options.size_bytes,
mode: options.mode,
}
}),
}
}
fn convert_mount_type(
typ: bollard::secret::MountTypeEnum,
) -> MountTypeEnum {
match typ {
bollard::secret::MountTypeEnum::EMPTY => MountTypeEnum::Empty,
bollard::secret::MountTypeEnum::BIND => MountTypeEnum::Bind,
bollard::secret::MountTypeEnum::VOLUME => MountTypeEnum::Volume,
bollard::secret::MountTypeEnum::IMAGE => MountTypeEnum::Image,
bollard::secret::MountTypeEnum::TMPFS => MountTypeEnum::Tmpfs,
bollard::secret::MountTypeEnum::NPIPE => MountTypeEnum::Npipe,
bollard::secret::MountTypeEnum::CLUSTER => MountTypeEnum::Cluster,
}
}
fn convert_mount_propogation(
propogation: bollard::secret::MountBindOptionsPropagationEnum,
) -> MountBindOptionsPropagationEnum {
match propogation {
bollard::secret::MountBindOptionsPropagationEnum::EMPTY => {
MountBindOptionsPropagationEnum::Empty
}
bollard::secret::MountBindOptionsPropagationEnum::PRIVATE => {
MountBindOptionsPropagationEnum::Private
}
bollard::secret::MountBindOptionsPropagationEnum::RPRIVATE => {
MountBindOptionsPropagationEnum::Rprivate
}
bollard::secret::MountBindOptionsPropagationEnum::SHARED => {
MountBindOptionsPropagationEnum::Shared
}
bollard::secret::MountBindOptionsPropagationEnum::RSHARED => {
MountBindOptionsPropagationEnum::Rshared
}
bollard::secret::MountBindOptionsPropagationEnum::SLAVE => {
MountBindOptionsPropagationEnum::Slave
}
bollard::secret::MountBindOptionsPropagationEnum::RSLAVE => {
MountBindOptionsPropagationEnum::Rslave
}
}
}
fn convert_mount_point_type(
typ: bollard::secret::MountPointTypeEnum,
) -> MountTypeEnum {
match typ {
bollard::secret::MountPointTypeEnum::EMPTY => {
MountTypeEnum::Empty
}
bollard::secret::MountPointTypeEnum::BIND => MountTypeEnum::Bind,
bollard::secret::MountPointTypeEnum::VOLUME => {
MountTypeEnum::Volume
}
bollard::secret::MountPointTypeEnum::IMAGE => {
MountTypeEnum::Image
}
bollard::secret::MountPointTypeEnum::TMPFS => {
MountTypeEnum::Tmpfs
}
bollard::secret::MountPointTypeEnum::NPIPE => {
MountTypeEnum::Npipe
}
bollard::secret::MountPointTypeEnum::CLUSTER => {
MountTypeEnum::Cluster
}
}
}
fn convert_health_config(
config: bollard::models::HealthConfig,
) -> HealthConfig {
HealthConfig {
test: config.test.unwrap_or_default(),
interval: config.interval,
timeout: config.timeout,
retries: config.retries,
start_period: config.start_period,
start_interval: config.start_interval,
}
}
fn convert_resources_ulimits(
ulimit: bollard::models::ResourcesUlimits,
) -> ResourcesUlimits {
ResourcesUlimits {
name: ulimit.name,
soft: ulimit.soft,
hard: ulimit.hard,
}
}
fn convert_resource_object(
object: bollard::models::ResourceObject,
) -> ResourceObject {
ResourceObject {
nano_cpus: object.nano_cpus,
memory_bytes: object.memory_bytes,
generic_resources: object
.generic_resources
.map(convert_generic_resources),
}
}
fn convert_generic_resources(
resources: Vec<bollard::models::GenericResourcesInner>,
) -> Vec<GenericResourcesInner> {
resources
.into_iter()
.map(|resource| GenericResourcesInner {
named_resource_spec: resource.named_resource_spec.map(|spec| {
GenericResourcesInnerNamedResourceSpec {
kind: spec.kind,
value: spec.value,
}
}),
discrete_resource_spec: resource.discrete_resource_spec.map(
|spec| GenericResourcesInnerDiscreteResourceSpec {
kind: spec.kind,
value: spec.value,
},
),
})
.collect()
}
fn convert_platform(platform: bollard::models::Platform) -> Platform {
Platform {
architecture: platform.architecture,
os: platform.os,
}
}
fn convert_endpoint_spec_ports(
ports: Vec<bollard::models::EndpointPortConfig>,
) -> Vec<EndpointPortConfig> {
ports
.into_iter()
.map(|port| EndpointPortConfig {
name: port.name,
protocol: port.protocol.map(|protocol| match protocol {
bollard::secret::EndpointPortConfigProtocolEnum::EMPTY => EndpointPortConfigProtocolEnum::EMPTY,
bollard::secret::EndpointPortConfigProtocolEnum::TCP => EndpointPortConfigProtocolEnum::TCP,
bollard::secret::EndpointPortConfigProtocolEnum::UDP => EndpointPortConfigProtocolEnum::UDP,
bollard::secret::EndpointPortConfigProtocolEnum::SCTP => EndpointPortConfigProtocolEnum::SCTP,
}),
target_port: port.target_port,
published_port: port.published_port,
publish_mode: port.publish_mode.map(|protocol| match protocol {
bollard::secret::EndpointPortConfigPublishModeEnum::EMPTY => EndpointPortConfigPublishModeEnum::EMPTY,
bollard::secret::EndpointPortConfigPublishModeEnum::INGRESS => EndpointPortConfigPublishModeEnum::INGRESS,
bollard::secret::EndpointPortConfigPublishModeEnum::HOST => EndpointPortConfigPublishModeEnum::HOST,
}),
})
.collect()
}
fn convert_task_spec(spec: bollard::models::TaskSpec) -> TaskSpec {
TaskSpec {
plugin_spec: spec.plugin_spec.map(|spec| TaskSpecPluginSpec {
name: spec.name,
remote: spec.remote,
disabled: spec.disabled,
plugin_privilege: spec.plugin_privilege.map(|privileges| {
privileges
.into_iter()
.map(|privilege| PluginPrivilege {
name: privilege.name,
description: privilege.description,
value: privilege.value,
})
.collect()
}),
}),
container_spec: spec
.container_spec
.map(convert_task_spec_container_spec),
network_attachment_spec: spec.network_attachment_spec.map(
|spec| TaskSpecNetworkAttachmentSpec {
container_id: spec.container_id,
},
),
resources: spec.resources.map(|resources| TaskSpecResources {
limits: resources.limits.map(|limits| Limit {
nano_cpus: limits.nano_cpus,
memory_bytes: limits.memory_bytes,
pids: limits.pids,
}),
reservations: resources
.reservations
.map(convert_resource_object),
}),
restart_policy: spec.restart_policy.map(|policy| {
TaskSpecRestartPolicy {
condition: policy
.condition
.map(convert_task_spec_restart_policy_condition),
delay: policy.delay,
max_attempts: policy.max_attempts,
window: policy.window,
}
}),
placement: spec.placement.map(|placement| TaskSpecPlacement {
constraints: placement.constraints,
preferences: placement.preferences.map(|preferences| {
preferences
.into_iter()
.map(|preference| TaskSpecPlacementPreferences {
spread: preference.spread.map(|spread| {
TaskSpecPlacementSpread {
spread_descriptor: spread.spread_descriptor,
}
}),
})
.collect()
}),
max_replicas: placement.max_replicas,
platforms: placement.platforms.map(|platforms| {
platforms.into_iter().map(convert_platform).collect()
}),
}),
force_update: spec.force_update,
runtime: spec.runtime,
networks: spec.networks.map(|networks| {
networks
.into_iter()
.map(|network| NetworkAttachmentConfig {
target: network.target,
aliases: network.aliases,
driver_opts: network.driver_opts,
})
.collect()
}),
log_driver: spec.log_driver.map(|driver| TaskSpecLogDriver {
name: driver.name,
options: driver.options,
}),
}
}
fn convert_task_spec_container_spec(
spec: bollard::models::TaskSpecContainerSpec,
) -> TaskSpecContainerSpec {
TaskSpecContainerSpec {
image: spec.image,
labels: spec.labels,
command: spec.command,
args: spec.args,
hostname: spec.hostname,
env: spec.env,
dir: spec.dir,
user: spec.user,
groups: spec.groups,
privileges: spec.privileges.map(|privilege| {
TaskSpecContainerSpecPrivileges {
credential_spec: privilege.credential_spec.map(|spec| {
TaskSpecContainerSpecPrivilegesCredentialSpec {
config: spec.config,
file: spec.file,
registry: spec.registry,
}
}),
se_linux_context: privilege.se_linux_context.map(|context| {
TaskSpecContainerSpecPrivilegesSeLinuxContext {
disable: context.disable,
user: context.user,
role: context.role,
typ: context.typ,
level: context.level,
}
}),
seccomp: privilege.seccomp.map(|seccomp| {
TaskSpecContainerSpecPrivilegesSeccomp {
mode: seccomp.mode.map(|mode| match mode {
bollard::secret::TaskSpecContainerSpecPrivilegesSeccompModeEnum::EMPTY => TaskSpecContainerSpecPrivilegesSeccompModeEnum::EMPTY,
bollard::secret::TaskSpecContainerSpecPrivilegesSeccompModeEnum::DEFAULT => TaskSpecContainerSpecPrivilegesSeccompModeEnum::DEFAULT,
bollard::secret::TaskSpecContainerSpecPrivilegesSeccompModeEnum::UNCONFINED => TaskSpecContainerSpecPrivilegesSeccompModeEnum::UNCONFINED,
bollard::secret::TaskSpecContainerSpecPrivilegesSeccompModeEnum::CUSTOM => TaskSpecContainerSpecPrivilegesSeccompModeEnum::CUSTOM,
}),
profile: seccomp.profile,
}
}),
app_armor: privilege.app_armor.map(|app_armor| {
TaskSpecContainerSpecPrivilegesAppArmor {
mode: app_armor.mode.map(|mode| match mode {
bollard::secret::TaskSpecContainerSpecPrivilegesAppArmorModeEnum::EMPTY => TaskSpecContainerSpecPrivilegesAppArmorModeEnum::EMPTY,
bollard::secret::TaskSpecContainerSpecPrivilegesAppArmorModeEnum::DEFAULT => TaskSpecContainerSpecPrivilegesAppArmorModeEnum::DEFAULT,
bollard::secret::TaskSpecContainerSpecPrivilegesAppArmorModeEnum::DISABLED => TaskSpecContainerSpecPrivilegesAppArmorModeEnum::DISABLED,
}),
}
}),
no_new_privileges: privilege.no_new_privileges,
}
}),
tty: spec.tty,
open_stdin: spec.open_stdin,
read_only: spec.read_only,
mounts: spec.mounts.map(|mounts| mounts.into_iter().map(convert_mount).collect()),
stop_signal: spec.stop_signal,
stop_grace_period: spec.stop_grace_period,
health_check: spec.health_check.map(convert_health_config),
hosts: spec.hosts,
dns_config: spec.dns_config.map(|config| TaskSpecContainerSpecDnsConfig {
nameservers: config.nameservers,
search: config.search,
options: config.options,
}),
secrets: spec.secrets.map(|secrets| secrets.into_iter().map(|secret| TaskSpecContainerSpecSecrets {
file: secret.file.map(|file| TaskSpecContainerSpecFile {
name: file.name,
uid: file.uid,
gid: file.gid,
mode: file.mode,
}),
secret_id: secret.secret_id,
secret_name: secret.secret_name,
}).collect()),
oom_score_adj: spec.oom_score_adj,
configs: spec.configs.map(|configs| configs.into_iter().map(|config| TaskSpecContainerSpecConfigs {
file: config.file.map(|file| TaskSpecContainerSpecFile {
name: file.name,
uid: file.uid,
gid: file.gid,
mode: file.mode,
}),
config_id: config.config_id,
config_name: config.config_name,
}).collect()),
isolation: spec.isolation.map(|isolation| match isolation {
bollard::secret::TaskSpecContainerSpecIsolationEnum::DEFAULT => TaskSpecContainerSpecIsolationEnum::DEFAULT,
bollard::secret::TaskSpecContainerSpecIsolationEnum::PROCESS => TaskSpecContainerSpecIsolationEnum::PROCESS,
bollard::secret::TaskSpecContainerSpecIsolationEnum::HYPERV => TaskSpecContainerSpecIsolationEnum::HYPERV,
bollard::secret::TaskSpecContainerSpecIsolationEnum::EMPTY => TaskSpecContainerSpecIsolationEnum::EMPTY,
}),
init: spec.init,
sysctls: spec.sysctls,
capability_add: spec.capability_add,
capability_drop: spec.capability_drop,
ulimits: spec.ulimits.map(|ulimits| ulimits.into_iter().map(convert_resources_ulimits).collect()),
}
}
fn convert_task_spec_restart_policy_condition(
condition: bollard::secret::TaskSpecRestartPolicyConditionEnum,
) -> TaskSpecRestartPolicyConditionEnum {
match condition {
bollard::secret::TaskSpecRestartPolicyConditionEnum::EMPTY => TaskSpecRestartPolicyConditionEnum::EMPTY,
bollard::secret::TaskSpecRestartPolicyConditionEnum::NONE => TaskSpecRestartPolicyConditionEnum::NONE,
bollard::secret::TaskSpecRestartPolicyConditionEnum::ON_FAILURE => TaskSpecRestartPolicyConditionEnum::ON_FAILURE,
bollard::secret::TaskSpecRestartPolicyConditionEnum::ANY => TaskSpecRestartPolicyConditionEnum::ANY,
}
}
fn convert_tls_info(tls_info: bollard::models::TlsInfo) -> TlsInfo {
TlsInfo {
trust_root: tls_info.trust_root,
cert_issuer_subject: tls_info.cert_issuer_subject,
cert_issuer_public_key: tls_info.cert_issuer_public_key,
}
}
+180
View File
@@ -0,0 +1,180 @@
use anyhow::Context;
use bollard::query_parameters::ListNodesOptions;
use komodo_client::entities::docker::node::*;
use super::{
DockerClient, convert_platform, convert_resource_object,
};
impl DockerClient {
/// Lists swarm nodes
pub async fn list_swarm_nodes(
&self,
) -> anyhow::Result<Vec<SwarmNodeListItem>> {
let nodes = self
.docker
.list_nodes(Option::<ListNodesOptions>::None)
.await
.context("Failed to query for swarm node list")?
.into_iter()
.map(convert_node_list_item)
.collect();
Ok(nodes)
}
/// Inspect a swarm node
pub async fn inspect_swarm_node(
&self,
node_name: &str,
) -> anyhow::Result<SwarmNode> {
self
.docker
.inspect_node(node_name)
.await
.map(convert_node)
.with_context(|| {
format!(
"Failed to query for swarm node with name {node_name}"
)
})
}
}
fn convert_node_list_item(
node: bollard::models::Node,
) -> SwarmNodeListItem {
let (name, role, availability) = node
.spec
.map(|spec| {
(
spec.name,
spec.role.map(convert_role),
spec.availability.map(convert_availability),
)
})
.unwrap_or_default();
SwarmNodeListItem {
id: node.id,
name,
role,
availability,
hostname: node
.description
.and_then(|description| description.hostname),
state: node
.status
.and_then(|status| status.state.map(convert_state)),
created_at: node.created_at,
updated_at: node.updated_at,
}
}
fn convert_node(node: bollard::models::Node) -> SwarmNode {
SwarmNode {
id: node.id,
version: node.version.map(super::convert_object_version),
created_at: node.created_at,
updated_at: node.updated_at,
spec: node.spec.map(convert_node_spec),
description: node.description.map(|description| {
NodeDescription {
hostname: description.hostname,
platform: description.platform.map(convert_platform),
resources: description.resources.map(convert_resource_object),
engine: description.engine.map(|engine| EngineDescription {
engine_version: engine.engine_version,
labels: engine.labels,
plugins: engine.plugins.map(|plugins| {
plugins
.into_iter()
.map(|plugin| EngineDescriptionPlugins {
typ: plugin.typ,
name: plugin.name,
})
.collect()
}),
}),
tls_info: description.tls_info.map(super::convert_tls_info),
}
}),
status: node.status.map(|status| NodeStatus {
state: status.state.map(convert_state),
message: status.message,
addr: status.addr,
}),
manager_status: node.manager_status.map(|manager_status| {
ManagerStatus {
leader: manager_status.leader,
reachability: manager_status.reachability.map(
|reachability| match reachability {
bollard::secret::Reachability::UNKNOWN => {
NodeReachability::UNKNOWN
}
bollard::secret::Reachability::UNREACHABLE => {
NodeReachability::UNREACHABLE
}
bollard::secret::Reachability::REACHABLE => {
NodeReachability::REACHABLE
}
},
),
addr: manager_status.addr,
}
}),
}
}
fn convert_node_spec(spec: bollard::models::NodeSpec) -> NodeSpec {
NodeSpec {
name: spec.name,
labels: spec.labels,
role: spec.role.map(convert_role),
availability: spec.availability.map(convert_availability),
}
}
fn convert_role(
role: bollard::secret::NodeSpecRoleEnum,
) -> NodeSpecRoleEnum {
match role {
bollard::secret::NodeSpecRoleEnum::EMPTY => {
NodeSpecRoleEnum::EMPTY
}
bollard::secret::NodeSpecRoleEnum::WORKER => {
NodeSpecRoleEnum::WORKER
}
bollard::secret::NodeSpecRoleEnum::MANAGER => {
NodeSpecRoleEnum::MANAGER
}
}
}
fn convert_availability(
availability: bollard::secret::NodeSpecAvailabilityEnum,
) -> NodeSpecAvailabilityEnum {
match availability {
bollard::secret::NodeSpecAvailabilityEnum::EMPTY => {
NodeSpecAvailabilityEnum::EMPTY
}
bollard::secret::NodeSpecAvailabilityEnum::ACTIVE => {
NodeSpecAvailabilityEnum::ACTIVE
}
bollard::secret::NodeSpecAvailabilityEnum::PAUSE => {
NodeSpecAvailabilityEnum::PAUSE
}
bollard::secret::NodeSpecAvailabilityEnum::DRAIN => {
NodeSpecAvailabilityEnum::DRAIN
}
}
}
fn convert_state(state: bollard::secret::NodeState) -> NodeState {
match state {
bollard::secret::NodeState::UNKNOWN => NodeState::UNKNOWN,
bollard::secret::NodeState::DOWN => NodeState::DOWN,
bollard::secret::NodeState::READY => NodeState::READY,
bollard::secret::NodeState::DISCONNECTED => {
NodeState::DISCONNECTED
}
}
}
+78
View File
@@ -0,0 +1,78 @@
use anyhow::Context;
use bollard::query_parameters::ListSecretsOptions;
use komodo_client::entities::docker::secret::{
SecretSpec, SwarmSecret, SwarmSecretListItem,
};
use super::DockerClient;
impl DockerClient {
pub async fn list_swarm_secrets(
&self,
) -> anyhow::Result<Vec<SwarmSecretListItem>> {
let secrets = self
.docker
.list_secrets(Option::<ListSecretsOptions>::None)
.await
.context("Failed to query for swarm secret list")?
.into_iter()
.map(convert_secret_list_item)
.collect();
Ok(secrets)
}
pub async fn inspect_swarm_secret(
&self,
secret_id: &str,
) -> anyhow::Result<SwarmSecret> {
self
.docker
.inspect_secret(secret_id)
.await
.map(convert_secret)
.with_context(|| {
format!(
"Failed to query for swarm secret with id {secret_id}"
)
})
}
}
fn convert_secret_list_item(
secret: bollard::models::Secret,
) -> SwarmSecretListItem {
let (name, driver, templating) = secret
.spec
.map(|spec| {
(
spec.name,
spec.driver.map(|driver| driver.name),
spec.templating.map(|driver| driver.name),
)
})
.unwrap_or_default();
SwarmSecretListItem {
id: secret.id,
name,
driver,
templating,
created_at: secret.created_at,
updated_at: secret.updated_at,
}
}
fn convert_secret(secret: bollard::models::Secret) -> SwarmSecret {
SwarmSecret {
id: secret.id,
version: secret.version.map(super::convert_object_version),
created_at: secret.created_at,
updated_at: secret.updated_at,
spec: secret.spec.map(|spec| SecretSpec {
name: spec.name,
labels: spec.labels,
data: spec.data,
driver: spec.driver.map(super::convert_driver),
templating: spec.templating.map(super::convert_driver),
}),
}
}
+227
View File
@@ -0,0 +1,227 @@
use anyhow::Context;
use bollard::query_parameters::{
InspectServiceOptions, ListServicesOptions,
};
use komodo_client::entities::docker::{
NetworkAttachmentConfig, service::*,
};
use super::*;
impl DockerClient {
/// List swarm services
pub async fn list_swarm_services(
&self,
) -> anyhow::Result<Vec<SwarmServiceListItem>> {
let services = self
.docker
.list_services(Option::<ListServicesOptions>::None)
.await
.context("Failed to query for swarm service list")?
.into_iter()
.map(convert_service_list_item)
.collect();
Ok(services)
}
pub async fn inspect_swarm_service(
&self,
service_name: &str,
) -> anyhow::Result<SwarmService> {
self
.docker
.inspect_service(
service_name,
Some(InspectServiceOptions {
insert_defaults: true,
}),
)
.await
.map(convert_service)
.with_context(|| {
format!(
"Failed to query for swarm service with name {service_name}"
)
})
}
}
fn convert_service_list_item(
service: bollard::models::Service,
) -> SwarmServiceListItem {
let (name, (image, restart, runtime), replicas) = service
.spec
.map(|spec| {
(
spec.name,
spec
.task_template
.map(|template| {
(
template.container_spec.and_then(|spec| spec.image),
template.restart_policy.and_then(|policy| {
policy
.condition
.map(convert_task_spec_restart_policy_condition)
}),
template.runtime,
)
})
.unwrap_or_default(),
spec.mode.and_then(|mode| {
mode.replicated.and_then(|replicated| replicated.replicas)
}),
)
})
.unwrap_or_default();
SwarmServiceListItem {
id: service.id,
name,
replicas,
image,
restart,
runtime,
created_at: service.created_at,
updated_at: service.updated_at,
}
}
fn convert_service(
service: bollard::models::Service,
) -> SwarmService {
SwarmService {
id: service.id,
version: service.version.map(convert_object_version),
created_at: service.created_at,
updated_at: service.updated_at,
spec: service.spec.map(|spec| ServiceSpec {
name: spec.name,
labels: spec.labels,
task_template: spec.task_template.map(convert_task_spec),
mode: spec.mode.map(|mode| ServiceSpecMode {
replicated: mode.replicated.map(|replicated| ServiceSpecModeReplicated {
replicas: replicated.replicas,
}),
// global: mode.global,
replicated_job: mode.replicated_job.map(|job| ServiceSpecModeReplicatedJob {
max_concurrent: job.max_concurrent,
total_completions: job.total_completions,
}),
// global_job: mode.global_job,
}),
update_config: spec.update_config.map(|config| {
ServiceSpecUpdateConfig {
parallelism: config.parallelism,
delay: config.delay,
failure_action: config.failure_action.map(|action| match action {
bollard::secret::ServiceSpecUpdateConfigFailureActionEnum::EMPTY => ServiceSpecUpdateConfigFailureActionEnum::EMPTY,
bollard::secret::ServiceSpecUpdateConfigFailureActionEnum::CONTINUE => ServiceSpecUpdateConfigFailureActionEnum::CONTINUE,
bollard::secret::ServiceSpecUpdateConfigFailureActionEnum::PAUSE => ServiceSpecUpdateConfigFailureActionEnum::PAUSE,
bollard::secret::ServiceSpecUpdateConfigFailureActionEnum::ROLLBACK => ServiceSpecUpdateConfigFailureActionEnum::ROLLBACK,
}),
monitor: config.monitor,
max_failure_ratio: config.max_failure_ratio,
order: config.order.map(|order| match order {
bollard::secret::ServiceSpecUpdateConfigOrderEnum::EMPTY => ServiceSpecUpdateConfigOrderEnum::EMPTY,
bollard::secret::ServiceSpecUpdateConfigOrderEnum::STOP_FIRST => ServiceSpecUpdateConfigOrderEnum::STOP_FIRST,
bollard::secret::ServiceSpecUpdateConfigOrderEnum::START_FIRST => ServiceSpecUpdateConfigOrderEnum::START_FIRST,
}),
}
}),
rollback_config: spec.rollback_config.map(|config| {
ServiceSpecRollbackConfig {
parallelism: config.parallelism,
delay: config.delay,
failure_action: config.failure_action.map(|action| match action {
bollard::secret::ServiceSpecRollbackConfigFailureActionEnum::EMPTY => ServiceSpecRollbackConfigFailureActionEnum::EMPTY,
bollard::secret::ServiceSpecRollbackConfigFailureActionEnum::CONTINUE => ServiceSpecRollbackConfigFailureActionEnum::CONTINUE,
bollard::secret::ServiceSpecRollbackConfigFailureActionEnum::PAUSE => ServiceSpecRollbackConfigFailureActionEnum::PAUSE,
}),
monitor: config.monitor,
max_failure_ratio: config.max_failure_ratio,
order: config.order.map(|order| match order {
bollard::secret::ServiceSpecRollbackConfigOrderEnum::EMPTY => ServiceSpecRollbackConfigOrderEnum::EMPTY,
bollard::secret::ServiceSpecRollbackConfigOrderEnum::STOP_FIRST => ServiceSpecRollbackConfigOrderEnum::STOP_FIRST,
bollard::secret::ServiceSpecRollbackConfigOrderEnum::START_FIRST => ServiceSpecRollbackConfigOrderEnum::START_FIRST,
}),
}
}),
networks: spec.networks.map(|networks| {
networks
.into_iter()
.map(|network| NetworkAttachmentConfig {
target: network.target,
aliases: network.aliases,
driver_opts: network.driver_opts,
})
.collect()
}),
endpoint_spec: spec.endpoint_spec.map(convert_endpoint_spec),
}),
endpoint: service.endpoint.map(|endpoint| ServiceEndpoint {
spec: endpoint.spec.map(convert_endpoint_spec),
ports: endpoint.ports.map(convert_endpoint_spec_ports),
virtual_ips: endpoint.virtual_ips.map(|ips| {
ips
.into_iter()
.map(|ip| ServiceEndpointVirtualIps {
network_id: ip.network_id,
addr: ip.addr,
})
.collect()
}),
}),
update_status: service.update_status.map(|status| {
ServiceUpdateStatus {
state: status.state.map(convert_state),
started_at: status.started_at,
completed_at: status.completed_at,
message: status.message,
}
}),
service_status: service.service_status.map(|status| {
ServiceServiceStatus {
running_tasks: status.running_tasks,
desired_tasks: status.desired_tasks,
completed_tasks: status.completed_tasks,
}
}),
job_status: service.job_status.map(|status| ServiceJobStatus {
job_iteration: status.job_iteration.map(convert_object_version),
last_execution: status.last_execution,
}),
}
}
fn convert_endpoint_spec(
spec: bollard::models::EndpointSpec,
) -> EndpointSpec {
EndpointSpec {
mode: spec.mode.map(|mode| match mode {
bollard::secret::EndpointSpecModeEnum::EMPTY => {
EndpointSpecModeEnum::EMPTY
}
bollard::secret::EndpointSpecModeEnum::VIP => {
EndpointSpecModeEnum::VIP
}
bollard::secret::EndpointSpecModeEnum::DNSRR => {
EndpointSpecModeEnum::DNSRR
}
}),
ports: spec.ports.map(convert_endpoint_spec_ports),
}
}
fn convert_state(
state: bollard::secret::ServiceUpdateStatusStateEnum,
) -> ServiceUpdateStatusStateEnum {
match state {
bollard::secret::ServiceUpdateStatusStateEnum::EMPTY => ServiceUpdateStatusStateEnum::EMPTY,
bollard::secret::ServiceUpdateStatusStateEnum::UPDATING => ServiceUpdateStatusStateEnum::UPDATING,
bollard::secret::ServiceUpdateStatusStateEnum::PAUSED => ServiceUpdateStatusStateEnum::PAUSED,
bollard::secret::ServiceUpdateStatusStateEnum::COMPLETED => ServiceUpdateStatusStateEnum::COMPLETED,
bollard::secret::ServiceUpdateStatusStateEnum::ROLLBACK_STARTED => ServiceUpdateStatusStateEnum::ROLLBACK_STARTED,
bollard::secret::ServiceUpdateStatusStateEnum::ROLLBACK_PAUSED => ServiceUpdateStatusStateEnum::ROLLBACK_PAUSED,
bollard::secret::ServiceUpdateStatusStateEnum::ROLLBACK_COMPLETED => ServiceUpdateStatusStateEnum::ROLLBACK_COMPLETED,
}
}
+93
View File
@@ -0,0 +1,93 @@
use anyhow::{Context, anyhow};
use command::run_komodo_standard_command;
use komodo_client::entities::docker::stack::{
SwarmStackListItem, SwarmStackLists, SwarmStackServiceListItem,
SwarmStackTaskListItem,
};
pub async fn inspect_swarm_stack(
name: String,
) -> anyhow::Result<SwarmStackLists> {
let (services, tasks) = tokio::try_join!(
list_swarm_stack_services(&name),
list_swarm_stack_tasks(&name),
)?;
Ok(SwarmStackLists {
name,
services,
tasks,
})
}
pub async fn list_swarm_stacks()
-> anyhow::Result<Vec<SwarmStackListItem>> {
let res = run_komodo_standard_command(
"List Swarm Stacks",
None,
"docker stack ls --format json",
)
.await;
if !res.success {
return Err(anyhow!("{}", res.combined()).context(
"Failed to list swarm stacks using 'docker stack ls'",
));
}
// The output is in JSONL, need to convert to standard JSON vec.
serde_json::from_str(&format!(
"[{}]",
res.stdout.trim().replace('\n', ",")
))
.context("Failed to parse 'docker stack ls' response from json")
}
pub async fn list_swarm_stack_services(
stack: &str,
) -> anyhow::Result<Vec<SwarmStackServiceListItem>> {
let res = run_komodo_standard_command(
"List Swarm Stack Services",
None,
format!("docker stack services --format json {stack}"),
)
.await;
if !res.success {
return Err(anyhow!("{}", res.combined()).context(
"Failed to list swarm stacks using 'docker stack services'",
));
}
// The output is in JSONL, need to convert to standard JSON vec.
serde_json::from_str(&format!(
"[{}]",
res.stdout.trim().replace('\n', ",")
))
.context(
"Failed to parse 'docker stack services' response from json",
)
}
pub async fn list_swarm_stack_tasks(
stack: &str,
) -> anyhow::Result<Vec<SwarmStackTaskListItem>> {
let res = run_komodo_standard_command(
"List Swarm Stack Tasks",
None,
format!("docker stack ps --format json {stack}"),
)
.await;
if !res.success {
return Err(anyhow!("{}", res.combined()).context(
"Failed to list swarm stacks using 'docker stack ps'",
));
}
// The output is in JSONL, need to convert to standard JSON vec.
serde_json::from_str(&format!(
"[{}]",
res.stdout.trim().replace('\n', ",")
))
.context("Failed to parse 'docker stack ps' response from json")
}
+96
View File
@@ -0,0 +1,96 @@
use anyhow::Context;
use komodo_client::entities::docker::swarm::{
JoinTokens, SwarmInspectInfo, SwarmSpec, SwarmSpecCaConfig,
SwarmSpecCaConfigExternalCas,
SwarmSpecCaConfigExternalCasProtocolEnum, SwarmSpecDispatcher,
SwarmSpecEncryptionConfig, SwarmSpecOrchestration, SwarmSpecRaft,
SwarmSpecTaskDefaults, SwarmSpecTaskDefaultsLogDriver,
};
use super::DockerClient;
impl DockerClient {
/// Inspect swarm info
pub async fn inspect_swarm(
&self,
) -> anyhow::Result<SwarmInspectInfo> {
self
.docker
.inspect_swarm()
.await
.map(convert_swarm_info)
.context("Failed to query for swarm info")
}
}
fn convert_swarm_info(
swarm: bollard::models::Swarm,
) -> SwarmInspectInfo {
SwarmInspectInfo {
id: swarm.id,
version: swarm.version.map(super::convert_object_version),
created_at: swarm.created_at,
updated_at: swarm.updated_at,
spec: swarm.spec.map(|spec| SwarmSpec {
name: spec.name,
labels: spec.labels,
orchestration: spec.orchestration.map(|orchestration| {
SwarmSpecOrchestration {
task_history_retention_limit: orchestration
.task_history_retention_limit,
}
}),
raft: spec.raft.map(|raft| SwarmSpecRaft {
snapshot_interval: raft.snapshot_interval,
keep_old_snapshots: raft.keep_old_snapshots,
log_entries_for_slow_followers: raft
.log_entries_for_slow_followers,
election_tick: raft.election_tick,
heartbeat_tick: raft.heartbeat_tick,
}),
dispatcher: spec.dispatcher.map(|dispatcher| {
SwarmSpecDispatcher {
heartbeat_period: dispatcher.heartbeat_period,
}
}),
ca_config: spec.ca_config.map(|config| SwarmSpecCaConfig {
node_cert_expiry: config.node_cert_expiry,
external_cas: config.external_cas.map(|cas| {
cas
.into_iter()
.map(|cas| SwarmSpecCaConfigExternalCas {
protocol: cas.protocol.map(|protocol| match protocol {
bollard::secret::SwarmSpecCaConfigExternalCasProtocolEnum::EMPTY => SwarmSpecCaConfigExternalCasProtocolEnum::EMPTY,
bollard::secret::SwarmSpecCaConfigExternalCasProtocolEnum::CFSSL => SwarmSpecCaConfigExternalCasProtocolEnum::CFSSL,
}),
url: cas.url,
options: cas.options,
ca_cert: cas.ca_cert,
})
.collect()
}),
signing_ca_cert: config.signing_ca_cert,
signing_ca_key: config.signing_ca_key,
force_rotate: config.force_rotate,
}),
encryption_config: spec.encryption_config.map(|config| SwarmSpecEncryptionConfig {
auto_lock_managers: config.auto_lock_managers,
}),
task_defaults: spec.task_defaults.map(|defaults| SwarmSpecTaskDefaults {
log_driver: defaults.log_driver.map(|driver| SwarmSpecTaskDefaultsLogDriver {
name: driver.name,
options: driver.options,
}),
}),
}),
tls_info: swarm.tls_info.map(super::convert_tls_info),
root_rotation_in_progress: swarm.root_rotation_in_progress,
data_path_port: swarm.data_path_port,
default_addr_pool: swarm.default_addr_pool,
subnet_size: swarm.subnet_size,
join_tokens: swarm.join_tokens.map(|tokens| JoinTokens {
worker: tokens.worker,
manager: tokens.manager,
}),
}
}
+120
View File
@@ -0,0 +1,120 @@
use anyhow::Context;
use bollard::query_parameters::ListTasksOptions;
use komodo_client::entities::docker::task::*;
use super::*;
impl DockerClient {
pub async fn list_swarm_tasks(
&self,
) -> anyhow::Result<Vec<SwarmTaskListItem>> {
let tasks = self
.docker
.list_tasks(Option::<ListTasksOptions>::None)
.await
.context("Failed to query for swarm tasks list")?
.into_iter()
.map(convert_task_list_item)
.collect();
Ok(tasks)
}
pub async fn inspect_swarm_task(
&self,
task_id: &str,
) -> anyhow::Result<SwarmTask> {
self
.docker
.inspect_task(task_id)
.await
.map(convert_task)
.with_context(|| {
format!("Failed to query for swarm task with id {task_id}")
})
}
}
fn convert_task_list_item(
task: bollard::models::Task,
) -> SwarmTaskListItem {
let (container_id, state) = task
.status
.map(|status| {
(
status
.container_status
.and_then(|status| status.container_id),
status.state.map(convert_task_state),
)
})
.unwrap_or_default();
SwarmTaskListItem {
id: task.id,
name: task.name,
node_id: task.node_id,
service_id: task.service_id,
container_id,
state,
desired_state: task.desired_state.map(convert_task_state),
created_at: task.created_at,
updated_at: task.updated_at,
}
}
fn convert_task(task: bollard::models::Task) -> SwarmTask {
SwarmTask {
id: task.id,
version: task.version.map(convert_object_version),
created_at: task.created_at,
updated_at: task.updated_at,
name: task.name,
labels: task.labels,
spec: task.spec.map(convert_task_spec),
service_id: task.service_id,
slot: task.slot,
node_id: task.node_id,
assigned_generic_resources: task
.assigned_generic_resources
.map(convert_generic_resources),
status: task.status.map(|status| TaskStatus {
timestamp: status.timestamp,
state: status.state.map(convert_task_state),
message: status.message,
err: status.err,
container_status: status.container_status.map(|status| {
ContainerStatus {
container_id: status.container_id,
pid: status.pid,
exit_code: status.exit_code,
}
}),
port_status: status.port_status.map(|status| PortStatus {
ports: status.ports.map(convert_endpoint_spec_ports),
}),
}),
desired_state: task.desired_state.map(convert_task_state),
job_iteration: task.job_iteration.map(convert_object_version),
}
}
fn convert_task_state(
state: bollard::models::TaskState,
) -> TaskState {
match state {
bollard::secret::TaskState::NEW => TaskState::NEW,
bollard::secret::TaskState::ALLOCATED => TaskState::ALLOCATED,
bollard::secret::TaskState::PENDING => TaskState::PENDING,
bollard::secret::TaskState::ASSIGNED => TaskState::ASSIGNED,
bollard::secret::TaskState::ACCEPTED => TaskState::ACCEPTED,
bollard::secret::TaskState::PREPARING => TaskState::PREPARING,
bollard::secret::TaskState::READY => TaskState::READY,
bollard::secret::TaskState::STARTING => TaskState::STARTING,
bollard::secret::TaskState::RUNNING => TaskState::RUNNING,
bollard::secret::TaskState::COMPLETE => TaskState::COMPLETE,
bollard::secret::TaskState::SHUTDOWN => TaskState::SHUTDOWN,
bollard::secret::TaskState::FAILED => TaskState::FAILED,
bollard::secret::TaskState::REJECTED => TaskState::REJECTED,
bollard::secret::TaskState::REMOVE => TaskState::REMOVE,
bollard::secret::TaskState::ORPHANED => TaskState::ORPHANED,
}
}
@@ -78,9 +78,7 @@ impl DockerClient {
cluster_volume: volume.cluster_volume.map(|volume| {
ClusterVolume {
id: volume.id,
version: volume.version.map(|version| ObjectVersion {
index: version.index,
}),
version: volume.version.map(super::convert_object_version),
created_at: volume.created_at,
updated_at: volume.updated_at,
spec: volume.spec.map(|spec| ClusterVolumeSpec {
@@ -100,8 +98,8 @@ impl DockerClient {
bollard::secret::ClusterVolumeSpecAccessModeSharingEnum::ALL => ClusterVolumeSpecAccessModeSharingEnum::All,
}).unwrap_or_default(),
secrets: mode.secrets.unwrap_or_default().into_iter().map(|secret| ClusterVolumeSpecAccessModeSecrets {
key: secret.key,
secret: secret.secret,
key: secret.key,
secret: secret.secret,
}).collect(),
accessibility_requirements: mode
.accessibility_requirements.map(|req| ClusterVolumeSpecAccessModeAccessibilityRequirements {
+2
View File
@@ -18,6 +18,7 @@ mod repo;
mod schedule;
mod server;
mod stack;
mod swarm;
mod sync;
mod tag;
mod terminal;
@@ -42,6 +43,7 @@ pub use repo::*;
pub use schedule::*;
pub use server::*;
pub use stack::*;
pub use swarm::*;
pub use sync::*;
pub use tag::*;
pub use terminal::*;
+487
View File
@@ -0,0 +1,487 @@
use derive_empty_traits::EmptyTraits;
use resolver_api::Resolve;
use serde::{Deserialize, Serialize};
use typeshare::typeshare;
use crate::entities::{
SearchCombinator, U64,
docker::{
config::{SwarmConfig, SwarmConfigListItem},
node::{SwarmNode, SwarmNodeListItem},
secret::{SwarmSecret, SwarmSecretListItem},
service::{SwarmService, SwarmServiceListItem},
stack::{SwarmStackListItem, SwarmStackLists},
swarm::SwarmInspectInfo,
task::{SwarmTask, SwarmTaskListItem},
},
swarm::{Swarm, SwarmActionState, SwarmListItem, SwarmQuery},
update::Log,
};
use super::KomodoReadRequest;
//
/// Get a specific swarm. Response: [Swarm].
#[typeshare]
#[derive(
Serialize, Deserialize, Debug, Clone, Resolve, EmptyTraits,
)]
#[empty_traits(KomodoReadRequest)]
#[response(Swarm)]
#[error(serror::Error)]
pub struct GetSwarm {
/// Id or name
#[serde(alias = "id", alias = "name")]
pub swarm: String,
}
#[typeshare]
pub type GetSwarmResponse = Swarm;
//
/// List Swarms matching optional query. Response: [ListSwarmsResponse].
#[typeshare]
#[derive(
Serialize, Deserialize, Debug, Clone, Default, Resolve, EmptyTraits,
)]
#[empty_traits(KomodoReadRequest)]
#[response(ListSwarmsResponse)]
#[error(serror::Error)]
pub struct ListSwarms {
/// Optional structured query to filter Swarms.
#[serde(default)]
pub query: SwarmQuery,
}
#[typeshare]
pub type ListSwarmsResponse = Vec<SwarmListItem>;
//
/// List Swarms matching optional query. Response: [ListFullSwarmsResponse].
#[typeshare]
#[derive(
Serialize, Deserialize, Debug, Clone, Default, Resolve, EmptyTraits,
)]
#[empty_traits(KomodoReadRequest)]
#[response(ListFullSwarmsResponse)]
#[error(serror::Error)]
pub struct ListFullSwarms {
/// optional structured query to filter swarms.
#[serde(default)]
pub query: SwarmQuery,
}
#[typeshare]
pub type ListFullSwarmsResponse = Vec<Swarm>;
//
/// Get current action state for the swarm. Response: [SwarmActionState].
#[typeshare]
#[derive(
Serialize, Deserialize, Debug, Clone, Resolve, EmptyTraits,
)]
#[empty_traits(KomodoReadRequest)]
#[response(GetSwarmActionStateResponse)]
#[error(serror::Error)]
pub struct GetSwarmActionState {
/// Id or name
#[serde(alias = "id", alias = "name")]
pub swarm: String,
}
#[typeshare]
pub type GetSwarmActionStateResponse = SwarmActionState;
//
/// Gets a summary of data relating to all swarms.
/// Response: [GetSwarmsSummaryResponse].
#[typeshare]
#[derive(
Serialize, Deserialize, Debug, Clone, Resolve, EmptyTraits,
)]
#[empty_traits(KomodoReadRequest)]
#[response(GetSwarmsSummaryResponse)]
#[error(serror::Error)]
pub struct GetSwarmsSummary {}
/// Response for [GetSwarmsSummary]
#[typeshare]
#[derive(Serialize, Deserialize, Debug, Clone, Default)]
pub struct GetSwarmsSummaryResponse {
/// The total number of Swarms
pub total: u32,
/// The number of Swarms with Healthy state.
pub healthy: u32,
/// The number of Swarms with Unhealthy state
pub unhealthy: u32,
/// The number of Swarms with Unknown state
pub unknown: u32,
}
//
/// Inspect information about the swarm.
/// Response: [SwarmInspectInfo].
#[typeshare]
#[derive(
Serialize, Deserialize, Debug, Clone, Resolve, EmptyTraits,
)]
#[empty_traits(KomodoReadRequest)]
#[response(InspectSwarmResponse)]
#[error(serror::Error)]
pub struct InspectSwarm {
/// Id or name
#[serde(alias = "id", alias = "name")]
pub swarm: String,
}
#[typeshare]
pub type InspectSwarmResponse = SwarmInspectInfo;
//
/// List nodes part of the target Swarm.
/// Response: [ListSwarmNodesResponse].
#[typeshare]
#[derive(
Serialize, Deserialize, Debug, Clone, Resolve, EmptyTraits,
)]
#[empty_traits(KomodoReadRequest)]
#[response(ListSwarmNodesResponse)]
#[error(serror::Error)]
pub struct ListSwarmNodes {
/// Id or name
#[serde(alias = "id", alias = "name")]
pub swarm: String,
}
#[typeshare]
pub type ListSwarmNodesResponse = Vec<SwarmNodeListItem>;
//
/// Inspect a Swarm node.
/// Response: [SwarmNode].
#[typeshare]
#[derive(
Serialize, Deserialize, Debug, Clone, Resolve, EmptyTraits,
)]
#[empty_traits(KomodoReadRequest)]
#[response(InspectSwarmNodeResponse)]
#[error(serror::Error)]
pub struct InspectSwarmNode {
/// Id or name
#[serde(alias = "id", alias = "name")]
pub swarm: String,
/// Node id
pub node: String,
}
#[typeshare]
pub type InspectSwarmNodeResponse = SwarmNode;
//
/// List services on the target Swarm.
/// Response: [ListSwarmServicesResponse].
#[typeshare]
#[derive(
Serialize, Deserialize, Debug, Clone, Resolve, EmptyTraits,
)]
#[empty_traits(KomodoReadRequest)]
#[response(ListSwarmServicesResponse)]
#[error(serror::Error)]
pub struct ListSwarmServices {
/// Id or name
#[serde(alias = "id", alias = "name")]
pub swarm: String,
}
#[typeshare]
pub type ListSwarmServicesResponse = Vec<SwarmServiceListItem>;
//
/// Inspect a Swarm service.
/// Response: [SwarmService].
#[typeshare]
#[derive(
Serialize, Deserialize, Debug, Clone, Resolve, EmptyTraits,
)]
#[empty_traits(KomodoReadRequest)]
#[response(InspectSwarmServiceResponse)]
#[error(serror::Error)]
pub struct InspectSwarmService {
/// Id or name
#[serde(alias = "id", alias = "name")]
pub swarm: String,
/// Service id
pub service: String,
}
#[typeshare]
pub type InspectSwarmServiceResponse = SwarmService;
//
/// Get a swarm service's logs. Response: [GetSwarmServiceLogResponse].
///
/// Note. This call will hit the underlying server directly for most up to date log.
#[typeshare]
#[derive(
Serialize, Deserialize, Debug, Clone, Resolve, EmptyTraits,
)]
#[empty_traits(KomodoReadRequest)]
#[response(GetSwarmServiceLogResponse)]
#[error(serror::Error)]
pub struct GetSwarmServiceLog {
/// Id or name
#[serde(alias = "id", alias = "name")]
pub swarm: String,
/// Select the swarm service to get logs for.
pub service: String,
/// The number of lines of the log tail to include.
/// Default: 100.
/// Max: 5000.
#[serde(default = "default_tail")]
pub tail: U64,
/// Enable `--timestamps`
#[serde(default)]
pub timestamps: bool,
/// Enable `--no-task-ids`
#[serde(default)]
pub no_task_ids: bool,
/// Enable `--no-resolve`
#[serde(default)]
pub no_resolve: bool,
/// Enable `--details`
#[serde(default)]
pub details: bool,
}
fn default_tail() -> u64 {
50
}
#[typeshare]
pub type GetSwarmServiceLogResponse = Log;
//
/// Search the swarm service log's tail using `grep`. All lines go to stdout.
/// Response: [SearchSwarmServiceLogResponse].
///
/// Note. This call will hit the underlying server directly for most up to date log.
#[typeshare]
#[derive(
Serialize, Deserialize, Debug, Clone, Resolve, EmptyTraits,
)]
#[empty_traits(KomodoReadRequest)]
#[response(SearchSwarmServiceLogResponse)]
#[error(serror::Error)]
pub struct SearchSwarmServiceLog {
/// Id or name
#[serde(alias = "id", alias = "name")]
pub swarm: String,
/// Select the swarm service to get logs for.
pub service: String,
/// The terms to search for.
pub terms: Vec<String>,
/// When searching for multiple terms, can use `AND` or `OR` combinator.
///
/// - `AND`: Only include lines with **all** terms present in that line.
/// - `OR`: Include lines that have one or more matches in the terms.
#[serde(default)]
pub combinator: SearchCombinator,
/// Invert the results, ie return all lines that DON'T match the terms / combinator.
#[serde(default)]
pub invert: bool,
/// Enable `--timestamps`
#[serde(default)]
pub timestamps: bool,
/// Enable `--no-task-ids`
#[serde(default)]
pub no_task_ids: bool,
/// Enable `--no-resolve`
#[serde(default)]
pub no_resolve: bool,
/// Enable `--details`
#[serde(default)]
pub details: bool,
}
#[typeshare]
pub type SearchSwarmServiceLogResponse = Log;
//
/// List tasks on the target Swarm.
/// Response: [ListSwarmTasksResponse].
#[typeshare]
#[derive(
Serialize, Deserialize, Debug, Clone, Resolve, EmptyTraits,
)]
#[empty_traits(KomodoReadRequest)]
#[response(ListSwarmTasksResponse)]
#[error(serror::Error)]
pub struct ListSwarmTasks {
/// Id or name
#[serde(alias = "id", alias = "name")]
pub swarm: String,
}
#[typeshare]
pub type ListSwarmTasksResponse = Vec<SwarmTaskListItem>;
//
/// Inspect a Swarm task.
/// Response: [SwarmTask].
#[typeshare]
#[derive(
Serialize, Deserialize, Debug, Clone, Resolve, EmptyTraits,
)]
#[empty_traits(KomodoReadRequest)]
#[response(InspectSwarmTaskResponse)]
#[error(serror::Error)]
pub struct InspectSwarmTask {
/// Id or name
#[serde(alias = "id", alias = "name")]
pub swarm: String,
/// Task id
pub task: String,
}
#[typeshare]
pub type InspectSwarmTaskResponse = SwarmTask;
//
/// List secrets on the target Swarm.
/// Response: [ListSwarmSecretsResponse].
#[typeshare]
#[derive(
Serialize, Deserialize, Debug, Clone, Resolve, EmptyTraits,
)]
#[empty_traits(KomodoReadRequest)]
#[response(ListSwarmSecretsResponse)]
#[error(serror::Error)]
pub struct ListSwarmSecrets {
/// Id or name
#[serde(alias = "id", alias = "name")]
pub swarm: String,
}
#[typeshare]
pub type ListSwarmSecretsResponse = Vec<SwarmSecretListItem>;
//
/// Inspect a Swarm secret.
/// Response: [SwarmSecret].
#[typeshare]
#[derive(
Serialize, Deserialize, Debug, Clone, Resolve, EmptyTraits,
)]
#[empty_traits(KomodoReadRequest)]
#[response(InspectSwarmSecretResponse)]
#[error(serror::Error)]
pub struct InspectSwarmSecret {
/// Id or name
#[serde(alias = "id", alias = "name")]
pub swarm: String,
/// Secret id
pub secret: String,
}
#[typeshare]
pub type InspectSwarmSecretResponse = SwarmSecret;
//
/// List configs on the target Swarm.
/// Response: [ListSwarmConfigsResponse].
#[typeshare]
#[derive(
Serialize, Deserialize, Debug, Clone, Resolve, EmptyTraits,
)]
#[empty_traits(KomodoReadRequest)]
#[response(ListSwarmConfigsResponse)]
#[error(serror::Error)]
pub struct ListSwarmConfigs {
/// Id or name
#[serde(alias = "id", alias = "name")]
pub swarm: String,
}
#[typeshare]
pub type ListSwarmConfigsResponse = Vec<SwarmConfigListItem>;
//
/// Inspect a config on the target Swarm.
/// Response: [InspectSwarmConfigResponse].
#[typeshare]
#[derive(
Serialize, Deserialize, Debug, Clone, Resolve, EmptyTraits,
)]
#[empty_traits(KomodoReadRequest)]
#[response(InspectSwarmConfigResponse)]
#[error(serror::Error)]
pub struct InspectSwarmConfig {
/// Id or name
#[serde(alias = "id", alias = "name")]
pub swarm: String,
/// Swarm config ID or Name
pub config: String,
}
#[typeshare]
pub type InspectSwarmConfigResponse = Vec<SwarmConfig>;
//
/// List stacks on the target Swarm.
/// Response: [ListSwarmStacksResponse].
#[typeshare]
#[derive(
Serialize, Deserialize, Debug, Clone, Resolve, EmptyTraits,
)]
#[empty_traits(KomodoReadRequest)]
#[response(ListSwarmStacksResponse)]
#[error(serror::Error)]
pub struct ListSwarmStacks {
/// Id or name
#[serde(alias = "id", alias = "name")]
pub swarm: String,
}
#[typeshare]
pub type ListSwarmStacksResponse = Vec<SwarmStackListItem>;
//
/// Inspect a stack on the target Swarm.
/// Response: [SwarmStackLists].
#[typeshare]
#[derive(
Serialize, Deserialize, Debug, Clone, Resolve, EmptyTraits,
)]
#[empty_traits(KomodoReadRequest)]
#[response(InspectSwarmStackResponse)]
#[error(serror::Error)]
pub struct InspectSwarmStack {
/// Id or name
#[serde(alias = "id", alias = "name")]
pub swarm: String,
/// Swarm stack name
pub stack: String,
}
#[typeshare]
pub type InspectSwarmStackResponse = SwarmStackLists;
+2
View File
@@ -13,6 +13,7 @@ mod repo;
mod resource;
mod server;
mod stack;
mod swarm;
mod sync;
mod tags;
mod terminal;
@@ -35,6 +36,7 @@ pub use repo::*;
pub use resource::*;
pub use server::*;
pub use stack::*;
pub use swarm::*;
pub use sync::*;
pub use tags::*;
pub use terminal::*;
+108
View File
@@ -0,0 +1,108 @@
use derive_empty_traits::EmptyTraits;
use resolver_api::Resolve;
use serde::{Deserialize, Serialize};
use typeshare::typeshare;
use crate::entities::{
swarm::{_PartialSwarmConfig, Swarm},
update::Update,
};
use super::KomodoWriteRequest;
//
/// Create a Swarm. Response: [Swarm].
#[typeshare]
#[derive(
Serialize, Deserialize, Debug, Clone, Resolve, EmptyTraits,
)]
#[empty_traits(KomodoWriteRequest)]
#[response(Swarm)]
#[error(serror::Error)]
pub struct CreateSwarm {
/// The name given to newly created swarm.
pub name: String,
/// Optional partial config to initialize the swarm with.
#[serde(default)]
pub config: _PartialSwarmConfig,
}
//
/// Creates a new Swarm with given `name` and the configuration
/// of the Swarm at the given `id`. Response: [Swarm].
#[typeshare]
#[derive(
Serialize, Deserialize, Debug, Clone, Resolve, EmptyTraits,
)]
#[empty_traits(KomodoWriteRequest)]
#[response(Swarm)]
#[error(serror::Error)]
pub struct CopySwarm {
/// The name of the new swarm.
pub name: String,
/// The id of the swarm to copy.
pub id: String,
}
//
/// Deletes the Swarm at the given id, and returns the deleted Swarm.
/// Response: [Swarm]
#[typeshare]
#[derive(
Serialize, Deserialize, Debug, Clone, Resolve, EmptyTraits,
)]
#[empty_traits(KomodoWriteRequest)]
#[response(Swarm)]
#[error(serror::Error)]
pub struct DeleteSwarm {
/// The id or name of the swarm to delete.
pub id: String,
}
//
/// Update the Swarm at the given id, and return the updated Swarm.
/// Response: [Swarm].
///
/// Note. If the attached server for the Swarm changes,
/// the Swarm will be deleted / cleaned up on the old server.
///
/// Note. This method updates only the fields which are set in the [_PartialSwarmConfig],
/// effectively merging diffs into the final document.
/// This is helpful when multiple users are using
/// the same resources concurrently by ensuring no unintentional
/// field changes occur from out of date local state.
#[typeshare]
#[derive(
Serialize, Deserialize, Debug, Clone, Resolve, EmptyTraits,
)]
#[empty_traits(KomodoWriteRequest)]
#[response(Swarm)]
#[error(serror::Error)]
pub struct UpdateSwarm {
/// The id of the swarm to update.
pub id: String,
/// The partial config update to apply.
pub config: _PartialSwarmConfig,
}
//
/// Rename the Swarm at id to the given name.
/// Response: [Update].
#[typeshare]
#[derive(
Serialize, Deserialize, Debug, Clone, Resolve, EmptyTraits,
)]
#[empty_traits(KomodoWriteRequest)]
#[response(Update)]
#[error(serror::Error)]
pub struct RenameSwarm {
/// The id or name of the Swarm to rename.
pub id: String,
/// The new name.
pub name: String,
}
+8 -1
View File
@@ -2,13 +2,20 @@ use crate::entities::{
action::ActionActionState, build::BuildActionState,
deployment::DeploymentActionState, procedure::ProcedureActionState,
repo::RepoActionState, server::ServerActionState,
stack::StackActionState, sync::ResourceSyncActionState,
stack::StackActionState, swarm::SwarmActionState,
sync::ResourceSyncActionState,
};
pub trait Busy {
fn busy(&self) -> bool;
}
impl Busy for SwarmActionState {
fn busy(&self) -> bool {
false
}
}
impl Busy for ServerActionState {
fn busy(&self) -> bool {
self.pruning_containers
+78 -1
View File
@@ -166,6 +166,8 @@ pub struct Env {
/// Override `local_auth`
pub komodo_local_auth: Option<bool>,
/// Override `min_password_length`
pub komodo_min_password_length: Option<u16>,
/// Override `init_admin_username`
pub komodo_init_admin_username: Option<String>,
/// Override `init_admin_username` from file
@@ -218,6 +220,18 @@ pub struct Env {
/// Override `github_oauth.secret` from file
pub komodo_github_oauth_secret_file: Option<PathBuf>,
/// Override `auth_rate_limit_disabled`
pub komodo_auth_rate_limit_disabled: Option<bool>,
/// Override `auth_rate_limit_max_attempts`
pub komodo_auth_rate_limit_max_attempts: Option<u16>,
/// Override `auth_rate_limit_window_seconds`
pub komodo_auth_rate_limit_window_seconds: Option<u64>,
/// Override `cors_allowed_origins`
pub komodo_cors_allowed_origins: Option<Vec<String>>,
/// Override `cors_allow_credentials`
pub komodo_cors_allow_credentials: Option<bool>,
/// Override `database.uri`
#[serde(alias = "komodo_mongo_uri")]
pub komodo_database_uri: Option<String>,
@@ -405,14 +419,20 @@ pub struct CoreConfig {
// ================
// = Auth / Login =
// ================
/// enable login with local auth
/// Enable login with local auth
#[serde(default)]
pub local_auth: bool,
/// Configure a minimum password length.
/// Default: 1
#[serde(default = "default_min_password_length")]
pub min_password_length: u16,
/// Upon fresh launch, initalize an Admin user with this username.
/// If this is not provided, no initial user will be created.
#[serde(skip_serializing_if = "Option::is_none")]
pub init_admin_username: Option<String>,
/// Upon fresh launch, initalize an Admin user with this password.
/// Default: `changeme`
#[serde(default = "default_init_admin_password")]
@@ -511,6 +531,35 @@ pub struct CoreConfig {
#[serde(default)]
pub github_oauth: OauthCredentials,
// =================
// = Rate Limiting =
// =================
/// Disable the auth rate limiter.
#[serde(default)]
pub auth_rate_limit_disabled: bool,
/// Set the max allowed attempts per IP
#[serde(default = "default_auth_rate_limit_max_attempts")]
pub auth_rate_limit_max_attempts: u16,
#[serde(default = "default_auth_rate_limit_window_seconds")]
pub auth_rate_limit_window_seconds: u64,
// =======
// = CORS =
// =======
/// List of CORS allowed origins.
/// If empty, allows all origins (`*`).
/// Production setups should configure this explicitly.
/// Example: `["https://komodo.example.com", "https://app.example.com"]`.
#[serde(default)]
pub cors_allowed_origins: Vec<String>,
/// Tell CORS to allow credentials in requests.
/// Used if needed for authentication proxy.
#[serde(default)]
pub cors_allow_credentials: bool,
// ============
// = Webhooks =
// ============
@@ -681,10 +730,22 @@ fn default_jwt_ttl() -> Timelength {
Timelength::OneDay
}
fn default_min_password_length() -> u16 {
1
}
fn default_init_admin_password() -> String {
String::from("changeme")
}
fn default_auth_rate_limit_max_attempts() -> u16 {
5
}
fn default_auth_rate_limit_window_seconds() -> u64 {
15
}
fn default_sync_directory() -> PathBuf {
PathBuf::from("/syncs")
}
@@ -739,6 +800,7 @@ impl Default for CoreConfig {
frontend_path: default_frontend_path(),
database: Default::default(),
local_auth: Default::default(),
min_password_length: default_min_password_length(),
init_admin_username: Default::default(),
init_admin_password: default_init_admin_password(),
transparent_mode: Default::default(),
@@ -757,6 +819,13 @@ impl Default for CoreConfig {
oidc_additional_audiences: Default::default(),
google_oauth: Default::default(),
github_oauth: Default::default(),
auth_rate_limit_disabled: Default::default(),
auth_rate_limit_max_attempts:
default_auth_rate_limit_max_attempts(),
auth_rate_limit_window_seconds:
default_auth_rate_limit_window_seconds(),
cors_allowed_origins: Default::default(),
cors_allow_credentials: Default::default(),
webhook_secret: Default::default(),
webhook_base_url: Default::default(),
logging: Default::default(),
@@ -824,6 +893,7 @@ impl CoreConfig {
disable_non_admin_create: config.disable_non_admin_create,
lock_login_credentials_for: config.lock_login_credentials_for,
local_auth: config.local_auth,
min_password_length: config.min_password_length,
init_admin_username: config
.init_admin_username
.map(|u| empty_or_redacted(&u)),
@@ -853,6 +923,13 @@ impl CoreConfig {
id: empty_or_redacted(&config.github_oauth.id),
secret: empty_or_redacted(&config.github_oauth.id),
},
auth_rate_limit_disabled: config.auth_rate_limit_disabled,
auth_rate_limit_max_attempts: config
.auth_rate_limit_max_attempts,
auth_rate_limit_window_seconds: config
.auth_rate_limit_window_seconds,
cors_allowed_origins: config.cors_allowed_origins,
cors_allow_credentials: config.cors_allow_credentials,
webhook_secret: empty_or_redacted(&config.webhook_secret),
webhook_base_url: config.webhook_base_url,
database: config.database.sanitized(),
@@ -0,0 +1,78 @@
use std::collections::HashMap;
use serde::{Deserialize, Serialize};
use typeshare::typeshare;
use super::*;
/// Swarm config list item.
/// Returned by `docker config ls --format json`
#[typeshare]
#[derive(
Debug, Clone, Default, PartialEq, Serialize, Deserialize,
)]
pub struct SwarmConfigListItem {
/// User-defined name of the config.
#[serde(rename = "Name")]
pub name: Option<String>,
#[serde(rename = "ID")]
pub id: Option<String>,
#[serde(rename = "CreatedAt")]
pub created_at: Option<String>,
#[serde(rename = "UpdatedAt")]
pub updated_at: Option<String>,
/// User-defined key/value metadata, formatted as a string:
/// `"lab1=val1,lab2=val2"`.
#[serde(rename = "Labels")]
pub labels: Option<String>,
}
/// Swarm config details.
#[typeshare]
#[derive(
Debug, Clone, Default, PartialEq, Serialize, Deserialize,
)]
pub struct SwarmConfig {
#[serde(rename = "ID")]
pub id: Option<String>,
#[serde(rename = "Version")]
pub version: Option<ObjectVersion>,
#[serde(rename = "CreatedAt")]
pub created_at: Option<String>,
#[serde(rename = "UpdatedAt")]
pub updated_at: Option<String>,
#[serde(rename = "Spec")]
pub spec: Option<ConfigSpec>,
}
#[typeshare]
#[derive(
Debug, Clone, Default, PartialEq, Serialize, Deserialize,
)]
pub struct ConfigSpec {
/// User-defined name of the config.
#[serde(rename = "Name")]
pub name: Option<String>,
/// User-defined key/value metadata.
#[serde(rename = "Labels")]
pub labels: Option<HashMap<String, String>>,
/// Data is the data to store as a config, formatted as a Base64-url-safe-encoded ([RFC 4648](https://tools.ietf.org/html/rfc4648#section-5)) string.
/// It must be empty if the Driver field is set, in which case the data is loaded from an external secret store.
/// The maximum allowed size is 500KB, as defined in [MaxSecretSize](https://pkg.go.dev/github.com/moby/swarmkit/v2@v2.0.0-20250103191802-8c1959736554/api/validation#MaxSecretSize).
#[serde(rename = "Data")]
pub data: Option<String>,
/// Templating driver, if applicable Templating controls whether and how to evaluate the config payload as a template. If no driver is set, no templating is used.
#[serde(rename = "Templating")]
pub templating: Option<Driver>,
}
+6 -197
View File
@@ -7,7 +7,10 @@ use typeshare::typeshare;
use crate::entities::{I64, Usize};
use super::{ContainerConfig, GraphDriverData, PortBinding};
use super::{
ContainerConfig, GraphDriverData, Mount, MountTypeEnum,
PortBinding, ResourcesUlimits,
};
/// Container summary returned by container list apis.
#[typeshare]
@@ -15,7 +18,7 @@ use super::{ContainerConfig, GraphDriverData, PortBinding};
Debug, Clone, Default, PartialEq, Serialize, Deserialize,
)]
pub struct ContainerListItem {
/// The Server which holds the container.
/// The Server which hosts the container.
#[serde(skip_serializing_if = "Option::is_none")]
pub server_id: Option<String>,
/// The first name in Names, not including the initial '/'
@@ -543,7 +546,7 @@ pub struct HostConfig {
/// Specification for mounts to be added to the container.
#[serde(default, rename = "Mounts")]
pub mounts: Vec<ContainerMount>,
pub mounts: Vec<Mount>,
/// Initial console size, as an `[height, width]` array.
#[serde(default, rename = "ConsoleSize")]
@@ -728,24 +731,6 @@ pub struct DeviceRequest {
pub options: HashMap<String, String>,
}
#[typeshare]
#[derive(
Debug, Clone, Default, PartialEq, Serialize, Deserialize,
)]
pub struct ResourcesUlimits {
/// Name of ulimit
#[serde(rename = "Name")]
pub name: Option<String>,
/// Soft limit
#[serde(rename = "Soft")]
pub soft: Option<I64>,
/// Hard limit
#[serde(rename = "Hard")]
pub hard: Option<I64>,
}
#[typeshare]
#[derive(
Debug,
@@ -826,182 +811,6 @@ pub enum RestartPolicyNameEnum {
OnFailure,
}
#[typeshare]
#[derive(
Debug, Clone, Default, PartialEq, Serialize, Deserialize,
)]
pub struct ContainerMount {
/// Container path.
#[serde(rename = "Target")]
pub target: Option<String>,
/// Mount source (e.g. a volume name, a host path).
#[serde(rename = "Source")]
pub source: Option<String>,
/// The mount type. Available types: - `bind` Mounts a file or directory from the host into the container. Must exist prior to creating the container. - `volume` Creates a volume with the given name and options (or uses a pre-existing volume with the same name and options). These are **not** removed when the container is removed. - `tmpfs` Create a tmpfs with the given options. The mount source cannot be specified for tmpfs. - `npipe` Mounts a named pipe from the host into the container. Must exist prior to creating the container. - `cluster` a Swarm cluster volume
#[serde(default, rename = "Type")]
pub typ: MountTypeEnum,
/// Whether the mount should be read-only.
#[serde(rename = "ReadOnly")]
pub read_only: Option<bool>,
/// The consistency requirement for the mount: `default`, `consistent`, `cached`, or `delegated`.
#[serde(rename = "Consistency")]
pub consistency: Option<String>,
#[serde(rename = "BindOptions")]
pub bind_options: Option<MountBindOptions>,
#[serde(rename = "VolumeOptions")]
pub volume_options: Option<MountVolumeOptions>,
#[serde(rename = "TmpfsOptions")]
pub tmpfs_options: Option<MountTmpfsOptions>,
}
#[typeshare]
#[derive(
Debug,
Clone,
Copy,
PartialEq,
PartialOrd,
Serialize,
Deserialize,
Eq,
Ord,
Default,
)]
pub enum MountTypeEnum {
#[default]
#[serde(rename = "")]
Empty,
#[serde(rename = "bind")]
Bind,
#[serde(rename = "volume")]
Volume,
#[serde(rename = "image")]
Image,
#[serde(rename = "tmpfs")]
Tmpfs,
#[serde(rename = "npipe")]
Npipe,
#[serde(rename = "cluster")]
Cluster,
}
/// Optional configuration for the `bind` type.
#[typeshare]
#[derive(
Debug, Clone, Default, PartialEq, Serialize, Deserialize,
)]
pub struct MountBindOptions {
/// A propagation mode with the value `[r]private`, `[r]shared`, or `[r]slave`.
#[serde(default, rename = "Propagation")]
pub propagation: MountBindOptionsPropagationEnum,
/// Disable recursive bind mount.
#[serde(rename = "NonRecursive")]
pub non_recursive: Option<bool>,
/// Create mount point on host if missing
#[serde(rename = "CreateMountpoint")]
pub create_mountpoint: Option<bool>,
/// Make the mount non-recursively read-only, but still leave the mount recursive (unless NonRecursive is set to `true` in conjunction). Addded in v1.44, before that version all read-only mounts were non-recursive by default. To match the previous behaviour this will default to `true` for clients on versions prior to v1.44.
#[serde(rename = "ReadOnlyNonRecursive")]
pub read_only_non_recursive: Option<bool>,
/// Raise an error if the mount cannot be made recursively read-only.
#[serde(rename = "ReadOnlyForceRecursive")]
pub read_only_force_recursive: Option<bool>,
}
#[typeshare]
#[derive(
Debug,
Clone,
Copy,
PartialEq,
PartialOrd,
Serialize,
Deserialize,
Eq,
Ord,
Default,
)]
pub enum MountBindOptionsPropagationEnum {
#[default]
#[serde(rename = "")]
Empty,
#[serde(rename = "private")]
Private,
#[serde(rename = "rprivate")]
Rprivate,
#[serde(rename = "shared")]
Shared,
#[serde(rename = "rshared")]
Rshared,
#[serde(rename = "slave")]
Slave,
#[serde(rename = "rslave")]
Rslave,
}
/// Optional configuration for the `volume` type.
#[typeshare]
#[derive(
Debug, Clone, Default, PartialEq, Serialize, Deserialize,
)]
pub struct MountVolumeOptions {
/// Populate volume with data from the target.
#[serde(rename = "NoCopy")]
pub no_copy: Option<bool>,
/// User-defined key/value metadata.
#[serde(default, rename = "Labels")]
pub labels: HashMap<String, String>,
#[serde(rename = "DriverConfig")]
pub driver_config: Option<MountVolumeOptionsDriverConfig>,
/// Source path inside the volume. Must be relative without any back traversals.
#[serde(rename = "Subpath")]
pub subpath: Option<String>,
}
/// Map of driver specific options
#[typeshare]
#[derive(
Debug, Clone, Default, PartialEq, Serialize, Deserialize,
)]
pub struct MountVolumeOptionsDriverConfig {
/// Name of the driver to use to create the volume.
#[serde(rename = "Name")]
pub name: Option<String>,
/// key/value map of driver specific options.
#[serde(default, rename = "Options")]
pub options: HashMap<String, String>,
}
/// Optional configuration for the `tmpfs` type.
#[typeshare]
#[derive(
Debug, Clone, Default, PartialEq, Serialize, Deserialize,
)]
pub struct MountTmpfsOptions {
/// The size for the tmpfs mount in bytes.
#[serde(rename = "SizeBytes")]
pub size_bytes: Option<I64>,
/// The permission mode for the tmpfs mount in an integer.
#[serde(rename = "Mode")]
pub mode: Option<I64>,
}
#[typeshare]
#[derive(
Debug,
+447 -1
View File
@@ -3,14 +3,56 @@ use std::collections::HashMap;
use serde::{Deserialize, Serialize};
use typeshare::typeshare;
use super::I64;
use crate::entities::{
docker::{
config::SwarmConfigListItem, container::ContainerListItem,
image::ImageListItem, network::NetworkListItem,
node::SwarmNodeListItem, secret::SwarmSecretListItem,
service::SwarmServiceListItem, stack::SwarmStackListItem,
task::SwarmTaskListItem, volume::VolumeListItem,
},
stack::ComposeProject,
};
use super::{I64, U64};
pub mod config;
pub mod container;
pub mod image;
pub mod network;
pub mod node;
pub mod secret;
pub mod service;
pub mod stack;
pub mod stats;
pub mod swarm;
pub mod task;
pub mod volume;
/// Swarm lists available from a manager node.
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct SwarmLists {
pub nodes: Vec<SwarmNodeListItem>,
pub services: Vec<SwarmServiceListItem>,
pub tasks: Vec<SwarmTaskListItem>,
pub secrets: Vec<SwarmSecretListItem>,
pub configs: Vec<SwarmConfigListItem>,
pub stacks: Vec<SwarmStackListItem>,
}
/// Standard docker lists available from a Server.
#[typeshare]
#[derive(
Debug, Clone, Default, PartialEq, Serialize, Deserialize,
)]
pub struct DockerLists {
pub containers: Vec<ContainerListItem>,
pub networks: Vec<NetworkListItem>,
pub images: Vec<ImageListItem>,
pub volumes: Vec<VolumeListItem>,
pub projects: Vec<ComposeProject>,
}
/// PortBinding represents a binding between a host IP address and a host port.
#[typeshare]
#[derive(
@@ -176,3 +218,407 @@ pub struct HealthConfig {
#[serde(rename = "StartInterval")]
pub start_interval: Option<I64>,
}
/// The version number of the object such as node, service, etc. This is needed to avoid conflicting writes. The client must send the version number along with the modified specification when updating these objects. This approach ensures safe concurrency and determinism in that the change on the object may not be applied if the version number has changed from the last read. In other words, if two update requests specify the same base version, only one of the requests can succeed. As a result, two separate update requests that happen at the same time will not unintentionally overwrite each other.
#[typeshare]
#[derive(
Debug, Clone, Default, PartialEq, Serialize, Deserialize,
)]
pub struct ObjectVersion {
#[serde(rename = "Index")]
pub index: Option<U64>,
}
/// Driver represents a driver (network, logging, secrets).
#[typeshare]
#[derive(
Debug, Clone, Default, PartialEq, Serialize, Deserialize,
)]
pub struct Driver {
/// Name of the driver.
#[serde(rename = "Name")]
pub name: String,
/// Key/value map of driver-specific options.
#[serde(rename = "Options")]
pub options: Option<HashMap<String, String>>,
}
#[typeshare]
#[derive(
Debug, Clone, Default, PartialEq, Serialize, Deserialize,
)]
pub struct Mount {
/// Container path.
#[serde(rename = "Target")]
pub target: Option<String>,
/// Mount source (e.g. a volume name, a host path).
#[serde(rename = "Source")]
pub source: Option<String>,
/// The mount type. Available types:
/// - `bind` Mounts a file or directory from the host into the container. Must exist prior to creating the container.
/// - `volume` Creates a volume with the given name and options (or uses a pre-existing volume with the same name and options). These are **not** removed when the container is removed.
/// - `tmpfs` Create a tmpfs with the given options. The mount source cannot be specified for tmpfs. - `npipe` Mounts a named pipe from the host into the container. Must exist prior to creating the container.
/// - `cluster` a Swarm cluster volume
#[serde(default, rename = "Type")]
pub typ: MountTypeEnum,
/// Whether the mount should be read-only.
#[serde(rename = "ReadOnly")]
pub read_only: Option<bool>,
/// The consistency requirement for the mount: `default`, `consistent`, `cached`, or `delegated`.
#[serde(rename = "Consistency")]
pub consistency: Option<String>,
#[serde(rename = "BindOptions")]
pub bind_options: Option<MountBindOptions>,
#[serde(rename = "VolumeOptions")]
pub volume_options: Option<MountVolumeOptions>,
#[serde(rename = "TmpfsOptions")]
pub tmpfs_options: Option<MountTmpfsOptions>,
}
#[typeshare]
#[derive(
Debug,
Clone,
Copy,
PartialEq,
PartialOrd,
Serialize,
Deserialize,
Eq,
Ord,
Default,
)]
pub enum MountTypeEnum {
#[default]
#[serde(rename = "")]
Empty,
#[serde(rename = "bind")]
Bind,
#[serde(rename = "volume")]
Volume,
#[serde(rename = "image")]
Image,
#[serde(rename = "tmpfs")]
Tmpfs,
#[serde(rename = "npipe")]
Npipe,
#[serde(rename = "cluster")]
Cluster,
}
/// Optional configuration for the `bind` type.
#[typeshare]
#[derive(
Debug, Clone, Default, PartialEq, Serialize, Deserialize,
)]
pub struct MountBindOptions {
/// A propagation mode with the value `[r]private`, `[r]shared`, or `[r]slave`.
#[serde(default, rename = "Propagation")]
pub propagation: MountBindOptionsPropagationEnum,
/// Disable recursive bind mount.
#[serde(rename = "NonRecursive")]
pub non_recursive: Option<bool>,
/// Create mount point on host if missing
#[serde(rename = "CreateMountpoint")]
pub create_mountpoint: Option<bool>,
/// Make the mount non-recursively read-only, but still leave the mount recursive (unless NonRecursive is set to `true` in conjunction). Addded in v1.44, before that version all read-only mounts were non-recursive by default. To match the previous behaviour this will default to `true` for clients on versions prior to v1.44.
#[serde(rename = "ReadOnlyNonRecursive")]
pub read_only_non_recursive: Option<bool>,
/// Raise an error if the mount cannot be made recursively read-only.
#[serde(rename = "ReadOnlyForceRecursive")]
pub read_only_force_recursive: Option<bool>,
}
#[typeshare]
#[derive(
Debug,
Clone,
Copy,
PartialEq,
PartialOrd,
Serialize,
Deserialize,
Eq,
Ord,
Default,
)]
pub enum MountBindOptionsPropagationEnum {
#[default]
#[serde(rename = "")]
Empty,
#[serde(rename = "private")]
Private,
#[serde(rename = "rprivate")]
Rprivate,
#[serde(rename = "shared")]
Shared,
#[serde(rename = "rshared")]
Rshared,
#[serde(rename = "slave")]
Slave,
#[serde(rename = "rslave")]
Rslave,
}
/// Optional configuration for the `volume` type.
#[typeshare]
#[derive(
Debug, Clone, Default, PartialEq, Serialize, Deserialize,
)]
pub struct MountVolumeOptions {
/// Populate volume with data from the target.
#[serde(rename = "NoCopy")]
pub no_copy: Option<bool>,
/// User-defined key/value metadata.
#[serde(default, rename = "Labels")]
pub labels: HashMap<String, String>,
#[serde(rename = "DriverConfig")]
pub driver_config: Option<MountVolumeOptionsDriverConfig>,
/// Source path inside the volume. Must be relative without any back traversals.
#[serde(rename = "Subpath")]
pub subpath: Option<String>,
}
/// Map of driver specific options
#[typeshare]
#[derive(
Debug, Clone, Default, PartialEq, Serialize, Deserialize,
)]
pub struct MountVolumeOptionsDriverConfig {
/// Name of the driver to use to create the volume.
#[serde(rename = "Name")]
pub name: Option<String>,
/// key/value map of driver specific options.
#[serde(default, rename = "Options")]
pub options: HashMap<String, String>,
}
/// Optional configuration for the `tmpfs` type.
#[typeshare]
#[derive(
Debug, Clone, Default, PartialEq, Serialize, Deserialize,
)]
pub struct MountTmpfsOptions {
/// The size for the tmpfs mount in bytes.
#[serde(rename = "SizeBytes")]
pub size_bytes: Option<I64>,
/// The permission mode for the tmpfs mount in an integer.
#[serde(rename = "Mode")]
pub mode: Option<I64>,
}
#[typeshare]
#[derive(
Debug, Clone, Default, PartialEq, Serialize, Deserialize,
)]
pub struct ResourcesUlimits {
/// Name of ulimit
#[serde(rename = "Name")]
pub name: Option<String>,
/// Soft limit
#[serde(rename = "Soft")]
pub soft: Option<I64>,
/// Hard limit
#[serde(rename = "Hard")]
pub hard: Option<I64>,
}
#[typeshare]
#[derive(
Debug, Clone, Default, PartialEq, Serialize, Deserialize,
)]
pub struct ResourceObject {
#[serde(rename = "NanoCPUs")]
pub nano_cpus: Option<I64>,
#[serde(rename = "MemoryBytes")]
pub memory_bytes: Option<I64>,
#[serde(rename = "GenericResources")]
pub generic_resources: Option<GenericResources>,
}
/// User-defined resources can be either Integer resources (e.g, `SSD=3`) or String resources (e.g, `GPU=UUID1`).
#[typeshare]
pub type GenericResources = Vec<GenericResourcesInner>;
#[typeshare]
#[derive(
Debug, Clone, Default, PartialEq, Serialize, Deserialize,
)]
pub struct GenericResourcesInner {
#[serde(rename = "NamedResourceSpec")]
pub named_resource_spec:
Option<GenericResourcesInnerNamedResourceSpec>,
#[serde(rename = "DiscreteResourceSpec")]
pub discrete_resource_spec:
Option<GenericResourcesInnerDiscreteResourceSpec>,
}
#[typeshare]
#[derive(
Debug, Clone, Default, PartialEq, Serialize, Deserialize,
)]
pub struct GenericResourcesInnerDiscreteResourceSpec {
#[serde(rename = "Kind")]
pub kind: Option<String>,
#[serde(rename = "Value")]
pub value: Option<I64>,
}
#[typeshare]
#[derive(
Debug, Clone, Default, PartialEq, Serialize, Deserialize,
)]
pub struct GenericResourcesInnerNamedResourceSpec {
#[serde(rename = "Kind")]
pub kind: Option<String>,
#[serde(rename = "Value")]
pub value: Option<String>,
}
#[typeshare]
#[derive(
Debug, Clone, Default, PartialEq, Serialize, Deserialize,
)]
pub struct Platform {
/// Architecture represents the hardware architecture (for example, `x86_64`).
#[serde(rename = "Architecture")]
pub architecture: Option<String>,
/// OS represents the Operating System (for example, `linux` or `windows`).
#[serde(rename = "OS")]
pub os: Option<String>,
}
/// Specifies how a service should be attached to a particular network.
#[typeshare]
#[derive(
Debug, Clone, Default, PartialEq, Serialize, Deserialize,
)]
pub struct NetworkAttachmentConfig {
/// The target network for attachment. Must be a network name or ID.
#[serde(rename = "Target")]
pub target: Option<String>,
/// Discoverable alternate names for the service on this network.
#[serde(rename = "Aliases")]
pub aliases: Option<Vec<String>>,
/// Driver attachment options for the network target.
#[serde(rename = "DriverOpts")]
pub driver_opts: Option<HashMap<String, String>>,
}
#[typeshare]
#[derive(
Debug, Clone, Default, PartialEq, Serialize, Deserialize,
)]
pub struct EndpointPortConfig {
#[serde(rename = "Name")]
pub name: Option<String>,
#[serde(rename = "Protocol")]
pub protocol: Option<EndpointPortConfigProtocolEnum>,
/// The port inside the container.
#[serde(rename = "TargetPort")]
pub target_port: Option<I64>,
/// The port on the swarm hosts.
#[serde(rename = "PublishedPort")]
pub published_port: Option<I64>,
/// The mode in which port is published. <p><br /></p> - \"ingress\" makes the target port accessible on every node, regardless of whether there is a task for the service running on that node or not. - \"host\" bypasses the routing mesh and publish the port directly on the swarm node where that service is running.
#[serde(rename = "PublishMode")]
pub publish_mode: Option<EndpointPortConfigPublishModeEnum>,
}
#[typeshare]
#[derive(
Debug,
Clone,
Copy,
PartialEq,
Eq,
PartialOrd,
Ord,
Default,
Serialize,
Deserialize,
)]
pub enum EndpointPortConfigProtocolEnum {
#[default]
#[serde(rename = "")]
EMPTY,
#[serde(rename = "tcp")]
TCP,
#[serde(rename = "udp")]
UDP,
#[serde(rename = "sctp")]
SCTP,
}
#[typeshare]
#[derive(
Debug,
Clone,
Copy,
PartialEq,
Eq,
PartialOrd,
Ord,
Default,
Serialize,
Deserialize,
)]
pub enum EndpointPortConfigPublishModeEnum {
#[default]
#[serde(rename = "")]
EMPTY,
#[serde(rename = "ingress")]
INGRESS,
#[serde(rename = "host")]
HOST,
}
/// Information about the issuer of leaf TLS certificates and the trusted root CA certificate.
#[typeshare]
#[derive(
Debug, Clone, Default, PartialEq, Serialize, Deserialize,
)]
pub struct TlsInfo {
/// The root CA certificate(s) that are used to validate leaf TLS certificates.
#[serde(rename = "TrustRoot")]
pub trust_root: Option<String>,
/// The base64-url-safe-encoded raw subject bytes of the issuer.
#[serde(rename = "CertIssuerSubject")]
pub cert_issuer_subject: Option<String>,
/// The base64-url-safe-encoded raw public key bytes of the issuer.
#[serde(rename = "CertIssuerPublicKey")]
pub cert_issuer_public_key: Option<String>,
}
@@ -4,7 +4,7 @@ use serde::{Deserialize, Serialize};
use typeshare::typeshare;
#[typeshare]
#[derive(Debug, Clone, Serialize, Deserialize)]
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
pub struct NetworkListItem {
pub name: Option<String>,
pub id: Option<String>,
+281
View File
@@ -0,0 +1,281 @@
//! Docker Swarm Node
use std::collections::HashMap;
use serde::{Deserialize, Serialize};
use typeshare::typeshare;
use super::*;
/// Swarm node list item.
#[typeshare]
#[derive(
Debug, Clone, Default, PartialEq, Serialize, Deserialize,
)]
pub struct SwarmNodeListItem {
#[serde(rename = "ID")]
pub id: Option<String>,
/// Name for the node.
#[serde(rename = "Name")]
pub name: Option<String>,
#[serde(rename = "Hostname")]
pub hostname: Option<String>,
/// Role of the node.
#[serde(rename = "Role")]
pub role: Option<NodeSpecRoleEnum>,
/// Availability of the node.
#[serde(rename = "Availability")]
pub availability: Option<NodeSpecAvailabilityEnum>,
/// State of the node
#[serde(rename = "State")]
pub state: Option<NodeState>,
/// Date and time at which the node was added to the swarm in [RFC 3339](https://www.ietf.org/rfc/rfc3339.txt) format with nano-seconds.
#[serde(rename = "CreatedAt")]
pub created_at: Option<String>,
/// Date and time at which the node was last updated in [RFC 3339](https://www.ietf.org/rfc/rfc3339.txt) format with nano-seconds.
#[serde(rename = "UpdatedAt")]
pub updated_at: Option<String>,
}
/// Swarm node details.
#[typeshare]
#[derive(
Debug, Clone, Default, PartialEq, Serialize, Deserialize,
)]
pub struct SwarmNode {
#[serde(rename = "ID")]
pub id: Option<String>,
#[serde(rename = "Version")]
pub version: Option<ObjectVersion>,
/// Date and time at which the node was added to the swarm in [RFC 3339](https://www.ietf.org/rfc/rfc3339.txt) format with nano-seconds.
#[serde(rename = "CreatedAt")]
pub created_at: Option<String>,
/// Date and time at which the node was last updated in [RFC 3339](https://www.ietf.org/rfc/rfc3339.txt) format with nano-seconds.
#[serde(rename = "UpdatedAt")]
pub updated_at: Option<String>,
#[serde(rename = "Spec")]
pub spec: Option<NodeSpec>,
#[serde(rename = "Description")]
pub description: Option<NodeDescription>,
#[serde(rename = "Status")]
pub status: Option<NodeStatus>,
#[serde(rename = "ManagerStatus")]
pub manager_status: Option<ManagerStatus>,
}
#[typeshare]
#[derive(
Debug, Clone, Default, PartialEq, Serialize, Deserialize,
)]
pub struct NodeSpec {
/// Name for the node.
#[serde(rename = "Name")]
pub name: Option<String>,
/// User-defined key/value metadata.
#[serde(rename = "Labels")]
pub labels: Option<HashMap<String, String>>,
/// Role of the node.
#[serde(rename = "Role")]
pub role: Option<NodeSpecRoleEnum>,
/// Availability of the node.
#[serde(rename = "Availability")]
pub availability: Option<NodeSpecAvailabilityEnum>,
}
#[typeshare]
#[derive(
Debug,
Clone,
Copy,
PartialEq,
Eq,
PartialOrd,
Ord,
Default,
Serialize,
Deserialize,
)]
pub enum NodeSpecRoleEnum {
#[default]
#[serde(rename = "")]
EMPTY,
#[serde(rename = "worker")]
WORKER,
#[serde(rename = "manager")]
MANAGER,
}
#[typeshare]
#[derive(
Debug,
Clone,
Copy,
PartialEq,
Eq,
PartialOrd,
Ord,
Default,
Serialize,
Deserialize,
)]
pub enum NodeSpecAvailabilityEnum {
#[default]
#[serde(rename = "")]
EMPTY,
#[serde(rename = "active")]
ACTIVE,
#[serde(rename = "pause")]
PAUSE,
#[serde(rename = "drain")]
DRAIN,
}
#[typeshare]
#[derive(
Debug, Clone, Default, PartialEq, Serialize, Deserialize,
)]
pub struct NodeDescription {
#[serde(rename = "Hostname")]
pub hostname: Option<String>,
#[serde(rename = "Platform")]
pub platform: Option<Platform>,
#[serde(rename = "Resources")]
pub resources: Option<ResourceObject>,
#[serde(rename = "Engine")]
pub engine: Option<EngineDescription>,
#[serde(rename = "TLSInfo")]
pub tls_info: Option<TlsInfo>,
}
/// EngineDescription provides information about an engine.
#[typeshare]
#[derive(
Debug, Clone, Default, PartialEq, Serialize, Deserialize,
)]
pub struct EngineDescription {
#[serde(rename = "EngineVersion")]
pub engine_version: Option<String>,
#[serde(rename = "Labels")]
pub labels: Option<HashMap<String, String>>,
#[serde(rename = "Plugins")]
pub plugins: Option<Vec<EngineDescriptionPlugins>>,
}
#[typeshare]
#[derive(
Debug, Clone, Default, PartialEq, Serialize, Deserialize,
)]
pub struct EngineDescriptionPlugins {
#[serde(rename = "Type")]
pub typ: Option<String>,
#[serde(rename = "Name")]
pub name: Option<String>,
}
/// NodeStatus represents the status of a node. It provides the current status of the node, as seen by the manager.
#[typeshare]
#[derive(
Debug, Clone, Default, PartialEq, Serialize, Deserialize,
)]
pub struct NodeStatus {
#[serde(rename = "State")]
pub state: Option<NodeState>,
#[serde(rename = "Message")]
pub message: Option<String>,
/// IP address of the node.
#[serde(rename = "Addr")]
pub addr: Option<String>,
}
/// NodeState represents the state of a node.
#[typeshare]
#[derive(
Debug,
Clone,
Copy,
PartialEq,
Eq,
PartialOrd,
Ord,
Default,
Serialize,
Deserialize,
)]
pub enum NodeState {
#[default]
#[serde(rename = "unknown")]
UNKNOWN,
#[serde(rename = "down")]
DOWN,
#[serde(rename = "ready")]
READY,
#[serde(rename = "disconnected")]
DISCONNECTED,
}
/// ManagerStatus represents the status of a manager. It provides the current status of a node's manager component, if the node is a manager.
#[typeshare]
#[derive(
Debug, Clone, Default, PartialEq, Serialize, Deserialize,
)]
pub struct ManagerStatus {
#[serde(rename = "Leader")]
pub leader: Option<bool>,
#[serde(rename = "Reachability")]
pub reachability: Option<NodeReachability>,
/// The IP address and port at which the manager is reachable.
#[serde(rename = "Addr")]
pub addr: Option<String>,
}
/// Reachability represents the reachability of a node.
#[typeshare]
#[derive(
Debug,
Clone,
Copy,
PartialEq,
Eq,
PartialOrd,
Ord,
Default,
Serialize,
Deserialize,
)]
pub enum NodeReachability {
#[default]
#[serde(rename = "unknown")]
UNKNOWN,
#[serde(rename = "unreachable")]
UNREACHABLE,
#[serde(rename = "reachable")]
REACHABLE,
}
@@ -0,0 +1,87 @@
use std::collections::HashMap;
use serde::{Deserialize, Serialize};
use typeshare::typeshare;
use super::*;
/// Swarm secret list item.
#[typeshare]
#[derive(
Debug, Clone, Default, PartialEq, Serialize, Deserialize,
)]
pub struct SwarmSecretListItem {
#[serde(rename = "ID")]
pub id: Option<String>,
/// User-defined name of the secret.
#[serde(rename = "Name")]
pub name: Option<String>,
/// Name of the secrets driver used to fetch the secret's value from an external secret store.
#[serde(rename = "Driver")]
pub driver: Option<String>,
/// Templating driver, if applicable Templating controls whether and how to evaluate the config payload as a template.
/// If no driver is set, no templating is used.
#[serde(rename = "Templating")]
pub templating: Option<String>,
#[serde(rename = "CreatedAt")]
pub created_at: Option<String>,
#[serde(rename = "UpdatedAt")]
pub updated_at: Option<String>,
}
/// Swarm secret details.
#[typeshare]
#[derive(
Debug, Clone, Default, PartialEq, Serialize, Deserialize,
)]
pub struct SwarmSecret {
#[serde(rename = "ID")]
pub id: Option<String>,
#[serde(rename = "Version")]
pub version: Option<ObjectVersion>,
#[serde(rename = "CreatedAt")]
pub created_at: Option<String>,
#[serde(rename = "UpdatedAt")]
pub updated_at: Option<String>,
#[serde(rename = "Spec")]
pub spec: Option<SecretSpec>,
}
#[typeshare]
#[derive(
Debug, Clone, Default, PartialEq, Serialize, Deserialize,
)]
pub struct SecretSpec {
/// User-defined name of the secret.
#[serde(rename = "Name")]
pub name: Option<String>,
/// User-defined key/value metadata.
#[serde(rename = "Labels")]
pub labels: Option<HashMap<String, String>>,
/// Data is the data to store as a secret, formatted as a Base64-url-safe-encoded ([RFC 4648](https://tools.ietf.org/html/rfc4648#section-5)) string.
/// It must be empty if the Driver field is set, in which case the data is loaded from an external secret store.
/// The maximum allowed size is 500KB, as defined in [MaxSecretSize](https://pkg.go.dev/github.com/moby/swarmkit/v2@v2.0.0-20250103191802-8c1959736554/api/validation#MaxSecretSize).
/// This field is only used to _create_ a secret, and is not returned by other endpoints.
#[serde(rename = "Data")]
pub data: Option<String>,
/// Name of the secrets driver used to fetch the secret's value from an external secret store.
#[serde(rename = "Driver")]
pub driver: Option<Driver>,
/// Templating driver, if applicable Templating controls whether and how to evaluate the config payload as a template.
/// If no driver is set, no templating is used.
#[serde(rename = "Templating")]
pub templating: Option<Driver>,
}
@@ -0,0 +1,473 @@
use std::collections::HashMap;
use serde::{Deserialize, Serialize};
use typeshare::typeshare;
use crate::entities::{
I64, U64, docker::task::TaskSpecRestartPolicyConditionEnum,
};
use super::{
EndpointPortConfig, NetworkAttachmentConfig, ObjectVersion,
task::TaskSpec,
};
/// Swarm service list item.
#[typeshare]
#[derive(
Debug, Clone, Default, PartialEq, Serialize, Deserialize,
)]
pub struct SwarmServiceListItem {
#[serde(rename = "ID")]
pub id: Option<String>,
/// Name of the service.
#[serde(rename = "Name")]
pub name: Option<String>,
/// The image associated with service
#[serde(rename = "Image")]
pub image: Option<String>,
/// Runtime is the type of runtime specified for the task executor.
#[serde(rename = "Runtime")]
pub runtime: Option<String>,
/// Condition for restart.
#[serde(rename = "Restart")]
pub restart: Option<TaskSpecRestartPolicyConditionEnum>,
/// Number of replicas
#[serde(rename = "Replicas")]
pub replicas: Option<I64>,
#[serde(rename = "CreatedAt")]
pub created_at: Option<String>,
#[serde(rename = "UpdatedAt")]
pub updated_at: Option<String>,
}
/// Swarm service details.
#[typeshare]
#[derive(
Debug, Clone, Default, PartialEq, Serialize, Deserialize,
)]
pub struct SwarmService {
#[serde(rename = "ID")]
pub id: Option<String>,
#[serde(rename = "Version")]
pub version: Option<ObjectVersion>,
#[serde(rename = "CreatedAt")]
pub created_at: Option<String>,
#[serde(rename = "UpdatedAt")]
pub updated_at: Option<String>,
#[serde(rename = "Spec")]
pub spec: Option<ServiceSpec>,
#[serde(rename = "Endpoint")]
pub endpoint: Option<ServiceEndpoint>,
#[serde(rename = "UpdateStatus")]
pub update_status: Option<ServiceUpdateStatus>,
#[serde(rename = "ServiceStatus")]
pub service_status: Option<ServiceServiceStatus>,
#[serde(rename = "JobStatus")]
pub job_status: Option<ServiceJobStatus>,
}
/// User modifiable configuration for a service.
#[typeshare]
#[derive(
Debug, Clone, Default, PartialEq, Serialize, Deserialize,
)]
pub struct ServiceSpec {
/// Name of the service.
#[serde(rename = "Name")]
pub name: Option<String>,
/// User-defined key/value metadata.
#[serde(rename = "Labels")]
pub labels: Option<HashMap<String, String>>,
#[serde(rename = "TaskTemplate")]
pub task_template: Option<TaskSpec>,
#[serde(rename = "Mode")]
pub mode: Option<ServiceSpecMode>,
#[serde(rename = "UpdateConfig")]
pub update_config: Option<ServiceSpecUpdateConfig>,
#[serde(rename = "RollbackConfig")]
pub rollback_config: Option<ServiceSpecRollbackConfig>,
/// Specifies which networks the service should attach to. Deprecated: This field is deprecated since v1.44. The Networks field in TaskSpec should be used instead.
#[serde(rename = "Networks")]
pub networks: Option<Vec<NetworkAttachmentConfig>>,
#[serde(rename = "EndpointSpec")]
pub endpoint_spec: Option<EndpointSpec>,
}
/// Scheduling mode for the service.
#[typeshare]
#[derive(
Debug, Clone, Default, PartialEq, Serialize, Deserialize,
)]
pub struct ServiceSpecMode {
#[serde(rename = "Replicated")]
pub replicated: Option<ServiceSpecModeReplicated>,
// #[serde(rename = "Global")]
// pub global: Option<HashMap<(), ()>>,
#[serde(rename = "ReplicatedJob")]
pub replicated_job: Option<ServiceSpecModeReplicatedJob>,
// /// The mode used for services which run a task to the completed state on each valid node.
// #[serde(rename = "GlobalJob")]
// pub global_job: Option<HashMap<(), ()>>,
}
#[typeshare]
#[derive(
Debug, Clone, Default, PartialEq, Serialize, Deserialize,
)]
pub struct ServiceSpecModeReplicated {
#[serde(rename = "Replicas")]
pub replicas: Option<I64>,
}
/// The mode used for services with a finite number of tasks that run to a completed state.
#[typeshare]
#[derive(
Debug, Clone, Default, PartialEq, Serialize, Deserialize,
)]
pub struct ServiceSpecModeReplicatedJob {
/// The maximum number of replicas to run simultaneously.
#[serde(rename = "MaxConcurrent")]
pub max_concurrent: Option<I64>,
/// The total number of replicas desired to reach the Completed state. If unset, will default to the value of `MaxConcurrent`
#[serde(rename = "TotalCompletions")]
pub total_completions: Option<I64>,
}
/// Specification for the update strategy of the service.
#[typeshare]
#[derive(
Debug, Clone, Default, PartialEq, Serialize, Deserialize,
)]
pub struct ServiceSpecUpdateConfig {
/// Maximum number of tasks to be updated in one iteration (0 means unlimited parallelism).
#[serde(rename = "Parallelism")]
pub parallelism: Option<I64>,
/// Amount of time between updates, in nanoseconds.
#[serde(rename = "Delay")]
pub delay: Option<I64>,
/// Action to take if an updated task fails to run, or stops running during the update.
#[serde(rename = "FailureAction")]
pub failure_action:
Option<ServiceSpecUpdateConfigFailureActionEnum>,
/// Amount of time to monitor each updated task for failures, in nanoseconds.
#[serde(rename = "Monitor")]
pub monitor: Option<I64>,
/// The fraction of tasks that may fail during an update before the failure action is invoked, specified as a floating point number between 0 and 1.
#[serde(rename = "MaxFailureRatio")]
pub max_failure_ratio: Option<f64>,
/// The order of operations when rolling out an updated task. Either the old task is shut down before the new task is started, or the new task is started before the old task is shut down.
#[serde(rename = "Order")]
pub order: Option<ServiceSpecUpdateConfigOrderEnum>,
}
#[typeshare]
#[derive(
Debug,
Clone,
Copy,
PartialEq,
Eq,
PartialOrd,
Ord,
Default,
Serialize,
Deserialize,
)]
pub enum ServiceSpecUpdateConfigFailureActionEnum {
#[default]
#[serde(rename = "")]
EMPTY,
#[serde(rename = "continue")]
CONTINUE,
#[serde(rename = "pause")]
PAUSE,
#[serde(rename = "rollback")]
ROLLBACK,
}
#[allow(non_camel_case_types)]
#[typeshare]
#[derive(
Debug,
Clone,
Copy,
PartialEq,
Eq,
PartialOrd,
Ord,
Default,
Serialize,
Deserialize,
)]
pub enum ServiceSpecUpdateConfigOrderEnum {
#[default]
#[serde(rename = "")]
EMPTY,
#[serde(rename = "stop-first")]
STOP_FIRST,
#[serde(rename = "start-first")]
START_FIRST,
}
/// Specification for the rollback strategy of the service.
#[typeshare]
#[derive(
Debug, Clone, Default, PartialEq, Serialize, Deserialize,
)]
pub struct ServiceSpecRollbackConfig {
/// Maximum number of tasks to be rolled back in one iteration (0 means unlimited parallelism).
#[serde(rename = "Parallelism")]
pub parallelism: Option<I64>,
/// Amount of time between rollback iterations, in nanoseconds.
#[serde(rename = "Delay")]
pub delay: Option<I64>,
/// Action to take if an rolled back task fails to run, or stops running during the rollback.
#[serde(rename = "FailureAction")]
pub failure_action:
Option<ServiceSpecRollbackConfigFailureActionEnum>,
/// Amount of time to monitor each rolled back task for failures, in nanoseconds.
#[serde(rename = "Monitor")]
pub monitor: Option<I64>,
/// The fraction of tasks that may fail during a rollback before the failure action is invoked, specified as a floating point number between 0 and 1.
#[serde(rename = "MaxFailureRatio")]
pub max_failure_ratio: Option<f64>,
/// The order of operations when rolling back a task. Either the old task is shut down before the new task is started, or the new task is started before the old task is shut down.
#[serde(rename = "Order")]
pub order: Option<ServiceSpecRollbackConfigOrderEnum>,
}
#[typeshare]
#[derive(
Debug,
Clone,
Copy,
PartialEq,
Eq,
PartialOrd,
Ord,
Default,
Serialize,
Deserialize,
)]
pub enum ServiceSpecRollbackConfigFailureActionEnum {
#[default]
#[serde(rename = "")]
EMPTY,
#[serde(rename = "continue")]
CONTINUE,
#[serde(rename = "pause")]
PAUSE,
}
#[allow(non_camel_case_types)]
#[typeshare]
#[derive(
Debug,
Clone,
Copy,
PartialEq,
Eq,
PartialOrd,
Ord,
Default,
Serialize,
Deserialize,
)]
pub enum ServiceSpecRollbackConfigOrderEnum {
#[default]
#[serde(rename = "")]
EMPTY,
#[serde(rename = "stop-first")]
STOP_FIRST,
#[serde(rename = "start-first")]
START_FIRST,
}
/// Properties that can be configured to access and load balance a service.
#[typeshare]
#[derive(
Debug, Clone, Default, PartialEq, Serialize, Deserialize,
)]
pub struct EndpointSpec {
/// The mode of resolution to use for internal load balancing between tasks.
#[serde(rename = "Mode")]
pub mode: Option<EndpointSpecModeEnum>,
/// List of exposed ports that this service is accessible on from the outside. Ports can only be provided if `vip` resolution mode is used.
#[serde(rename = "Ports")]
pub ports: Option<Vec<EndpointPortConfig>>,
}
#[typeshare]
#[derive(
Debug,
Clone,
Copy,
PartialEq,
Eq,
PartialOrd,
Ord,
Default,
Serialize,
Deserialize,
)]
pub enum EndpointSpecModeEnum {
#[default]
#[serde(rename = "")]
EMPTY,
#[serde(rename = "vip")]
VIP,
#[serde(rename = "dnsrr")]
DNSRR,
}
#[typeshare]
#[derive(
Debug, Clone, Default, PartialEq, Serialize, Deserialize,
)]
pub struct ServiceEndpoint {
#[serde(rename = "Spec")]
pub spec: Option<EndpointSpec>,
#[serde(rename = "Ports")]
pub ports: Option<Vec<EndpointPortConfig>>,
#[serde(rename = "VirtualIPs")]
pub virtual_ips: Option<Vec<ServiceEndpointVirtualIps>>,
}
#[typeshare]
#[derive(
Debug, Clone, Default, PartialEq, Serialize, Deserialize,
)]
pub struct ServiceEndpointVirtualIps {
#[serde(rename = "NetworkID")]
pub network_id: Option<String>,
#[serde(rename = "Addr")]
pub addr: Option<String>,
}
/// The status of a service update.
#[typeshare]
#[derive(
Debug, Clone, Default, PartialEq, Serialize, Deserialize,
)]
pub struct ServiceUpdateStatus {
#[serde(rename = "State")]
pub state: Option<ServiceUpdateStatusStateEnum>,
#[serde(rename = "StartedAt")]
pub started_at: Option<String>,
#[serde(rename = "CompletedAt")]
pub completed_at: Option<String>,
#[serde(rename = "Message")]
pub message: Option<String>,
}
#[allow(non_camel_case_types)]
#[typeshare]
#[derive(
Debug,
Clone,
Copy,
PartialEq,
Eq,
PartialOrd,
Ord,
Default,
Serialize,
Deserialize,
)]
pub enum ServiceUpdateStatusStateEnum {
#[default]
#[serde(rename = "")]
EMPTY,
#[serde(rename = "updating")]
UPDATING,
#[serde(rename = "paused")]
PAUSED,
#[serde(rename = "completed")]
COMPLETED,
#[serde(rename = "rollback_started")]
ROLLBACK_STARTED,
#[serde(rename = "rollback_paused")]
ROLLBACK_PAUSED,
#[serde(rename = "rollback_completed")]
ROLLBACK_COMPLETED,
}
/// The status of the service's tasks. Provided only when requested as part of a ServiceList operation.
#[typeshare]
#[derive(
Debug, Clone, Default, PartialEq, Serialize, Deserialize,
)]
pub struct ServiceServiceStatus {
/// The number of tasks for the service currently in the Running state.
#[serde(rename = "RunningTasks")]
pub running_tasks: Option<U64>,
/// The number of tasks for the service desired to be running.
/// For replicated services, this is the replica count from the service spec.
/// For global services, this is computed by taking count of all tasks for the service with a Desired State other than Shutdown.
#[serde(rename = "DesiredTasks")]
pub desired_tasks: Option<U64>,
/// The number of tasks for a job that are in the Completed state.
/// This field must be cross-referenced with the service type, as the value of 0 may mean the service is not in a job mode,
/// or it may mean the job-mode service has no tasks yet Completed.
#[serde(rename = "CompletedTasks")]
pub completed_tasks: Option<U64>,
}
/// The status of the service when it is in one of ReplicatedJob or GlobalJob modes. Absent on Replicated and Global mode services. The JobIteration is an ObjectVersion, but unlike the Service's version, does not need to be sent with an update request.
#[typeshare]
#[derive(
Debug, Clone, Default, PartialEq, Serialize, Deserialize,
)]
pub struct ServiceJobStatus {
/// JobIteration is a value increased each time a Job is executed, successfully or otherwise. \"Executed\", in this case, means the job as a whole has been started, not that an individual Task has been launched. A job is \"Executed\" when its ServiceSpec is updated. JobIteration can be used to disambiguate Tasks belonging to different executions of a job. Though JobIteration will increase with each subsequent execution, it may not necessarily increase by 1, and so JobIteration should not be used to
#[serde(rename = "JobIteration")]
pub job_iteration: Option<ObjectVersion>,
/// The last time, as observed by the server, that this job was started.
#[serde(rename = "LastExecution")]
pub last_execution: Option<String>,
}
+124
View File
@@ -0,0 +1,124 @@
use serde::{Deserialize, Serialize};
use typeshare::typeshare;
/// Swarm stack list item.
/// Returned by `docker stack ls --format json`
///
/// https://docs.docker.com/reference/cli/docker/stack/ls/#format
#[typeshare]
#[derive(
Debug, Clone, Default, PartialEq, Serialize, Deserialize,
)]
pub struct SwarmStackListItem {
/// Swarm stack name.
#[serde(rename = "Name")]
pub name: Option<String>,
/// Number of services which are part of the stack
#[serde(rename = "Services")]
pub services: Option<String>,
/// The stack orchestrator
#[serde(rename = "Orchestrator")]
pub orchestrator: Option<String>,
/// The stack namespace
#[serde(rename = "Namespace")]
pub namespace: Option<String>,
}
/// All entities related to docker stack available over CLI.
/// Returned by:
/// ```
/// docker stack services --format json <STACK>
/// docker stack ps --format json <STACK>
/// ```
#[typeshare]
#[derive(
Debug, Clone, Default, PartialEq, Serialize, Deserialize,
)]
pub struct SwarmStackLists {
/// Swarm stack name.
#[serde(rename = "Name")]
pub name: String,
/// Services part of the stack
#[serde(rename = "Services")]
pub services: Vec<SwarmStackServiceListItem>,
/// Tasks part of the stack
#[serde(rename = "Tasks")]
pub tasks: Vec<SwarmStackTaskListItem>,
}
/// Swarm stack service list item.
/// Returned by `docker stack services --format json <NAME>`
///
/// https://docs.docker.com/reference/cli/docker/stack/services/#format
#[typeshare]
#[derive(
Debug, Clone, Default, PartialEq, Serialize, Deserialize,
)]
pub struct SwarmStackServiceListItem {
#[serde(rename = "ID")]
pub id: Option<String>,
/// Swarm stack task name.
#[serde(rename = "Name")]
pub name: Option<String>,
/// The service mode.
#[serde(rename = "Mode")]
pub mode: Option<String>,
/// The service replicas, formatted as string.
#[serde(rename = "Replicas")]
pub replicas: Option<String>,
/// The image associated with service
#[serde(rename = "Image")]
pub image: Option<String>,
/// Task exposed ports, formatted as a string.
#[serde(rename = "Ports")]
pub ports: Option<String>,
}
/// Swarm stack task list item.
/// Returned by `docker stack ps --format json <NAME>`
///
/// https://docs.docker.com/reference/cli/docker/stack/ps/#format
#[typeshare]
#[derive(
Debug, Clone, Default, PartialEq, Serialize, Deserialize,
)]
pub struct SwarmStackTaskListItem {
#[serde(rename = "ID")]
pub id: Option<String>,
/// Swarm stack task name.
#[serde(rename = "Name")]
pub name: Option<String>,
/// The image associated with task
#[serde(rename = "Image")]
pub image: Option<String>,
/// The node the task is running on
#[serde(rename = "Node")]
pub node: Option<String>,
#[serde(rename = "DesiredState")]
pub desired_state: Option<String>,
#[serde(rename = "CurrentState")]
pub current_state: Option<String>,
/// An error message, if one exists
#[serde(rename = "Error")]
pub error: Option<String>,
/// Task exposed ports, formatted as a string.
#[serde(rename = "Ports")]
pub ports: Option<String>,
}
+257
View File
@@ -0,0 +1,257 @@
use serde::{Deserialize, Serialize};
use typeshare::typeshare;
use super::*;
/// Docker-level information about the Swarm.
#[typeshare]
#[derive(
Debug, Clone, Default, PartialEq, Serialize, Deserialize,
)]
pub struct SwarmInspectInfo {
/// The (Docker) ID of the swarm.
#[serde(rename = "ID")]
pub id: Option<String>,
#[serde(rename = "Version")]
pub version: Option<ObjectVersion>,
/// Date and time at which the swarm was initialised in [RFC 3339](https://www.ietf.org/rfc/rfc3339.txt) format with nano-seconds.
#[serde(rename = "CreatedAt")]
pub created_at: Option<String>,
/// Date and time at which the swarm was last updated in [RFC 3339](https://www.ietf.org/rfc/rfc3339.txt) format with nano-seconds.
#[serde(rename = "UpdatedAt")]
pub updated_at: Option<String>,
#[serde(rename = "Spec")]
pub spec: Option<SwarmSpec>,
#[serde(rename = "TLSInfo")]
pub tls_info: Option<TlsInfo>,
/// Whether there is currently a root CA rotation in progress for the swarm
#[serde(rename = "RootRotationInProgress")]
pub root_rotation_in_progress: Option<bool>,
/// DataPathPort specifies the data path port number for data traffic. Acceptable port range is 1024 to 49151. If no port is set or is set to 0, the default port (4789) is used.
#[serde(rename = "DataPathPort")]
pub data_path_port: Option<u32>,
/// Default Address Pool specifies default subnet pools for global scope networks.
#[serde(rename = "DefaultAddrPool")]
pub default_addr_pool: Option<Vec<String>>,
/// SubnetSize specifies the subnet size of the networks created from the default subnet pool.
#[serde(rename = "SubnetSize")]
pub subnet_size: Option<u32>,
#[serde(rename = "JoinTokens")]
pub join_tokens: Option<JoinTokens>,
}
/// User modifiable swarm configuration.
#[typeshare]
#[derive(
Debug, Clone, Default, PartialEq, Serialize, Deserialize,
)]
pub struct SwarmSpec {
/// Name of the swarm.
#[serde(rename = "Name")]
pub name: Option<String>,
/// User-defined key/value metadata.
#[serde(rename = "Labels")]
pub labels: Option<HashMap<String, String>>,
#[serde(rename = "Orchestration")]
pub orchestration: Option<SwarmSpecOrchestration>,
#[serde(rename = "Raft")]
pub raft: Option<SwarmSpecRaft>,
#[serde(rename = "Dispatcher")]
pub dispatcher: Option<SwarmSpecDispatcher>,
#[serde(rename = "CAConfig")]
pub ca_config: Option<SwarmSpecCaConfig>,
#[serde(rename = "EncryptionConfig")]
pub encryption_config: Option<SwarmSpecEncryptionConfig>,
#[serde(rename = "TaskDefaults")]
pub task_defaults: Option<SwarmSpecTaskDefaults>,
}
/// Orchestration configuration.
#[typeshare]
#[derive(
Debug, Clone, Default, PartialEq, Serialize, Deserialize,
)]
pub struct SwarmSpecOrchestration {
/// The number of historic tasks to keep per instance or node.
/// If negative, never remove completed or failed tasks.
#[serde(rename = "TaskHistoryRetentionLimit")]
pub task_history_retention_limit: Option<I64>,
}
/// Raft configuration.
#[typeshare]
#[derive(
Debug, Clone, Default, PartialEq, Serialize, Deserialize,
)]
pub struct SwarmSpecRaft {
/// The number of log entries between snapshots.
#[serde(rename = "SnapshotInterval")]
pub snapshot_interval: Option<U64>,
/// The number of snapshots to keep beyond the current snapshot.
#[serde(rename = "KeepOldSnapshots")]
pub keep_old_snapshots: Option<U64>,
/// The number of log entries to keep around to sync up slow followers after a snapshot is created.
#[serde(rename = "LogEntriesForSlowFollowers")]
pub log_entries_for_slow_followers: Option<U64>,
/// The number of ticks that a follower will wait for a message from the leader before becoming a candidate and starting an election. `ElectionTick` must be greater than `HeartbeatTick`. A tick currently defaults to one second, so these translate directly to seconds currently, but this is NOT guaranteed.
#[serde(rename = "ElectionTick")]
pub election_tick: Option<I64>,
/// The number of ticks between heartbeats.
/// Every HeartbeatTick ticks, the leader will send a heartbeat to the followers.
/// A tick currently defaults to one second, so these translate directly to seconds currently, but this is NOT guaranteed.
#[serde(rename = "HeartbeatTick")]
pub heartbeat_tick: Option<I64>,
}
/// Dispatcher configuration.
#[typeshare]
#[derive(
Debug, Clone, Default, PartialEq, Serialize, Deserialize,
)]
pub struct SwarmSpecDispatcher {
/// The delay for an agent to send a heartbeat to the dispatcher.
#[serde(rename = "HeartbeatPeriod")]
pub heartbeat_period: Option<I64>,
}
/// CA configuration.
#[typeshare]
#[derive(
Debug, Clone, Default, PartialEq, Serialize, Deserialize,
)]
pub struct SwarmSpecCaConfig {
/// The duration node certificates are issued for.
#[serde(rename = "NodeCertExpiry")]
pub node_cert_expiry: Option<I64>,
/// Configuration for forwarding signing requests to an external certificate authority.
#[serde(rename = "ExternalCAs")]
pub external_cas: Option<Vec<SwarmSpecCaConfigExternalCas>>,
/// The desired signing CA certificate for all swarm node TLS leaf certificates, in PEM format.
#[serde(rename = "SigningCACert")]
pub signing_ca_cert: Option<String>,
/// The desired signing CA key for all swarm node TLS leaf certificates, in PEM format.
#[serde(rename = "SigningCAKey")]
pub signing_ca_key: Option<String>,
/// An integer whose purpose is to force swarm to generate a new signing CA certificate and key, if none have been specified in `SigningCACert` and `SigningCAKey`
#[serde(rename = "ForceRotate")]
pub force_rotate: Option<U64>,
}
#[typeshare]
#[derive(
Debug, Clone, Default, PartialEq, Serialize, Deserialize,
)]
pub struct SwarmSpecCaConfigExternalCas {
/// Protocol for communication with the external CA (currently only `cfssl` is supported).
#[serde(rename = "Protocol")]
pub protocol: Option<SwarmSpecCaConfigExternalCasProtocolEnum>,
/// URL where certificate signing requests should be sent.
#[serde(rename = "URL")]
pub url: Option<String>,
/// An object with key/value pairs that are interpreted as protocol-specific options for the external CA driver.
#[serde(rename = "Options")]
pub options: Option<HashMap<String, String>>,
/// The root CA certificate (in PEM format) this external CA uses to issue TLS certificates (assumed to be to the current swarm root CA certificate if not provided).
#[serde(rename = "CACert")]
pub ca_cert: Option<String>,
}
#[typeshare]
#[derive(
Debug,
Clone,
Copy,
PartialEq,
Eq,
PartialOrd,
Ord,
Default,
Serialize,
Deserialize,
)]
pub enum SwarmSpecCaConfigExternalCasProtocolEnum {
#[default]
#[serde(rename = "")]
EMPTY,
#[serde(rename = "cfssl")]
CFSSL,
}
/// Parameters related to encryption-at-rest.
#[typeshare]
#[derive(
Debug, Clone, Default, PartialEq, Serialize, Deserialize,
)]
pub struct SwarmSpecEncryptionConfig {
/// If set, generate a key and use it to lock data stored on the managers.
#[serde(rename = "AutoLockManagers")]
pub auto_lock_managers: Option<bool>,
}
/// Defaults for creating tasks in this cluster.
#[typeshare]
#[derive(
Debug, Clone, Default, PartialEq, Serialize, Deserialize,
)]
pub struct SwarmSpecTaskDefaults {
#[serde(rename = "LogDriver")]
pub log_driver: Option<SwarmSpecTaskDefaultsLogDriver>,
}
/// The log driver to use for tasks created in the orchestrator if unspecified by a service. Updating this value only affects new tasks. Existing tasks continue to use their previously configured log driver until recreated.
#[typeshare]
#[derive(
Debug, Clone, Default, PartialEq, Serialize, Deserialize,
)]
pub struct SwarmSpecTaskDefaultsLogDriver {
/// The log driver to use as a default for new tasks.
#[serde(rename = "Name")]
pub name: Option<String>,
/// Driver-specific options for the selected log driver, specified as key/value pairs.
#[serde(rename = "Options")]
pub options: Option<HashMap<String, String>>,
}
/// JoinTokens contains the tokens workers and managers need to join the swarm.
#[typeshare]
#[derive(
Debug, Clone, Default, PartialEq, Serialize, Deserialize,
)]
pub struct JoinTokens {
/// The token workers can use to join the swarm.
#[serde(rename = "Worker")]
pub worker: Option<String>,
/// The token managers can use to join the swarm.
#[serde(rename = "Manager")]
pub manager: Option<String>,
}
+788
View File
@@ -0,0 +1,788 @@
use serde::{Deserialize, Serialize};
use typeshare::typeshare;
use super::*;
/// Swarm task list item.
#[typeshare]
#[derive(
Debug, Clone, Default, PartialEq, Serialize, Deserialize,
)]
pub struct SwarmTaskListItem {
/// The ID of the task.
#[serde(rename = "ID")]
pub id: Option<String>,
/// Name of the task.
#[serde(rename = "Name")]
pub name: Option<String>,
/// The ID of the node that this task is on.
#[serde(rename = "NodeID")]
pub node_id: Option<String>,
/// The ID of the service this task is part of.
#[serde(rename = "ServiceID")]
pub service_id: Option<String>,
/// The ID of container associated with this task.
#[serde(rename = "ContainerID")]
pub container_id: Option<String>,
#[serde(rename = "State")]
pub state: Option<TaskState>,
#[serde(rename = "DesiredState")]
pub desired_state: Option<TaskState>,
#[serde(rename = "CreatedAt")]
pub created_at: Option<String>,
#[serde(rename = "UpdatedAt")]
pub updated_at: Option<String>,
}
/// Swarm task details.
#[typeshare]
#[derive(
Debug, Clone, Default, PartialEq, Serialize, Deserialize,
)]
pub struct SwarmTask {
/// The ID of the task.
#[serde(rename = "ID")]
pub id: Option<String>,
#[serde(rename = "Version")]
pub version: Option<ObjectVersion>,
#[serde(rename = "CreatedAt")]
pub created_at: Option<String>,
#[serde(rename = "UpdatedAt")]
pub updated_at: Option<String>,
/// Name of the task.
#[serde(rename = "Name")]
pub name: Option<String>,
/// User-defined key/value metadata.
#[serde(rename = "Labels")]
pub labels: Option<HashMap<String, String>>,
#[serde(rename = "Spec")]
pub spec: Option<TaskSpec>,
/// The ID of the service this task is part of.
#[serde(rename = "ServiceID")]
pub service_id: Option<String>,
#[serde(rename = "Slot")]
pub slot: Option<I64>,
/// The ID of the node that this task is on.
#[serde(rename = "NodeID")]
pub node_id: Option<String>,
#[serde(rename = "AssignedGenericResources")]
pub assigned_generic_resources: Option<GenericResources>,
#[serde(rename = "Status")]
pub status: Option<TaskStatus>,
#[serde(rename = "DesiredState")]
pub desired_state: Option<TaskState>,
/// If the Service this Task belongs to is a job-mode service, contains the JobIteration of the Service this Task was created for. Absent if the Task was created for a Replicated or Global Service.
#[serde(rename = "JobIteration")]
pub job_iteration: Option<ObjectVersion>,
}
/// User modifiable task configuration.
#[typeshare]
#[derive(
Debug, Clone, Default, PartialEq, Serialize, Deserialize,
)]
pub struct TaskSpec {
#[serde(rename = "PluginSpec")]
pub plugin_spec: Option<TaskSpecPluginSpec>,
#[serde(rename = "ContainerSpec")]
pub container_spec: Option<TaskSpecContainerSpec>,
#[serde(rename = "NetworkAttachmentSpec")]
pub network_attachment_spec: Option<TaskSpecNetworkAttachmentSpec>,
#[serde(rename = "Resources")]
pub resources: Option<TaskSpecResources>,
#[serde(rename = "RestartPolicy")]
pub restart_policy: Option<TaskSpecRestartPolicy>,
#[serde(rename = "Placement")]
pub placement: Option<TaskSpecPlacement>,
/// A counter that triggers an update even if no relevant parameters have been changed.
#[serde(rename = "ForceUpdate")]
pub force_update: Option<U64>,
/// Runtime is the type of runtime specified for the task executor.
#[serde(rename = "Runtime")]
pub runtime: Option<String>,
/// Specifies which networks the service should attach to.
#[serde(rename = "Networks")]
pub networks: Option<Vec<NetworkAttachmentConfig>>,
#[serde(rename = "LogDriver")]
pub log_driver: Option<TaskSpecLogDriver>,
}
/// Plugin spec for the service.
/// *(Experimental release only.)*
/// <p><br /></p> > **Note**: ContainerSpec, NetworkAttachmentSpec, and PluginSpec are > mutually exclusive. PluginSpec is only used when the Runtime field > is set to `plugin`. NetworkAttachmentSpec is used when the Runtime > field is set to `attachment`.
#[typeshare]
#[derive(
Debug, Clone, Default, PartialEq, Serialize, Deserialize,
)]
pub struct TaskSpecPluginSpec {
/// The name or 'alias' to use for the plugin.
#[serde(rename = "Name")]
pub name: Option<String>,
/// The plugin image reference to use.
#[serde(rename = "Remote")]
pub remote: Option<String>,
/// Disable the plugin once scheduled.
#[serde(rename = "Disabled")]
pub disabled: Option<bool>,
#[serde(rename = "PluginPrivilege")]
pub plugin_privilege: Option<Vec<PluginPrivilege>>,
}
/// Describes a permission the user has to accept upon installing the plugin.
#[typeshare]
#[derive(
Debug, Clone, Default, PartialEq, Serialize, Deserialize,
)]
pub struct PluginPrivilege {
#[serde(rename = "Name")]
pub name: Option<String>,
#[serde(rename = "Description")]
pub description: Option<String>,
#[serde(rename = "Value")]
pub value: Option<Vec<String>>,
}
/// Container spec for the service.
/// **Note**: ContainerSpec, NetworkAttachmentSpec, and PluginSpec are > mutually exclusive.
/// PluginSpec is only used when the Runtime field > is set to `plugin`.
/// NetworkAttachmentSpec is used when the Runtime > field is set to `attachment`.
#[typeshare]
#[derive(
Debug, Clone, Default, PartialEq, Serialize, Deserialize,
)]
pub struct TaskSpecContainerSpec {
/// The image name to use for the container
#[serde(rename = "Image")]
pub image: Option<String>,
/// User-defined key/value data.
#[serde(rename = "Labels")]
pub labels: Option<HashMap<String, String>>,
/// The command to be run in the image.
#[serde(rename = "Command")]
pub command: Option<Vec<String>>,
/// Arguments to the command.
#[serde(rename = "Args")]
pub args: Option<Vec<String>>,
/// The hostname to use for the container, as a valid [RFC 1123](https://tools.ietf.org/html/rfc1123) hostname.
#[serde(rename = "Hostname")]
pub hostname: Option<String>,
/// A list of environment variables in the form `VAR=value`.
#[serde(rename = "Env")]
pub env: Option<Vec<String>>,
/// The working directory for commands to run in.
#[serde(rename = "Dir")]
pub dir: Option<String>,
/// The user inside the container.
#[serde(rename = "User")]
pub user: Option<String>,
/// A list of additional groups that the container process will run as.
#[serde(rename = "Groups")]
pub groups: Option<Vec<String>>,
#[serde(rename = "Privileges")]
pub privileges: Option<TaskSpecContainerSpecPrivileges>,
/// Whether a pseudo-TTY should be allocated.
#[serde(rename = "TTY")]
pub tty: Option<bool>,
/// Open `stdin`
#[serde(rename = "OpenStdin")]
pub open_stdin: Option<bool>,
/// Mount the container's root filesystem as read only.
#[serde(rename = "ReadOnly")]
pub read_only: Option<bool>,
/// Specification for mounts to be added to containers created as part of the service.
#[serde(rename = "Mounts")]
pub mounts: Option<Vec<Mount>>,
/// Signal to stop the container.
#[serde(rename = "StopSignal")]
pub stop_signal: Option<String>,
/// Amount of time to wait for the container to terminate before forcefully killing it.
#[serde(rename = "StopGracePeriod")]
pub stop_grace_period: Option<I64>,
#[serde(rename = "HealthCheck")]
pub health_check: Option<HealthConfig>,
/// A list of hostname/IP mappings to add to the container's `hosts` file. The format of extra hosts is specified in the [hosts(5)](http://man7.org/linux/man-pages/man5/hosts.5.html) man page: IP_address canonical_hostname [aliases...]
#[serde(rename = "Hosts")]
pub hosts: Option<Vec<String>>,
#[serde(rename = "DNSConfig")]
pub dns_config: Option<TaskSpecContainerSpecDnsConfig>,
/// Secrets contains references to zero or more secrets that will be exposed to the service.
#[serde(rename = "Secrets")]
pub secrets: Option<Vec<TaskSpecContainerSpecSecrets>>,
/// An integer value containing the score given to the container in order to tune OOM killer preferences.
#[serde(rename = "OomScoreAdj")]
pub oom_score_adj: Option<I64>,
/// Configs contains references to zero or more configs that will be exposed to the service.
#[serde(rename = "Configs")]
pub configs: Option<Vec<TaskSpecContainerSpecConfigs>>,
/// Isolation technology of the containers running the service. (Windows only)
#[serde(rename = "Isolation")]
pub isolation: Option<TaskSpecContainerSpecIsolationEnum>,
/// Run an init inside the container that forwards signals and reaps processes. This field is omitted if empty, and the default (as configured on the daemon) is used.
#[serde(rename = "Init")]
pub init: Option<bool>,
/// Set kernel namedspaced parameters (sysctls) in the container. The Sysctls option on services accepts the same sysctls as the are supported on containers. Note that while the same sysctls are supported, no guarantees or checks are made about their suitability for a clustered environment, and it's up to the user to determine whether a given sysctl will work properly in a Service.
#[serde(rename = "Sysctls")]
pub sysctls: Option<HashMap<String, String>>,
/// A list of kernel capabilities to add to the default set for the container.
#[serde(rename = "CapabilityAdd")]
pub capability_add: Option<Vec<String>>,
/// A list of kernel capabilities to drop from the default set for the container.
#[serde(rename = "CapabilityDrop")]
pub capability_drop: Option<Vec<String>>,
/// A list of resource limits to set in the container. For example: `{\"Name\": \"nofile\", \"Soft\": 1024, \"Hard\": 2048}`\"
#[serde(rename = "Ulimits")]
pub ulimits: Option<Vec<ResourcesUlimits>>,
}
/// Security options for the container
#[typeshare]
#[derive(
Debug, Clone, Default, PartialEq, Serialize, Deserialize,
)]
pub struct TaskSpecContainerSpecPrivileges {
#[serde(rename = "CredentialSpec")]
pub credential_spec:
Option<TaskSpecContainerSpecPrivilegesCredentialSpec>,
#[serde(rename = "SELinuxContext")]
pub se_linux_context:
Option<TaskSpecContainerSpecPrivilegesSeLinuxContext>,
#[serde(rename = "Seccomp")]
pub seccomp: Option<TaskSpecContainerSpecPrivilegesSeccomp>,
#[serde(rename = "AppArmor")]
pub app_armor: Option<TaskSpecContainerSpecPrivilegesAppArmor>,
/// Configuration of the no_new_privs bit in the container
#[serde(rename = "NoNewPrivileges")]
pub no_new_privileges: Option<bool>,
}
/// CredentialSpec for managed service account (Windows only)
#[typeshare]
#[derive(
Debug, Clone, Default, PartialEq, Serialize, Deserialize,
)]
pub struct TaskSpecContainerSpecPrivilegesCredentialSpec {
/// Load credential spec from a Swarm Config with the given ID. The specified config must also be present in the Configs field with the Runtime property set. <p><br /></p> > **Note**: `CredentialSpec.File`, `CredentialSpec.Registry`, > and `CredentialSpec.Config` are mutually exclusive.
#[serde(rename = "Config")]
pub config: Option<String>,
/// Load credential spec from this file. The file is read by the daemon, and must be present in the `CredentialSpecs` subdirectory in the docker data directory, which defaults to `C:\\ProgramData\\Docker\\` on Windows. For example, specifying `spec.json` loads `C:\\ProgramData\\Docker\\CredentialSpecs\\spec.json`. <p><br /></p> > **Note**: `CredentialSpec.File`, `CredentialSpec.Registry`, > and `CredentialSpec.Config` are mutually exclusive.
#[serde(rename = "File")]
pub file: Option<String>,
/// Load credential spec from this value in the Windows registry. The specified registry value must be located in: `HKLM\\SOFTWARE\\Microsoft\\Windows NT\\CurrentVersion\\Virtualization\\Containers\\CredentialSpecs` <p><br /></p> > **Note**: `CredentialSpec.File`, `CredentialSpec.Registry`, > and `CredentialSpec.Config` are mutually exclusive.
#[serde(rename = "Registry")]
pub registry: Option<String>,
}
/// SELinux labels of the container
#[typeshare]
#[derive(
Debug, Clone, Default, PartialEq, Serialize, Deserialize,
)]
pub struct TaskSpecContainerSpecPrivilegesSeLinuxContext {
/// Disable SELinux
#[serde(rename = "Disable")]
pub disable: Option<bool>,
/// SELinux user label
#[serde(rename = "User")]
pub user: Option<String>,
/// SELinux role label
#[serde(rename = "Role")]
pub role: Option<String>,
/// SELinux type label
#[serde(rename = "Type")]
pub typ: Option<String>,
/// SELinux level label
#[serde(rename = "Level")]
pub level: Option<String>,
}
/// Options for configuring seccomp on the container
#[typeshare]
#[derive(
Debug, Clone, Default, PartialEq, Serialize, Deserialize,
)]
pub struct TaskSpecContainerSpecPrivilegesSeccomp {
#[serde(rename = "Mode")]
pub mode: Option<TaskSpecContainerSpecPrivilegesSeccompModeEnum>,
/// The custom seccomp profile as a json object
#[serde(rename = "Profile")]
pub profile: Option<String>,
}
#[typeshare]
#[derive(
Debug,
Clone,
Copy,
PartialEq,
Eq,
PartialOrd,
Ord,
Default,
Serialize,
Deserialize,
)]
pub enum TaskSpecContainerSpecPrivilegesSeccompModeEnum {
#[default]
#[serde(rename = "")]
EMPTY,
#[serde(rename = "default")]
DEFAULT,
#[serde(rename = "unconfined")]
UNCONFINED,
#[serde(rename = "custom")]
CUSTOM,
}
/// Options for configuring AppArmor on the container
#[typeshare]
#[derive(
Debug, Clone, Default, PartialEq, Serialize, Deserialize,
)]
pub struct TaskSpecContainerSpecPrivilegesAppArmor {
#[serde(rename = "Mode")]
pub mode: Option<TaskSpecContainerSpecPrivilegesAppArmorModeEnum>,
}
#[typeshare]
#[derive(
Debug,
Clone,
Copy,
PartialEq,
Eq,
PartialOrd,
Ord,
Default,
Serialize,
Deserialize,
)]
pub enum TaskSpecContainerSpecPrivilegesAppArmorModeEnum {
#[default]
#[serde(rename = "")]
EMPTY,
#[serde(rename = "default")]
DEFAULT,
#[serde(rename = "disabled")]
DISABLED,
}
/// Specification for DNS related configurations in resolver configuration file (`resolv.conf`).
#[typeshare]
#[derive(
Debug, Clone, Default, PartialEq, Serialize, Deserialize,
)]
pub struct TaskSpecContainerSpecDnsConfig {
/// The IP addresses of the name servers.
#[serde(rename = "Nameservers")]
pub nameservers: Option<Vec<String>>,
/// A search list for host-name lookup.
#[serde(rename = "Search")]
pub search: Option<Vec<String>>,
/// A list of internal resolver variables to be modified (e.g., `debug`, `ndots:3`, etc.).
#[serde(rename = "Options")]
pub options: Option<Vec<String>>,
}
#[typeshare]
#[derive(
Debug, Clone, Default, PartialEq, Serialize, Deserialize,
)]
pub struct TaskSpecContainerSpecSecrets {
#[serde(rename = "File")]
pub file: Option<TaskSpecContainerSpecFile>,
/// SecretID represents the ID of the specific secret that we're referencing.
#[serde(rename = "SecretID")]
pub secret_id: Option<String>,
/// SecretName is the name of the secret that this references, but this is just provided for lookup/display purposes. The secret in the reference will be identified by its ID.
#[serde(rename = "SecretName")]
pub secret_name: Option<String>,
}
/// File represents a specific target that is backed by a file.
#[typeshare]
#[derive(
Debug, Clone, Default, PartialEq, Serialize, Deserialize,
)]
pub struct TaskSpecContainerSpecFile {
/// Name represents the final filename in the filesystem.
#[serde(rename = "Name")]
pub name: Option<String>,
/// UID represents the file UID.
#[serde(rename = "UID")]
pub uid: Option<String>,
/// GID represents the file GID.
#[serde(rename = "GID")]
pub gid: Option<String>,
/// Mode represents the FileMode of the file.
#[serde(rename = "Mode")]
pub mode: Option<u32>,
}
#[typeshare]
#[derive(
Debug, Clone, Default, PartialEq, Serialize, Deserialize,
)]
pub struct TaskSpecContainerSpecConfigs {
#[serde(rename = "File")]
pub file: Option<TaskSpecContainerSpecFile>,
// /// Runtime represents a target that is not mounted into the container but is used by the task <p><br /><p> > **Note**: `Configs.File` and `Configs.Runtime` are mutually > exclusive
// #[serde(rename = "Runtime")]
// pub runtime: Option<HashMap<(), ()>>,
/// ConfigID represents the ID of the specific config that we're referencing.
#[serde(rename = "ConfigID")]
pub config_id: Option<String>,
/// ConfigName is the name of the config that this references, but this is just provided for lookup/display purposes. The config in the reference will be identified by its ID.
#[serde(rename = "ConfigName")]
pub config_name: Option<String>,
}
#[typeshare]
#[derive(
Debug,
Clone,
Copy,
PartialEq,
Eq,
PartialOrd,
Ord,
Default,
Serialize,
Deserialize,
)]
pub enum TaskSpecContainerSpecIsolationEnum {
#[serde(rename = "default")]
DEFAULT,
#[serde(rename = "process")]
PROCESS,
#[serde(rename = "hyperv")]
HYPERV,
#[default]
#[serde(rename = "")]
EMPTY,
}
/// Read-only spec type for non-swarm containers attached to swarm overlay networks. <p><br /></p> > **Note**: ContainerSpec, NetworkAttachmentSpec, and PluginSpec are > mutually exclusive. PluginSpec is only used when the Runtime field > is set to `plugin`. NetworkAttachmentSpec is used when the Runtime > field is set to `attachment`.
#[typeshare]
#[derive(
Debug, Clone, Default, PartialEq, Serialize, Deserialize,
)]
pub struct TaskSpecNetworkAttachmentSpec {
/// ID of the container represented by this task
#[serde(rename = "ContainerID")]
pub container_id: Option<String>,
}
/// Resource requirements which apply to each individual container created as part of the service.
#[typeshare]
#[derive(
Debug, Clone, Default, PartialEq, Serialize, Deserialize,
)]
pub struct TaskSpecResources {
/// Define resources limits.
#[serde(rename = "Limits")]
pub limits: Option<Limit>,
/// Define resources reservation.
#[serde(rename = "Reservations")]
pub reservations: Option<ResourceObject>,
}
/// An object describing a limit on resources which can be requested by a task.
#[typeshare]
#[derive(
Debug, Clone, Default, PartialEq, Serialize, Deserialize,
)]
pub struct Limit {
#[serde(rename = "NanoCPUs")]
pub nano_cpus: Option<I64>,
#[serde(rename = "MemoryBytes")]
pub memory_bytes: Option<I64>,
/// Limits the maximum number of PIDs in the container. Set `0` for unlimited.
#[serde(rename = "Pids")]
pub pids: Option<I64>,
}
/// Specification for the restart policy which applies to containers created as part of this service.
#[typeshare]
#[derive(
Debug, Clone, Default, PartialEq, Serialize, Deserialize,
)]
pub struct TaskSpecRestartPolicy {
/// Condition for restart.
#[serde(rename = "Condition")]
pub condition: Option<TaskSpecRestartPolicyConditionEnum>,
/// Delay between restart attempts.
#[serde(rename = "Delay")]
pub delay: Option<I64>,
/// Maximum attempts to restart a given container before giving up (default value is 0, which is ignored).
#[serde(rename = "MaxAttempts")]
pub max_attempts: Option<I64>,
/// Windows is the time window used to evaluate the restart policy (default value is 0, which is unbounded).
#[serde(rename = "Window")]
pub window: Option<I64>,
}
#[allow(non_camel_case_types)]
#[typeshare]
#[derive(
Debug,
Clone,
Copy,
PartialEq,
Eq,
PartialOrd,
Ord,
Default,
Serialize,
Deserialize,
)]
pub enum TaskSpecRestartPolicyConditionEnum {
#[default]
#[serde(rename = "")]
EMPTY,
#[serde(rename = "none")]
NONE,
#[serde(rename = "on-failure")]
ON_FAILURE,
#[serde(rename = "any")]
ANY,
}
#[typeshare]
#[derive(
Debug, Clone, Default, PartialEq, Serialize, Deserialize,
)]
pub struct TaskSpecPlacement {
/// An array of constraint expressions to limit the set of nodes where a task can be scheduled. Constraint expressions can either use a _match_ (`==`) or _exclude_ (`!=`) rule. Multiple constraints find nodes that satisfy every expression (AND match). Constraints can match node or Docker Engine labels as follows: node attribute | matches | example ---------------------|--------------------------------|----------------------------------------------- `node.id` | Node ID | `node.id==2ivku8v2gvtg4` `node.hostname` | Node hostname | `node.hostname!=node-2` `node.role` | Node role (`manager`/`worker`) | `node.role==manager` `node.platform.os` | Node operating system | `node.platform.os==windows` `node.platform.arch` | Node architecture | `node.platform.arch==x86_64` `node.labels` | User-defined node labels | `node.labels.security==high` `engine.labels` | Docker Engine's labels | `engine.labels.operatingsystem==ubuntu-24.04` `engine.labels` apply to Docker Engine labels like operating system, drivers, etc. Swarm administrators add `node.labels` for operational purposes by using the [`node update endpoint`](#operation/NodeUpdate).
#[serde(rename = "Constraints")]
pub constraints: Option<Vec<String>>,
/// Preferences provide a way to make the scheduler aware of factors such as topology. They are provided in order from highest to lowest precedence.
#[serde(rename = "Preferences")]
pub preferences: Option<Vec<TaskSpecPlacementPreferences>>,
/// Maximum number of replicas for per node (default value is 0, which is unlimited)
#[serde(rename = "MaxReplicas")]
pub max_replicas: Option<I64>,
/// Platforms stores all the platforms that the service's image can run on. This field is used in the platform filter for scheduling. If empty, then the platform filter is off, meaning there are no scheduling restrictions.
#[serde(rename = "Platforms")]
pub platforms: Option<Vec<Platform>>,
}
#[typeshare]
#[derive(
Debug, Clone, Default, PartialEq, Serialize, Deserialize,
)]
pub struct TaskSpecPlacementPreferences {
#[serde(rename = "Spread")]
pub spread: Option<TaskSpecPlacementSpread>,
}
#[typeshare]
#[derive(
Debug, Clone, Default, PartialEq, Serialize, Deserialize,
)]
pub struct TaskSpecPlacementSpread {
/// label descriptor, such as `engine.labels.az`.
#[serde(rename = "SpreadDescriptor")]
pub spread_descriptor: Option<String>,
}
/// Specifies the log driver to use for tasks created from this spec.
/// If not present, the default one for the swarm will be used,
/// finally falling back to the engine default if not specified.
#[typeshare]
#[derive(
Debug, Clone, Default, PartialEq, Serialize, Deserialize,
)]
pub struct TaskSpecLogDriver {
#[serde(rename = "Name")]
pub name: Option<String>,
#[serde(rename = "Options")]
pub options: Option<HashMap<String, String>>,
}
/// represents the status of a task.
#[typeshare]
#[derive(
Debug, Clone, Default, PartialEq, Serialize, Deserialize,
)]
pub struct TaskStatus {
#[serde(rename = "Timestamp")]
pub timestamp: Option<String>,
#[serde(rename = "State")]
pub state: Option<TaskState>,
#[serde(rename = "Message")]
pub message: Option<String>,
#[serde(rename = "Err")]
pub err: Option<String>,
#[serde(rename = "ContainerStatus")]
pub container_status: Option<ContainerStatus>,
#[serde(rename = "PortStatus")]
pub port_status: Option<PortStatus>,
}
#[typeshare]
#[derive(
Debug,
Clone,
Copy,
PartialEq,
Eq,
PartialOrd,
Ord,
Serialize,
Deserialize,
)]
pub enum TaskState {
#[serde(rename = "new")]
NEW,
#[serde(rename = "allocated")]
ALLOCATED,
#[serde(rename = "pending")]
PENDING,
#[serde(rename = "assigned")]
ASSIGNED,
#[serde(rename = "accepted")]
ACCEPTED,
#[serde(rename = "preparing")]
PREPARING,
#[serde(rename = "ready")]
READY,
#[serde(rename = "starting")]
STARTING,
#[serde(rename = "running")]
RUNNING,
#[serde(rename = "complete")]
COMPLETE,
#[serde(rename = "shutdown")]
SHUTDOWN,
#[serde(rename = "failed")]
FAILED,
#[serde(rename = "rejected")]
REJECTED,
#[serde(rename = "remove")]
REMOVE,
#[serde(rename = "orphaned")]
ORPHANED,
}
/// represents the status of a container.
#[typeshare]
#[derive(
Debug, Clone, Default, PartialEq, Serialize, Deserialize,
)]
pub struct ContainerStatus {
#[serde(rename = "ContainerID")]
pub container_id: Option<String>,
#[serde(rename = "PID")]
pub pid: Option<I64>,
#[serde(rename = "ExitCode")]
pub exit_code: Option<I64>,
}
/// represents the port status of a task's host ports whose service has published host ports
#[typeshare]
#[derive(
Debug, Clone, Default, PartialEq, Serialize, Deserialize,
)]
pub struct PortStatus {
#[serde(rename = "Ports")]
pub ports: Option<Vec<EndpointPortConfig>>,
}
+2 -12
View File
@@ -3,9 +3,9 @@ use std::collections::HashMap;
use serde::{Deserialize, Serialize};
use typeshare::typeshare;
use crate::entities::{I64, U64};
use crate::entities::I64;
use super::PortBinding;
use super::{ObjectVersion, PortBinding};
#[typeshare]
#[derive(
@@ -189,16 +189,6 @@ pub enum ClusterVolumePublishStatusStateEnum {
PendingControllerUnpublish,
}
/// The version number of the object such as node, service, etc. This is needed to avoid conflicting writes. The client must send the version number along with the modified specification when updating these objects. This approach ensures safe concurrency and determinism in that the change on the object may not be applied if the version number has changed from the last read. In other words, if two update requests specify the same base version, only one of the requests can succeed. As a result, two separate update requests that happen at the same time will not unintentionally overwrite each other.
#[typeshare]
#[derive(
Debug, Clone, Default, PartialEq, Serialize, Deserialize,
)]
pub struct ObjectVersion {
#[serde(rename = "Index")]
pub index: Option<U64>,
}
/// Cluster-specific options used to create the volume.
#[typeshare]
#[derive(

Some files were not shown because too many files have changed in this diff Show More