WP-12: contract and failing tests for the Kubernetes overview
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
This commit is contained in:
359
backend/Cargo.lock
generated
359
backend/Cargo.lock
generated
@ -326,6 +326,16 @@ version = "0.9.6"
|
|||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "c2459377285ad874054d797f3ccebf984978aa39129f6eafde5cdc8315b612f8"
|
checksum = "c2459377285ad874054d797f3ccebf984978aa39129f6eafde5cdc8315b612f8"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "core-foundation"
|
||||||
|
version = "0.10.1"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "b2a6cd9ae233e7f62ba4e9353e81a88df7fc8a5987b8d445b4d90c879bd156f6"
|
||||||
|
dependencies = [
|
||||||
|
"core-foundation-sys",
|
||||||
|
"libc",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "core-foundation-sys"
|
name = "core-foundation-sys"
|
||||||
version = "0.8.7"
|
version = "0.8.7"
|
||||||
@ -559,6 +569,20 @@ dependencies = [
|
|||||||
"percent-encoding",
|
"percent-encoding",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "futures"
|
||||||
|
version = "0.3.34"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "9a31d2a3fbaaeb2af2368bbdd904aa8e812d3c04a1ee10d3171f52d556e5d0a3"
|
||||||
|
dependencies = [
|
||||||
|
"futures-channel",
|
||||||
|
"futures-core",
|
||||||
|
"futures-io",
|
||||||
|
"futures-sink",
|
||||||
|
"futures-task",
|
||||||
|
"futures-util",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "futures-channel"
|
name = "futures-channel"
|
||||||
version = "0.3.34"
|
version = "0.3.34"
|
||||||
@ -621,6 +645,7 @@ version = "0.3.34"
|
|||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "0d50a92467f8ba5dd6e3ee5d4bd04d73ab2e4e1c44474a0674821dfce14b79bc"
|
checksum = "0d50a92467f8ba5dd6e3ee5d4bd04d73ab2e4e1c44474a0674821dfce14b79bc"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
|
"futures-channel",
|
||||||
"futures-core",
|
"futures-core",
|
||||||
"futures-io",
|
"futures-io",
|
||||||
"futures-sink",
|
"futures-sink",
|
||||||
@ -700,6 +725,30 @@ dependencies = [
|
|||||||
"hashbrown 0.15.5",
|
"hashbrown 0.15.5",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "headers"
|
||||||
|
version = "0.4.1"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "b3314d5adb5d94bcdf56771f2e50dbbc80bb4bdf88967526706205ac9eff24eb"
|
||||||
|
dependencies = [
|
||||||
|
"base64 0.22.1",
|
||||||
|
"bytes",
|
||||||
|
"headers-core",
|
||||||
|
"http",
|
||||||
|
"httpdate",
|
||||||
|
"mime",
|
||||||
|
"sha1",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "headers-core"
|
||||||
|
version = "0.3.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "54b4a22553d4242c49fddb9ba998a99962b5cc6f22cb5a3482bec22522403ce4"
|
||||||
|
dependencies = [
|
||||||
|
"http",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "heck"
|
name = "heck"
|
||||||
version = "0.5.0"
|
version = "0.5.0"
|
||||||
@ -819,6 +868,56 @@ dependencies = [
|
|||||||
"pin-project-lite",
|
"pin-project-lite",
|
||||||
"smallvec",
|
"smallvec",
|
||||||
"tokio",
|
"tokio",
|
||||||
|
"want",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "hyper-http-proxy"
|
||||||
|
version = "1.2.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "bd1d471ea2f65ba45eddb1d6ab7d58ac2671d2d4ac14da5c6516ae1d97e1327a"
|
||||||
|
dependencies = [
|
||||||
|
"bytes",
|
||||||
|
"futures-util",
|
||||||
|
"headers",
|
||||||
|
"http",
|
||||||
|
"hyper",
|
||||||
|
"hyper-rustls",
|
||||||
|
"hyper-util",
|
||||||
|
"pin-project-lite",
|
||||||
|
"tokio",
|
||||||
|
"tokio-rustls",
|
||||||
|
"tower-service",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "hyper-rustls"
|
||||||
|
version = "0.27.9"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "33ca68d021ef39cf6463ab54c1d0f5daf03377b70561305bb89a8f83aab66e0f"
|
||||||
|
dependencies = [
|
||||||
|
"http",
|
||||||
|
"hyper",
|
||||||
|
"hyper-util",
|
||||||
|
"log",
|
||||||
|
"rustls",
|
||||||
|
"rustls-native-certs",
|
||||||
|
"tokio",
|
||||||
|
"tokio-rustls",
|
||||||
|
"tower-service",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "hyper-timeout"
|
||||||
|
version = "0.5.2"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "2b90d566bffbce6a75bd8b09a05aa8c2cb1fabb6cb348f8840c9e4c90a0d83b0"
|
||||||
|
dependencies = [
|
||||||
|
"hyper",
|
||||||
|
"hyper-util",
|
||||||
|
"pin-project-lite",
|
||||||
|
"tokio",
|
||||||
|
"tower-service",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
@ -828,12 +927,17 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
|
|||||||
checksum = "96547c2556ec9d12fb1578c4eaf448b04993e7fb79cbaad930a656880a6bdfa0"
|
checksum = "96547c2556ec9d12fb1578c4eaf448b04993e7fb79cbaad930a656880a6bdfa0"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"bytes",
|
"bytes",
|
||||||
|
"futures-channel",
|
||||||
|
"futures-util",
|
||||||
"http",
|
"http",
|
||||||
"http-body",
|
"http-body",
|
||||||
"hyper",
|
"hyper",
|
||||||
|
"libc",
|
||||||
"pin-project-lite",
|
"pin-project-lite",
|
||||||
|
"socket2",
|
||||||
"tokio",
|
"tokio",
|
||||||
"tower-service",
|
"tower-service",
|
||||||
|
"tracing",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
@ -988,6 +1092,8 @@ dependencies = [
|
|||||||
"chrono",
|
"chrono",
|
||||||
"domain",
|
"domain",
|
||||||
"jsonwebtoken",
|
"jsonwebtoken",
|
||||||
|
"k8s-openapi",
|
||||||
|
"kube",
|
||||||
"lettre",
|
"lettre",
|
||||||
"serde",
|
"serde",
|
||||||
"serde_json",
|
"serde_json",
|
||||||
@ -1022,6 +1128,19 @@ dependencies = [
|
|||||||
"wasm-bindgen",
|
"wasm-bindgen",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "jsonpath-rust"
|
||||||
|
version = "0.7.5"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "0c00ae348f9f8fd2d09f82a98ca381c60df9e0820d8d79fce43e649b4dc3128b"
|
||||||
|
dependencies = [
|
||||||
|
"pest",
|
||||||
|
"pest_derive",
|
||||||
|
"regex",
|
||||||
|
"serde_json",
|
||||||
|
"thiserror",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "jsonwebtoken"
|
name = "jsonwebtoken"
|
||||||
version = "9.3.1"
|
version = "9.3.1"
|
||||||
@ -1037,6 +1156,83 @@ dependencies = [
|
|||||||
"simple_asn1",
|
"simple_asn1",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "k8s-openapi"
|
||||||
|
version = "0.24.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "2c75b990324f09bef15e791606b7b7a296d02fc88a344f6eba9390970a870ad5"
|
||||||
|
dependencies = [
|
||||||
|
"base64 0.22.1",
|
||||||
|
"chrono",
|
||||||
|
"serde",
|
||||||
|
"serde-value",
|
||||||
|
"serde_json",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "kube"
|
||||||
|
version = "0.99.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "9a4eb20010536b48abe97fec37d23d43069bcbe9686adcf9932202327bc5ca6e"
|
||||||
|
dependencies = [
|
||||||
|
"k8s-openapi",
|
||||||
|
"kube-client",
|
||||||
|
"kube-core",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "kube-client"
|
||||||
|
version = "0.99.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "7fc2ed952042df20d15ac2fe9614d0ec14b6118eab89633985d4b36e688dccf1"
|
||||||
|
dependencies = [
|
||||||
|
"base64 0.22.1",
|
||||||
|
"bytes",
|
||||||
|
"chrono",
|
||||||
|
"either",
|
||||||
|
"futures",
|
||||||
|
"home",
|
||||||
|
"http",
|
||||||
|
"http-body",
|
||||||
|
"http-body-util",
|
||||||
|
"hyper",
|
||||||
|
"hyper-http-proxy",
|
||||||
|
"hyper-rustls",
|
||||||
|
"hyper-timeout",
|
||||||
|
"hyper-util",
|
||||||
|
"jsonpath-rust",
|
||||||
|
"k8s-openapi",
|
||||||
|
"kube-core",
|
||||||
|
"pem",
|
||||||
|
"rustls",
|
||||||
|
"secrecy",
|
||||||
|
"serde",
|
||||||
|
"serde_json",
|
||||||
|
"serde_yaml",
|
||||||
|
"thiserror",
|
||||||
|
"tokio",
|
||||||
|
"tokio-util",
|
||||||
|
"tower",
|
||||||
|
"tower-http",
|
||||||
|
"tracing",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "kube-core"
|
||||||
|
version = "0.99.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "ff0d0793db58e70ca6d689489183816cb3aa481673e7433dc618cf7e8007c675"
|
||||||
|
dependencies = [
|
||||||
|
"chrono",
|
||||||
|
"form_urlencoded",
|
||||||
|
"http",
|
||||||
|
"k8s-openapi",
|
||||||
|
"serde",
|
||||||
|
"serde-value",
|
||||||
|
"serde_json",
|
||||||
|
"thiserror",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "lazy_static"
|
name = "lazy_static"
|
||||||
version = "1.5.0"
|
version = "1.5.0"
|
||||||
@ -1279,6 +1475,21 @@ version = "0.3.1"
|
|||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "c08d65885ee38876c4f86fa503fb49d7b507c2b62552df7c70b2fce627e06381"
|
checksum = "c08d65885ee38876c4f86fa503fb49d7b507c2b62552df7c70b2fce627e06381"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "openssl-probe"
|
||||||
|
version = "0.2.1"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "7c87def4c32ab89d880effc9e097653c8da5d6ef28e6b539d313baaacfbafcbe"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "ordered-float"
|
||||||
|
version = "2.10.1"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "68f19d67e5a2795c94e73e0bb1cc1a7edeb2e28efd39e2e1c9b7a40c1108b11c"
|
||||||
|
dependencies = [
|
||||||
|
"num-traits",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "parking"
|
name = "parking"
|
||||||
version = "2.2.1"
|
version = "2.2.1"
|
||||||
@ -1344,6 +1555,48 @@ version = "2.3.2"
|
|||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "9b4f627cb1b25917193a259e49bdad08f671f8d9708acfd5fe0a8c1455d87220"
|
checksum = "9b4f627cb1b25917193a259e49bdad08f671f8d9708acfd5fe0a8c1455d87220"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "pest"
|
||||||
|
version = "2.9.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "5a07a60cc7a4d00c91f95c685609d1d2f79050e6804b70ebedd7650f0b839bcf"
|
||||||
|
dependencies = [
|
||||||
|
"memchr",
|
||||||
|
"ucd-trie",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "pest_derive"
|
||||||
|
version = "2.9.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "b3a83744a5c8455b8b3e0dc5031362780a347c878bdd11584d1a8984228cc88d"
|
||||||
|
dependencies = [
|
||||||
|
"pest",
|
||||||
|
"pest_generator",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "pest_generator"
|
||||||
|
version = "2.9.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "e0cd3451aa3de60d4b9a1e736885e4dea6b31617598026f12256ad566d63304a"
|
||||||
|
dependencies = [
|
||||||
|
"pest",
|
||||||
|
"pest_meta",
|
||||||
|
"proc-macro2",
|
||||||
|
"quote",
|
||||||
|
"syn 2.0.119",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "pest_meta"
|
||||||
|
version = "2.9.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "e04d3a0849e241d7dfce834c83b1c5edc8622009e8dd51a12ba1927c32f05496"
|
||||||
|
dependencies = [
|
||||||
|
"pest",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "pin-project-lite"
|
name = "pin-project-lite"
|
||||||
version = "0.2.17"
|
version = "0.2.17"
|
||||||
@ -1575,6 +1828,18 @@ dependencies = [
|
|||||||
"zeroize",
|
"zeroize",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "rustls-native-certs"
|
||||||
|
version = "0.8.4"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "dab5152771c58876a2146916e53e35057e1a4dfa2b9df0f0305b07f611fdea4d"
|
||||||
|
dependencies = [
|
||||||
|
"openssl-probe",
|
||||||
|
"rustls-pki-types",
|
||||||
|
"schannel",
|
||||||
|
"security-framework",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "rustls-pki-types"
|
name = "rustls-pki-types"
|
||||||
version = "1.15.1"
|
version = "1.15.1"
|
||||||
@ -1607,12 +1872,53 @@ version = "1.0.23"
|
|||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "9774ba4a74de5f7b1c1451ed6cd5285a32eddb5cccb8cc655a4e50009e06477f"
|
checksum = "9774ba4a74de5f7b1c1451ed6cd5285a32eddb5cccb8cc655a4e50009e06477f"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "schannel"
|
||||||
|
version = "0.1.29"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "91c1b7e4904c873ef0710c1f407dde2e6287de2bebc1bbbf7d430bb7cbffd939"
|
||||||
|
dependencies = [
|
||||||
|
"windows-sys 0.61.2",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "scopeguard"
|
name = "scopeguard"
|
||||||
version = "1.2.0"
|
version = "1.2.0"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "94143f37725109f92c262ed2cf5e59bce7498c01bcc1502d7b9afe439a4e9f49"
|
checksum = "94143f37725109f92c262ed2cf5e59bce7498c01bcc1502d7b9afe439a4e9f49"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "secrecy"
|
||||||
|
version = "0.10.3"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "e891af845473308773346dc847b2c23ee78fe442e0472ac50e22a18a93d3ae5a"
|
||||||
|
dependencies = [
|
||||||
|
"zeroize",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "security-framework"
|
||||||
|
version = "3.7.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "b7f4bc775c73d9a02cde8bf7b2ec4c9d12743edf609006c7facc23998404cd1d"
|
||||||
|
dependencies = [
|
||||||
|
"bitflags",
|
||||||
|
"core-foundation",
|
||||||
|
"core-foundation-sys",
|
||||||
|
"libc",
|
||||||
|
"security-framework-sys",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "security-framework-sys"
|
||||||
|
version = "2.17.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "6ce2691df843ecc5d231c0b14ece2acc3efb62c0a398c7e1d875f3983ce020e3"
|
||||||
|
dependencies = [
|
||||||
|
"core-foundation-sys",
|
||||||
|
"libc",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "serde"
|
name = "serde"
|
||||||
version = "1.0.229"
|
version = "1.0.229"
|
||||||
@ -1623,6 +1929,16 @@ dependencies = [
|
|||||||
"serde_derive",
|
"serde_derive",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "serde-value"
|
||||||
|
version = "0.7.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "f3a1a3341211875ef120e117ea7fd5228530ae7e7036a779fdc9117be6b3282c"
|
||||||
|
dependencies = [
|
||||||
|
"ordered-float",
|
||||||
|
"serde",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "serde_core"
|
name = "serde_core"
|
||||||
version = "1.0.229"
|
version = "1.0.229"
|
||||||
@ -1679,6 +1995,19 @@ dependencies = [
|
|||||||
"serde",
|
"serde",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "serde_yaml"
|
||||||
|
version = "0.9.34+deprecated"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "6a8b1a1a2ebf674015cc02edccce75287f1a0130d394307b36743c2f5d504b47"
|
||||||
|
dependencies = [
|
||||||
|
"indexmap",
|
||||||
|
"itoa",
|
||||||
|
"ryu",
|
||||||
|
"serde",
|
||||||
|
"unsafe-libyaml",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "sha1"
|
name = "sha1"
|
||||||
version = "0.10.7"
|
version = "0.10.7"
|
||||||
@ -2192,6 +2521,7 @@ dependencies = [
|
|||||||
"bytes",
|
"bytes",
|
||||||
"futures-core",
|
"futures-core",
|
||||||
"futures-sink",
|
"futures-sink",
|
||||||
|
"libc",
|
||||||
"pin-project-lite",
|
"pin-project-lite",
|
||||||
"tokio",
|
"tokio",
|
||||||
]
|
]
|
||||||
@ -2207,6 +2537,7 @@ dependencies = [
|
|||||||
"pin-project-lite",
|
"pin-project-lite",
|
||||||
"sync_wrapper",
|
"sync_wrapper",
|
||||||
"tokio",
|
"tokio",
|
||||||
|
"tokio-util",
|
||||||
"tower-layer",
|
"tower-layer",
|
||||||
"tower-service",
|
"tower-service",
|
||||||
"tracing",
|
"tracing",
|
||||||
@ -2218,6 +2549,7 @@ version = "0.6.11"
|
|||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "4cfcf7e2740e6fc6d4d688b4ef00650406bb94adf4731e43c096c3a19fe40840"
|
checksum = "4cfcf7e2740e6fc6d4d688b4ef00650406bb94adf4731e43c096c3a19fe40840"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
|
"base64 0.22.1",
|
||||||
"bitflags",
|
"bitflags",
|
||||||
"bytes",
|
"bytes",
|
||||||
"futures-core",
|
"futures-core",
|
||||||
@ -2312,12 +2644,24 @@ dependencies = [
|
|||||||
"tracing-log",
|
"tracing-log",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "try-lock"
|
||||||
|
version = "0.2.5"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "e421abadd41a4225275504ea4d6566923418b7f05506fbc9c0fe86ba7396114b"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "typenum"
|
name = "typenum"
|
||||||
version = "1.20.1"
|
version = "1.20.1"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "b6f5e870be6c3b371b77fe0ee0bafb859fa4964b4404c27de1d380043c4dda20"
|
checksum = "b6f5e870be6c3b371b77fe0ee0bafb859fa4964b4404c27de1d380043c4dda20"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "ucd-trie"
|
||||||
|
version = "0.1.7"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "2896d95c02a80c6d6a5d6e953d479f5ddf2dfdb6a244441010e373ac0fb88971"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "unicase"
|
name = "unicase"
|
||||||
version = "2.9.0"
|
version = "2.9.0"
|
||||||
@ -2361,6 +2705,12 @@ dependencies = [
|
|||||||
"subtle",
|
"subtle",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "unsafe-libyaml"
|
||||||
|
version = "0.2.11"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "673aac59facbab8a9007c7f6108d11f63b603f7cabff99fabf650fea5c32b861"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "untrusted"
|
name = "untrusted"
|
||||||
version = "0.9.0"
|
version = "0.9.0"
|
||||||
@ -2440,6 +2790,15 @@ version = "0.9.5"
|
|||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "0b928f33d975fc6ad9f86c8f283853ad26bdd5b10b7f1542aa2fa15e2289105a"
|
checksum = "0b928f33d975fc6ad9f86c8f283853ad26bdd5b10b7f1542aa2fa15e2289105a"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "want"
|
||||||
|
version = "0.3.1"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "bfa7760aed19e106de2c7c0b581b509f2f25d3dacaf737cb82ac61bc6d760b0e"
|
||||||
|
dependencies = [
|
||||||
|
"try-lock",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "wasi"
|
name = "wasi"
|
||||||
version = "0.11.1+wasi-snapshot-preview1"
|
version = "0.11.1+wasi-snapshot-preview1"
|
||||||
|
|||||||
105
backend/crates/api/tests/cluster.rs
Normal file
105
backend/crates/api/tests/cluster.rs
Normal file
@ -0,0 +1,105 @@
|
|||||||
|
//! WP-12: /api/cluster
|
||||||
|
mod common;
|
||||||
|
|
||||||
|
use axum::http::StatusCode;
|
||||||
|
use common::{get, post, test_app_with_admin};
|
||||||
|
use serde_json::json;
|
||||||
|
|
||||||
|
const ADMIN: &str = "admin@example.com";
|
||||||
|
const PW: &str = "admin-password-123";
|
||||||
|
|
||||||
|
#[tokio::test]
|
||||||
|
async fn overview_and_admin_actions() {
|
||||||
|
let app = test_app_with_admin().await;
|
||||||
|
let token = common::login(&app, ADMIN, PW).await.access;
|
||||||
|
|
||||||
|
let res = get(&app, "/api/cluster/overview", Some(&token)).await;
|
||||||
|
assert_eq!(res.status, StatusCode::OK, "{}", res.json);
|
||||||
|
assert_eq!(res.json["nodes"][0]["version"], "v1.32.13");
|
||||||
|
let w = res.json["workloads"].as_array().unwrap();
|
||||||
|
assert!(w
|
||||||
|
.iter()
|
||||||
|
.any(|w| w["name"] == "gitea" && w["kind"] == "deployment"));
|
||||||
|
assert!(res.json["images"].as_array().unwrap().len() >= 2);
|
||||||
|
|
||||||
|
let base = "/api/cluster/workloads/gitea/deployment/gitea";
|
||||||
|
assert_eq!(
|
||||||
|
post(&app, &format!("{base}/restart"), json!({}), Some(&token))
|
||||||
|
.await
|
||||||
|
.status,
|
||||||
|
StatusCode::NO_CONTENT
|
||||||
|
);
|
||||||
|
assert_eq!(
|
||||||
|
post(
|
||||||
|
&app,
|
||||||
|
&format!("{base}/scale"),
|
||||||
|
json!({"replicas": 2}),
|
||||||
|
Some(&token)
|
||||||
|
)
|
||||||
|
.await
|
||||||
|
.status,
|
||||||
|
StatusCode::NO_CONTENT
|
||||||
|
);
|
||||||
|
assert_eq!(
|
||||||
|
post(
|
||||||
|
&app,
|
||||||
|
&format!("{base}/scale"),
|
||||||
|
json!({"replicas": 500}),
|
||||||
|
Some(&token)
|
||||||
|
)
|
||||||
|
.await
|
||||||
|
.status,
|
||||||
|
StatusCode::UNPROCESSABLE_ENTITY
|
||||||
|
);
|
||||||
|
assert_eq!(
|
||||||
|
post(
|
||||||
|
&app,
|
||||||
|
&format!("{base}/image"),
|
||||||
|
json!({"image": "gitea/gitea:1.23.0"}),
|
||||||
|
Some(&token)
|
||||||
|
)
|
||||||
|
.await
|
||||||
|
.status,
|
||||||
|
StatusCode::NO_CONTENT
|
||||||
|
);
|
||||||
|
assert_eq!(
|
||||||
|
post(
|
||||||
|
&app,
|
||||||
|
"/api/cluster/workloads/gitea/cronjob/x/restart",
|
||||||
|
json!({}),
|
||||||
|
Some(&token)
|
||||||
|
)
|
||||||
|
.await
|
||||||
|
.status,
|
||||||
|
StatusCode::NOT_FOUND
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[tokio::test]
|
||||||
|
async fn actions_are_admin_only_and_overview_needs_auth() {
|
||||||
|
let app = test_app_with_admin().await;
|
||||||
|
let admin = common::login(&app, ADMIN, PW).await.access;
|
||||||
|
post(&app, "/api/users", json!({"email": "u@x.de", "display_name": "U", "password": "user-password-123", "role": "user"}), Some(&admin)).await;
|
||||||
|
let user = common::login(&app, "u@x.de", "user-password-123")
|
||||||
|
.await
|
||||||
|
.access;
|
||||||
|
assert_eq!(
|
||||||
|
get(&app, "/api/cluster/overview", Some(&user)).await.status,
|
||||||
|
StatusCode::OK
|
||||||
|
);
|
||||||
|
assert_eq!(
|
||||||
|
post(
|
||||||
|
&app,
|
||||||
|
"/api/cluster/workloads/gitea/deployment/gitea/restart",
|
||||||
|
json!({}),
|
||||||
|
Some(&user)
|
||||||
|
)
|
||||||
|
.await
|
||||||
|
.status,
|
||||||
|
StatusCode::FORBIDDEN
|
||||||
|
);
|
||||||
|
assert_eq!(
|
||||||
|
get(&app, "/api/cluster/overview", None).await.status,
|
||||||
|
StatusCode::UNAUTHORIZED
|
||||||
|
);
|
||||||
|
}
|
||||||
37
backend/crates/application/src/cluster_service.rs
Normal file
37
backend/crates/application/src/cluster_service.rs
Normal file
@ -0,0 +1,37 @@
|
|||||||
|
use std::sync::Arc;
|
||||||
|
|
||||||
|
use domain::cluster::{ClusterOverview, WorkloadRef};
|
||||||
|
use domain::ports::ClusterGateway;
|
||||||
|
use domain::DomainError;
|
||||||
|
|
||||||
|
pub struct ClusterService {
|
||||||
|
gateway: Arc<dyn ClusterGateway>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl ClusterService {
|
||||||
|
pub fn new(gateway: Arc<dyn ClusterGateway>) -> Self {
|
||||||
|
let _ = &gateway;
|
||||||
|
Self { gateway }
|
||||||
|
}
|
||||||
|
|
||||||
|
pub async fn overview(&self) -> Result<ClusterOverview, DomainError> {
|
||||||
|
todo!()
|
||||||
|
}
|
||||||
|
|
||||||
|
pub async fn restart(&self, _w: WorkloadRef) -> Result<(), DomainError> {
|
||||||
|
todo!()
|
||||||
|
}
|
||||||
|
|
||||||
|
pub async fn scale(&self, _w: WorkloadRef, _replicas: i32) -> Result<(), DomainError> {
|
||||||
|
todo!()
|
||||||
|
}
|
||||||
|
|
||||||
|
pub async fn set_image(
|
||||||
|
&self,
|
||||||
|
_w: WorkloadRef,
|
||||||
|
_container: Option<String>,
|
||||||
|
_image: &str,
|
||||||
|
) -> Result<(), DomainError> {
|
||||||
|
todo!()
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -1,5 +1,6 @@
|
|||||||
//! Application layer: use cases orchestrating the domain through its ports.
|
//! Application layer: use cases orchestrating the domain through its ports.
|
||||||
pub mod auth_service;
|
pub mod auth_service;
|
||||||
|
pub mod cluster_service;
|
||||||
pub mod inventory_service;
|
pub mod inventory_service;
|
||||||
pub mod jobs;
|
pub mod jobs;
|
||||||
pub mod scheduler;
|
pub mod scheduler;
|
||||||
@ -8,6 +9,7 @@ pub mod upgrade_service;
|
|||||||
pub mod user_service;
|
pub mod user_service;
|
||||||
|
|
||||||
pub use auth_service::AuthService;
|
pub use auth_service::AuthService;
|
||||||
|
pub use cluster_service::ClusterService;
|
||||||
pub use inventory_service::{InventoryService, PackageRefreshJob};
|
pub use inventory_service::{InventoryService, PackageRefreshJob};
|
||||||
pub use jobs::{JobHandler, JobLog, JobRunner};
|
pub use jobs::{JobHandler, JobLog, JobRunner};
|
||||||
pub use settings_service::SettingsService;
|
pub use settings_service::SettingsService;
|
||||||
|
|||||||
@ -399,3 +399,98 @@ impl HostUpdater for FakeUpdater {
|
|||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
use domain::cluster::{
|
||||||
|
ClusterOverview, Container, NodeInfo, VolumeClaim, Workload, WorkloadKind, WorkloadRef,
|
||||||
|
};
|
||||||
|
use domain::ports::ClusterGateway;
|
||||||
|
|
||||||
|
#[derive(Default)]
|
||||||
|
pub struct MemCluster {
|
||||||
|
pub actions: Mutex<Vec<String>>,
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn sample_overview() -> ClusterOverview {
|
||||||
|
ClusterOverview {
|
||||||
|
nodes: vec![NodeInfo {
|
||||||
|
name: "node1".into(),
|
||||||
|
version: "v1.32.13".into(),
|
||||||
|
ready: true,
|
||||||
|
os_image: "Debian GNU/Linux 12 (bookworm)".into(),
|
||||||
|
kernel: "6.1.0-42-amd64".into(),
|
||||||
|
container_runtime: "containerd://1.6.36".into(),
|
||||||
|
}],
|
||||||
|
namespaces: vec!["default".into(), "gitea".into()],
|
||||||
|
workloads: vec![
|
||||||
|
Workload {
|
||||||
|
namespace: "gitea".into(),
|
||||||
|
kind: WorkloadKind::Deployment,
|
||||||
|
name: "gitea".into(),
|
||||||
|
ready: 1,
|
||||||
|
desired: 1,
|
||||||
|
containers: vec![Container {
|
||||||
|
name: "gitea".into(),
|
||||||
|
image: "gitea/gitea:1.22.3".into(),
|
||||||
|
}],
|
||||||
|
},
|
||||||
|
Workload {
|
||||||
|
namespace: "gitea".into(),
|
||||||
|
kind: WorkloadKind::StatefulSet,
|
||||||
|
name: "gitea-postgresql".into(),
|
||||||
|
ready: 1,
|
||||||
|
desired: 1,
|
||||||
|
containers: vec![Container {
|
||||||
|
name: "postgresql".into(),
|
||||||
|
image: "bitnami/postgresql:16.4.0".into(),
|
||||||
|
}],
|
||||||
|
},
|
||||||
|
],
|
||||||
|
volume_claims: vec![VolumeClaim {
|
||||||
|
namespace: "gitea".into(),
|
||||||
|
name: "gitea-shared-storage".into(),
|
||||||
|
capacity: "10Gi".into(),
|
||||||
|
storage_class: "microk8s-hostpath".into(),
|
||||||
|
status: "Bound".into(),
|
||||||
|
}],
|
||||||
|
fetched_at: Utc::now(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[async_trait]
|
||||||
|
impl ClusterGateway for MemCluster {
|
||||||
|
async fn overview(&self) -> Result<ClusterOverview, DomainError> {
|
||||||
|
Ok(sample_overview())
|
||||||
|
}
|
||||||
|
async fn restart(&self, w: &WorkloadRef) -> Result<(), DomainError> {
|
||||||
|
self.actions.lock().unwrap().push(format!(
|
||||||
|
"restart {}/{}/{}",
|
||||||
|
w.namespace,
|
||||||
|
w.kind.as_str(),
|
||||||
|
w.name
|
||||||
|
));
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
async fn scale(&self, w: &WorkloadRef, replicas: i32) -> Result<(), DomainError> {
|
||||||
|
self.actions.lock().unwrap().push(format!(
|
||||||
|
"scale {}/{}/{} {replicas}",
|
||||||
|
w.namespace,
|
||||||
|
w.kind.as_str(),
|
||||||
|
w.name
|
||||||
|
));
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
async fn set_image(
|
||||||
|
&self,
|
||||||
|
w: &WorkloadRef,
|
||||||
|
container: &str,
|
||||||
|
image: &str,
|
||||||
|
) -> Result<(), DomainError> {
|
||||||
|
self.actions.lock().unwrap().push(format!(
|
||||||
|
"image {}/{}/{} {container}={image}",
|
||||||
|
w.namespace,
|
||||||
|
w.kind.as_str(),
|
||||||
|
w.name
|
||||||
|
));
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
91
backend/crates/application/src/tests/cluster_tests.rs
Normal file
91
backend/crates/application/src/tests/cluster_tests.rs
Normal file
@ -0,0 +1,91 @@
|
|||||||
|
use std::sync::Arc;
|
||||||
|
|
||||||
|
use domain::cluster::{WorkloadKind, WorkloadRef};
|
||||||
|
use domain::DomainError;
|
||||||
|
|
||||||
|
use crate::test_fakes::MemCluster;
|
||||||
|
use crate::ClusterService;
|
||||||
|
|
||||||
|
fn gitea() -> WorkloadRef {
|
||||||
|
WorkloadRef {
|
||||||
|
namespace: "gitea".into(),
|
||||||
|
kind: WorkloadKind::Deployment,
|
||||||
|
name: "gitea".into(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn svc() -> (Arc<MemCluster>, ClusterService) {
|
||||||
|
let gw = Arc::new(MemCluster::default());
|
||||||
|
(gw.clone(), ClusterService::new(gw))
|
||||||
|
}
|
||||||
|
|
||||||
|
#[tokio::test]
|
||||||
|
async fn overview_lists_workloads_and_distinct_images() {
|
||||||
|
let (_, s) = svc();
|
||||||
|
let o = s.overview().await.unwrap();
|
||||||
|
assert_eq!(o.workloads.len(), 2);
|
||||||
|
assert_eq!(
|
||||||
|
o.images(),
|
||||||
|
vec!["bitnami/postgresql:16.4.0", "gitea/gitea:1.22.3"]
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[tokio::test]
|
||||||
|
async fn restart_and_scale_are_forwarded_with_validation() {
|
||||||
|
let (gw, s) = svc();
|
||||||
|
s.restart(gitea()).await.unwrap();
|
||||||
|
s.scale(gitea(), 2).await.unwrap();
|
||||||
|
assert!(matches!(
|
||||||
|
s.scale(gitea(), -1).await.unwrap_err(),
|
||||||
|
DomainError::Validation(_)
|
||||||
|
));
|
||||||
|
assert!(matches!(
|
||||||
|
s.scale(gitea(), 999).await.unwrap_err(),
|
||||||
|
DomainError::Validation(_)
|
||||||
|
));
|
||||||
|
let bad = WorkloadRef {
|
||||||
|
namespace: "Bad Name".into(),
|
||||||
|
..gitea()
|
||||||
|
};
|
||||||
|
assert!(matches!(
|
||||||
|
s.restart(bad).await.unwrap_err(),
|
||||||
|
DomainError::Validation(_)
|
||||||
|
));
|
||||||
|
assert_eq!(
|
||||||
|
*gw.actions.lock().unwrap(),
|
||||||
|
vec![
|
||||||
|
"restart gitea/deployment/gitea",
|
||||||
|
"scale gitea/deployment/gitea 2"
|
||||||
|
]
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[tokio::test]
|
||||||
|
async fn set_image_defaults_to_the_first_container_and_validates_image() {
|
||||||
|
let (gw, s) = svc();
|
||||||
|
s.set_image(gitea(), None, "gitea/gitea:1.23.0")
|
||||||
|
.await
|
||||||
|
.unwrap();
|
||||||
|
s.set_image(gitea(), Some("sidecar".into()), "x/y:1")
|
||||||
|
.await
|
||||||
|
.unwrap();
|
||||||
|
assert!(matches!(
|
||||||
|
s.set_image(gitea(), None, "has space").await.unwrap_err(),
|
||||||
|
DomainError::Validation(_)
|
||||||
|
));
|
||||||
|
let unknown = WorkloadRef {
|
||||||
|
name: "nope".into(),
|
||||||
|
..gitea()
|
||||||
|
};
|
||||||
|
assert_eq!(
|
||||||
|
s.set_image(unknown, None, "x/y:1").await.unwrap_err(),
|
||||||
|
DomainError::NotFound
|
||||||
|
);
|
||||||
|
assert_eq!(
|
||||||
|
*gw.actions.lock().unwrap(),
|
||||||
|
vec![
|
||||||
|
"image gitea/deployment/gitea gitea=gitea/gitea:1.23.0",
|
||||||
|
"image gitea/deployment/gitea sidecar=x/y:1"
|
||||||
|
]
|
||||||
|
);
|
||||||
|
}
|
||||||
@ -1,4 +1,5 @@
|
|||||||
mod auth_service_tests;
|
mod auth_service_tests;
|
||||||
|
mod cluster_tests;
|
||||||
mod inventory_tests;
|
mod inventory_tests;
|
||||||
mod jobs_tests;
|
mod jobs_tests;
|
||||||
mod scheduler_tests;
|
mod scheduler_tests;
|
||||||
|
|||||||
110
backend/crates/domain/src/cluster.rs
Normal file
110
backend/crates/domain/src/cluster.rs
Normal file
@ -0,0 +1,110 @@
|
|||||||
|
//! Kubernetes cluster overview and workload actions.
|
||||||
|
use chrono::{DateTime, Utc};
|
||||||
|
use serde::{Deserialize, Serialize};
|
||||||
|
|
||||||
|
#[derive(Clone, Copy, Debug, PartialEq, Eq, Serialize, Deserialize)]
|
||||||
|
#[serde(rename_all = "lowercase")]
|
||||||
|
pub enum WorkloadKind {
|
||||||
|
Deployment,
|
||||||
|
StatefulSet,
|
||||||
|
DaemonSet,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl WorkloadKind {
|
||||||
|
pub fn as_str(self) -> &'static str {
|
||||||
|
match self {
|
||||||
|
WorkloadKind::Deployment => "deployment",
|
||||||
|
WorkloadKind::StatefulSet => "statefulset",
|
||||||
|
WorkloadKind::DaemonSet => "daemonset",
|
||||||
|
}
|
||||||
|
}
|
||||||
|
pub fn parse(s: &str) -> Option<Self> {
|
||||||
|
[Self::Deployment, Self::StatefulSet, Self::DaemonSet]
|
||||||
|
.into_iter()
|
||||||
|
.find(|k| k.as_str() == s)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
|
||||||
|
pub struct Container {
|
||||||
|
pub name: String,
|
||||||
|
pub image: String,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
|
||||||
|
pub struct Workload {
|
||||||
|
pub namespace: String,
|
||||||
|
pub kind: WorkloadKind,
|
||||||
|
pub name: String,
|
||||||
|
pub ready: i32,
|
||||||
|
pub desired: i32,
|
||||||
|
pub containers: Vec<Container>,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
|
||||||
|
pub struct NodeInfo {
|
||||||
|
pub name: String,
|
||||||
|
pub version: String,
|
||||||
|
pub ready: bool,
|
||||||
|
pub os_image: String,
|
||||||
|
pub kernel: String,
|
||||||
|
pub container_runtime: String,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
|
||||||
|
pub struct VolumeClaim {
|
||||||
|
pub namespace: String,
|
||||||
|
pub name: String,
|
||||||
|
pub capacity: String,
|
||||||
|
pub storage_class: String,
|
||||||
|
pub status: String,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
|
||||||
|
pub struct ClusterOverview {
|
||||||
|
pub nodes: Vec<NodeInfo>,
|
||||||
|
pub namespaces: Vec<String>,
|
||||||
|
pub workloads: Vec<Workload>,
|
||||||
|
pub volume_claims: Vec<VolumeClaim>,
|
||||||
|
pub fetched_at: DateTime<Utc>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl ClusterOverview {
|
||||||
|
/// Distinct images across all workloads (input for vulnerability scans).
|
||||||
|
pub fn images(&self) -> Vec<String> {
|
||||||
|
let mut v: Vec<String> = self
|
||||||
|
.workloads
|
||||||
|
.iter()
|
||||||
|
.flat_map(|w| w.containers.iter().map(|c| c.image.clone()))
|
||||||
|
.collect();
|
||||||
|
v.sort();
|
||||||
|
v.dedup();
|
||||||
|
v
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Reference to a workload for actions.
|
||||||
|
#[derive(Clone, Debug, PartialEq, Eq)]
|
||||||
|
pub struct WorkloadRef {
|
||||||
|
pub namespace: String,
|
||||||
|
pub kind: WorkloadKind,
|
||||||
|
pub name: String,
|
||||||
|
}
|
||||||
|
|
||||||
|
pub const MAX_REPLICAS: i32 = 20;
|
||||||
|
|
||||||
|
pub fn validate_k8s_name(name: &str) -> Result<(), crate::DomainError> {
|
||||||
|
let ok = !name.is_empty()
|
||||||
|
&& name.len() <= 253
|
||||||
|
&& name
|
||||||
|
.chars()
|
||||||
|
.all(|c| c.is_ascii_lowercase() || c.is_ascii_digit() || matches!(c, '-' | '.'));
|
||||||
|
ok.then_some(())
|
||||||
|
.ok_or_else(|| crate::DomainError::Validation(format!("invalid kubernetes name: {name}")))
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn validate_image(image: &str) -> Result<(), crate::DomainError> {
|
||||||
|
let ok = !image.is_empty() && image.len() <= 512 && !image.chars().any(char::is_whitespace);
|
||||||
|
ok.then_some(())
|
||||||
|
.ok_or_else(|| crate::DomainError::Validation(format!("invalid image reference: {image}")))
|
||||||
|
}
|
||||||
@ -1,6 +1,7 @@
|
|||||||
//! Domain layer: entities, value objects, errors and the ports (traits) the application
|
//! Domain layer: entities, value objects, errors and the ports (traits) the application
|
||||||
//! layer depends on. No I/O here.
|
//! layer depends on. No I/O here.
|
||||||
pub mod auth;
|
pub mod auth;
|
||||||
|
pub mod cluster;
|
||||||
pub mod error;
|
pub mod error;
|
||||||
pub mod host;
|
pub mod host;
|
||||||
pub mod jobs;
|
pub mod jobs;
|
||||||
|
|||||||
@ -3,6 +3,7 @@ use async_trait::async_trait;
|
|||||||
use uuid::Uuid;
|
use uuid::Uuid;
|
||||||
|
|
||||||
use crate::auth::{AccessClaims, AuthEvent, RefreshToken};
|
use crate::auth::{AccessClaims, AuthEvent, RefreshToken};
|
||||||
|
use crate::cluster::{ClusterOverview, WorkloadRef};
|
||||||
use crate::host::{Inventory, OsInfo, Package};
|
use crate::host::{Inventory, OsInfo, Package};
|
||||||
use crate::jobs::{JobKind, JobRun, JobStatus};
|
use crate::jobs::{JobKind, JobRun, JobStatus};
|
||||||
use crate::settings::SmtpSettings;
|
use crate::settings::SmtpSettings;
|
||||||
@ -101,3 +102,18 @@ pub trait LineSink: Send + Sync {
|
|||||||
pub trait HostUpdater: Send + Sync {
|
pub trait HostUpdater: Send + Sync {
|
||||||
async fn upgrade(&self, packages: &[String], out: &dyn LineSink) -> Result<(), DomainError>;
|
async fn upgrade(&self, packages: &[String], out: &dyn LineSink) -> Result<(), DomainError>;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[async_trait]
|
||||||
|
pub trait ClusterGateway: Send + Sync {
|
||||||
|
async fn overview(&self) -> Result<ClusterOverview, DomainError>;
|
||||||
|
/// Rolling restart (like `kubectl rollout restart`).
|
||||||
|
async fn restart(&self, workload: &WorkloadRef) -> Result<(), DomainError>;
|
||||||
|
async fn scale(&self, workload: &WorkloadRef, replicas: i32) -> Result<(), DomainError>;
|
||||||
|
/// Set the image of one container (= application update).
|
||||||
|
async fn set_image(
|
||||||
|
&self,
|
||||||
|
workload: &WorkloadRef,
|
||||||
|
container: &str,
|
||||||
|
image: &str,
|
||||||
|
) -> Result<(), DomainError>;
|
||||||
|
}
|
||||||
|
|||||||
@ -19,6 +19,8 @@ serde_json.workspace = true
|
|||||||
tokio.workspace = true
|
tokio.workspace = true
|
||||||
sqlx.workspace = true
|
sqlx.workspace = true
|
||||||
uuid.workspace = true
|
uuid.workspace = true
|
||||||
|
kube = { version = "0.99", default-features = false, features = ["client", "rustls-tls"] }
|
||||||
|
k8s-openapi = { version = "0.24", features = ["v1_32"] }
|
||||||
|
|
||||||
[dev-dependencies]
|
[dev-dependencies]
|
||||||
tokio.workspace = true
|
tokio.workspace = true
|
||||||
|
|||||||
21
frontend/e2e/cluster.spec.ts
Normal file
21
frontend/e2e/cluster.spec.ts
Normal file
@ -0,0 +1,21 @@
|
|||||||
|
import { test, expect } from '@playwright/test'
|
||||||
|
|
||||||
|
test('cluster page shows node, workloads and lets an admin restart a workload', async ({
|
||||||
|
page,
|
||||||
|
}) => {
|
||||||
|
await page.goto('/login')
|
||||||
|
await page.getByLabel('Email').fill('admin@example.com')
|
||||||
|
await page.getByLabel('Password').fill('admin-password-123')
|
||||||
|
await page.getByRole('button', { name: 'Sign in' }).click()
|
||||||
|
await page.getByRole('link', { name: 'Kubernetes' }).click()
|
||||||
|
|
||||||
|
await expect(page.getByTestId('node-version')).toContainText('v1.32')
|
||||||
|
const row = page.getByRole('row', { name: /gitea-postgresql/ })
|
||||||
|
await expect(row).toContainText('statefulset')
|
||||||
|
page.once('dialog', (d) => d.accept())
|
||||||
|
await page
|
||||||
|
.getByRole('row', { name: /^gitea deployment/ })
|
||||||
|
.getByRole('button', { name: 'Restart' })
|
||||||
|
.click()
|
||||||
|
await expect(page.getByRole('status')).toContainText('Restart requested')
|
||||||
|
})
|
||||||
@ -91,3 +91,40 @@ export interface InventoryResponse {
|
|||||||
packages: Package[]
|
packages: Package[]
|
||||||
summary: { total: number; upgradable: number; security: number }
|
summary: { total: number; upgradable: number; security: number }
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export type WorkloadKind = 'deployment' | 'statefulset' | 'daemonset'
|
||||||
|
|
||||||
|
export interface Workload {
|
||||||
|
namespace: string
|
||||||
|
kind: WorkloadKind
|
||||||
|
name: string
|
||||||
|
ready: number
|
||||||
|
desired: number
|
||||||
|
containers: { name: string; image: string }[]
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface NodeInfo {
|
||||||
|
name: string
|
||||||
|
version: string
|
||||||
|
ready: boolean
|
||||||
|
os_image: string
|
||||||
|
kernel: string
|
||||||
|
container_runtime: string
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface VolumeClaim {
|
||||||
|
namespace: string
|
||||||
|
name: string
|
||||||
|
capacity: string
|
||||||
|
storage_class: string
|
||||||
|
status: string
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface ClusterOverview {
|
||||||
|
nodes: NodeInfo[]
|
||||||
|
namespaces: string[]
|
||||||
|
workloads: Workload[]
|
||||||
|
volume_claims: VolumeClaim[]
|
||||||
|
images: string[]
|
||||||
|
fetched_at: string
|
||||||
|
}
|
||||||
|
|||||||
45
frontend/src/components/WorkloadTable.test.ts
Normal file
45
frontend/src/components/WorkloadTable.test.ts
Normal file
@ -0,0 +1,45 @@
|
|||||||
|
import { mount } from '@vue/test-utils'
|
||||||
|
import WorkloadTable from './WorkloadTable.vue'
|
||||||
|
import type { Workload } from '../api/types'
|
||||||
|
|
||||||
|
const workloads: Workload[] = [
|
||||||
|
{
|
||||||
|
namespace: 'gitea',
|
||||||
|
kind: 'deployment',
|
||||||
|
name: 'gitea',
|
||||||
|
ready: 1,
|
||||||
|
desired: 1,
|
||||||
|
containers: [{ name: 'gitea', image: 'gitea/gitea:1.22.3' }],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
namespace: 'gitea',
|
||||||
|
kind: 'statefulset',
|
||||||
|
name: 'gitea-postgresql',
|
||||||
|
ready: 0,
|
||||||
|
desired: 1,
|
||||||
|
containers: [{ name: 'postgresql', image: 'bitnami/postgresql:16.4.0' }],
|
||||||
|
},
|
||||||
|
]
|
||||||
|
|
||||||
|
describe('WorkloadTable', () => {
|
||||||
|
it('renders workloads with readiness and images', () => {
|
||||||
|
const w = mount(WorkloadTable, { props: { workloads, canAct: false } })
|
||||||
|
const rows = w.findAll('tbody tr')
|
||||||
|
expect(rows).toHaveLength(2)
|
||||||
|
expect(rows[0].text()).toContain('gitea/gitea:1.22.3')
|
||||||
|
expect(rows[0].text()).toContain('1/1')
|
||||||
|
expect(rows[1].text()).toContain('0/1')
|
||||||
|
expect(rows[1].classes().join(' ')).toContain('red')
|
||||||
|
expect(w.findAll('button[name=restart]')).toHaveLength(0)
|
||||||
|
})
|
||||||
|
|
||||||
|
it('emits actions for admins', async () => {
|
||||||
|
const w = mount(WorkloadTable, { props: { workloads, canAct: true } })
|
||||||
|
await w.findAll('button[name=restart]')[0].trigger('click')
|
||||||
|
expect(w.emitted('restart')![0][0]).toMatchObject({ name: 'gitea', kind: 'deployment' })
|
||||||
|
await w.findAll('button[name=scale]')[1].trigger('click')
|
||||||
|
expect(w.emitted('scale')![0][0]).toMatchObject({ name: 'gitea-postgresql' })
|
||||||
|
await w.findAll('button[name=image]')[0].trigger('click')
|
||||||
|
expect(w.emitted('image')![0][0]).toMatchObject({ name: 'gitea' })
|
||||||
|
})
|
||||||
|
})
|
||||||
Reference in New Issue
Block a user