mirror of
https://github.com/rustfs/rustfs.git
synced 2026-07-31 10:32:24 +00:00
Compare commits
1 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| fd79678559 |
Generated
+32
-89
@@ -1528,7 +1528,7 @@ version = "0.10.4"
|
|||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "3078c7629b62d3f0439517fa394996acacc5cbc91c5a20d8c658e77abd503a71"
|
checksum = "3078c7629b62d3f0439517fa394996acacc5cbc91c5a20d8c658e77abd503a71"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"generic-array 0.14.7",
|
"generic-array 0.14.9",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
@@ -1547,7 +1547,7 @@ version = "0.3.3"
|
|||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "a8894febbff9f758034a5b8e12d87918f56dfc64a8e1fe757d65e29041538d93"
|
checksum = "a8894febbff9f758034a5b8e12d87918f56dfc64a8e1fe757d65e29041538d93"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"generic-array 0.14.7",
|
"generic-array 0.14.9",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
@@ -1598,7 +1598,7 @@ version = "3.9.3"
|
|||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "6dee98b0db6a962de883bf5d20362dee4d7ca0d12fe39a7c6c73c844e1cd7c1f"
|
checksum = "6dee98b0db6a962de883bf5d20362dee4d7ca0d12fe39a7c6c73c844e1cd7c1f"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"darling 0.20.11",
|
"darling 0.23.0",
|
||||||
"ident_case",
|
"ident_case",
|
||||||
"prettyplease",
|
"prettyplease",
|
||||||
"proc-macro2",
|
"proc-macro2",
|
||||||
@@ -1870,7 +1870,7 @@ version = "0.4.4"
|
|||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "773f3b9af64447d2ce9850330c473515014aa235e6a783b02db81ff39e4a3dad"
|
checksum = "773f3b9af64447d2ce9850330c473515014aa235e6a783b02db81ff39e4a3dad"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"crypto-common 0.1.7",
|
"crypto-common 0.1.6",
|
||||||
"inout 0.1.4",
|
"inout 0.1.4",
|
||||||
]
|
]
|
||||||
|
|
||||||
@@ -2328,7 +2328,7 @@ version = "0.5.5"
|
|||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "0dc92fb57ca44df6db8059111ab3af99a63d5d0f8375d9972e319a379c6bab76"
|
checksum = "0dc92fb57ca44df6db8059111ab3af99a63d5d0f8375d9972e319a379c6bab76"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"generic-array 0.14.7",
|
"generic-array 0.14.9",
|
||||||
"rand_core 0.6.4",
|
"rand_core 0.6.4",
|
||||||
"subtle",
|
"subtle",
|
||||||
"zeroize",
|
"zeroize",
|
||||||
@@ -2353,11 +2353,11 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "crypto-common"
|
name = "crypto-common"
|
||||||
version = "0.1.7"
|
version = "0.1.6"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "78c8292055d1c1df0cce5d180393dc8cce0abec0a7102adb6c7b1eef6016d60a"
|
checksum = "1bfb12502f3fc46cca1bb51ac28df9d618d813cdc3d2f25b9fe775a34af26bb3"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"generic-array 0.14.7",
|
"generic-array 0.14.9",
|
||||||
"typenum",
|
"typenum",
|
||||||
]
|
]
|
||||||
|
|
||||||
@@ -3554,7 +3554,7 @@ checksum = "9ed9a281f7bc9b7576e61468ba615a66a5c8cfdff42420a70aa82701a3b1e292"
|
|||||||
dependencies = [
|
dependencies = [
|
||||||
"block-buffer 0.10.4",
|
"block-buffer 0.10.4",
|
||||||
"const-oid 0.9.6",
|
"const-oid 0.9.6",
|
||||||
"crypto-common 0.1.7",
|
"crypto-common 0.1.6",
|
||||||
"subtle",
|
"subtle",
|
||||||
]
|
]
|
||||||
|
|
||||||
@@ -3672,7 +3672,6 @@ dependencies = [
|
|||||||
"flate2",
|
"flate2",
|
||||||
"futures",
|
"futures",
|
||||||
"hex",
|
"hex",
|
||||||
"hotpath",
|
|
||||||
"http 1.5.0",
|
"http 1.5.0",
|
||||||
"http-body-util",
|
"http-body-util",
|
||||||
"hyper",
|
"hyper",
|
||||||
@@ -3814,7 +3813,7 @@ dependencies = [
|
|||||||
"crypto-bigint 0.5.5",
|
"crypto-bigint 0.5.5",
|
||||||
"digest 0.10.7",
|
"digest 0.10.7",
|
||||||
"ff 0.13.1",
|
"ff 0.13.1",
|
||||||
"generic-array 0.14.7",
|
"generic-array 0.14.9",
|
||||||
"group 0.13.0",
|
"group 0.13.0",
|
||||||
"hkdf 0.12.4",
|
"hkdf 0.12.4",
|
||||||
"pem-rfc7468 0.7.0",
|
"pem-rfc7468 0.7.0",
|
||||||
@@ -4259,9 +4258,9 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "generic-array"
|
name = "generic-array"
|
||||||
version = "0.14.7"
|
version = "0.14.9"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "85649ca51fd72272d7821adaf274ad91c288277713d9c18820d8499a7ff69e9a"
|
checksum = "4bb6743198531e02858aeaea5398fcc883e71851fcbcb5a2f773e2fb6cb1edf2"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"typenum",
|
"typenum",
|
||||||
"version_check",
|
"version_check",
|
||||||
@@ -4274,7 +4273,7 @@ version = "1.4.4"
|
|||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "ab4e5aa225bc56696909483320f0ff9b600f1a971b52e07a17d70f3d9b43254b"
|
checksum = "ab4e5aa225bc56696909483320f0ff9b600f1a971b52e07a17d70f3d9b43254b"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"generic-array 0.14.7",
|
"generic-array 0.14.9",
|
||||||
"rustversion",
|
"rustversion",
|
||||||
"typenum",
|
"typenum",
|
||||||
]
|
]
|
||||||
@@ -4373,9 +4372,9 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "google-cloud-auth"
|
name = "google-cloud-auth"
|
||||||
version = "1.15.0"
|
version = "1.14.0"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "f54aab44c16b8463ae11b165a87c3d484780231f157bb1ed65843d591beb5abd"
|
checksum = "a3494870d06f3cbbb3561ada6f234982549e3a2fb31e719ef258e6eadb9ae09a"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"async-trait",
|
"async-trait",
|
||||||
"aws-lc-rs",
|
"aws-lc-rs",
|
||||||
@@ -4402,9 +4401,9 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "google-cloud-gax"
|
name = "google-cloud-gax"
|
||||||
version = "1.13.0"
|
version = "1.12.0"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "b9a46dd0fd026bbc4a5d84e6ab0c941cee6e3b057976a0bb107fdb5238ce598f"
|
checksum = "3103a4a9013f1aed573ca56e19a9680b0211643a99ea85caf524b397d6be8be3"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"bytes",
|
"bytes",
|
||||||
"futures",
|
"futures",
|
||||||
@@ -4421,9 +4420,9 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "google-cloud-gax-internal"
|
name = "google-cloud-gax-internal"
|
||||||
version = "0.7.16"
|
version = "0.7.15"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "fb04c54317ace06d489213f761797240b3046142a9b7ce6b9a82a9d134e193d1"
|
checksum = "c0df265fba091ed7e00ecd0755009423310163f8b52820f007b6b4d97f4c6617"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"bytes",
|
"bytes",
|
||||||
"futures",
|
"futures",
|
||||||
@@ -4525,9 +4524,9 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "google-cloud-storage"
|
name = "google-cloud-storage"
|
||||||
version = "1.17.0"
|
version = "1.16.0"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "9227f65175fa91a6e41f246797917697efdadfe09dd8ea84ad8b737a71efbd28"
|
checksum = "dc4b1d78c88db5c2530b12461e373a7d0d3a6caa3f6c1fc14e5d824cf2aeb307"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"async-trait",
|
"async-trait",
|
||||||
"base64 0.22.1",
|
"base64 0.22.1",
|
||||||
@@ -4578,9 +4577,9 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "google-cloud-wkt"
|
name = "google-cloud-wkt"
|
||||||
version = "1.7.0"
|
version = "1.6.0"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "7fccf98cfd5481a5f5a285181ab0c62123d7d47cd2bb7299448440649349e4e7"
|
checksum = "46df1fcc3ab69164af3f4199ed21f45b5dbc56d9f03211eb4fa20116d442364b"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"base64 0.22.1",
|
"base64 0.22.1",
|
||||||
"bytes",
|
"bytes",
|
||||||
@@ -4921,7 +4920,6 @@ dependencies = [
|
|||||||
"async-trait",
|
"async-trait",
|
||||||
"cfg-if",
|
"cfg-if",
|
||||||
"crossbeam-channel",
|
"crossbeam-channel",
|
||||||
"flate2",
|
|
||||||
"futures-channel",
|
"futures-channel",
|
||||||
"futures-util",
|
"futures-util",
|
||||||
"hdrhistogram",
|
"hdrhistogram",
|
||||||
@@ -4929,7 +4927,6 @@ dependencies = [
|
|||||||
"hotpath-meta",
|
"hotpath-meta",
|
||||||
"http 1.5.0",
|
"http 1.5.0",
|
||||||
"libc",
|
"libc",
|
||||||
"object 0.36.7",
|
|
||||||
"parking_lot",
|
"parking_lot",
|
||||||
"pin-project-lite",
|
"pin-project-lite",
|
||||||
"prettytable-rs",
|
"prettytable-rs",
|
||||||
@@ -4937,7 +4934,6 @@ dependencies = [
|
|||||||
"regex",
|
"regex",
|
||||||
"reqwest",
|
"reqwest",
|
||||||
"reqwest-middleware",
|
"reqwest-middleware",
|
||||||
"rustc-demangle",
|
|
||||||
"serde",
|
"serde",
|
||||||
"serde_json",
|
"serde_json",
|
||||||
"tiny_http",
|
"tiny_http",
|
||||||
@@ -5051,9 +5047,9 @@ checksum = "15cdd26707701c53297e2fa6afb323d55fbc1d0810c3aec078ae3ef0424c3c15"
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "hybrid-array"
|
name = "hybrid-array"
|
||||||
version = "0.4.14"
|
version = "0.4.13"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "707114b52a152fa7bdb290cd7cd5912d9467273b6d74e21b8d81aca1f8533f6b"
|
checksum = "818356c5132c1fede50f837ca96afbe78ff42413047f4abb886217845e1b6c8c"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"ctutils",
|
"ctutils",
|
||||||
"subtle",
|
"subtle",
|
||||||
@@ -5301,7 +5297,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
|
|||||||
checksum = "879f10e63c20629ecabbb64a8010319738c66a5cd0c29b02d63d272b03751d01"
|
checksum = "879f10e63c20629ecabbb64a8010319738c66a5cd0c29b02d63d272b03751d01"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"block-padding 0.3.3",
|
"block-padding 0.3.3",
|
||||||
"generic-array 0.14.7",
|
"generic-array 0.14.9",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
@@ -6811,15 +6807,6 @@ dependencies = [
|
|||||||
"objc2-core-foundation",
|
"objc2-core-foundation",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "object"
|
|
||||||
version = "0.36.7"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "62948e14d923ea95ea2c7c86c71013138b66525b86bdc08d2dcc262bdb497b87"
|
|
||||||
dependencies = [
|
|
||||||
"memchr",
|
|
||||||
]
|
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "object"
|
name = "object"
|
||||||
version = "0.37.3"
|
version = "0.37.3"
|
||||||
@@ -7857,7 +7844,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
|
|||||||
checksum = "be769465445e8c1474e9c5dac2018218498557af32d9ed057325ec9a41ae81bf"
|
checksum = "be769465445e8c1474e9c5dac2018218498557af32d9ed057325ec9a41ae81bf"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"heck",
|
"heck",
|
||||||
"itertools 0.13.0",
|
"itertools 0.10.5",
|
||||||
"log",
|
"log",
|
||||||
"multimap",
|
"multimap",
|
||||||
"once_cell",
|
"once_cell",
|
||||||
@@ -7877,7 +7864,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
|
|||||||
checksum = "03da047801ff44bb6a4d407d4860c05fd70bb81714e6b2f3812603d5b145b042"
|
checksum = "03da047801ff44bb6a4d407d4860c05fd70bb81714e6b2f3812603d5b145b042"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"heck",
|
"heck",
|
||||||
"itertools 0.13.0",
|
"itertools 0.10.5",
|
||||||
"log",
|
"log",
|
||||||
"multimap",
|
"multimap",
|
||||||
"petgraph 0.8.3",
|
"petgraph 0.8.3",
|
||||||
@@ -7898,7 +7885,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
|
|||||||
checksum = "8a56d757972c98b346a9b766e3f02746cde6dd1cd1d1d563472929fdd74bec4d"
|
checksum = "8a56d757972c98b346a9b766e3f02746cde6dd1cd1d1d563472929fdd74bec4d"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"anyhow",
|
"anyhow",
|
||||||
"itertools 0.13.0",
|
"itertools 0.10.5",
|
||||||
"proc-macro2",
|
"proc-macro2",
|
||||||
"quote",
|
"quote",
|
||||||
"syn 2.0.119",
|
"syn 2.0.119",
|
||||||
@@ -7911,7 +7898,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
|
|||||||
checksum = "b570b25f7617e43d59005d0990ccb79e950a423952cea19671b7a876da390adf"
|
checksum = "b570b25f7617e43d59005d0990ccb79e950a423952cea19671b7a876da390adf"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"anyhow",
|
"anyhow",
|
||||||
"itertools 0.13.0",
|
"itertools 0.10.5",
|
||||||
"proc-macro2",
|
"proc-macro2",
|
||||||
"quote",
|
"quote",
|
||||||
"syn 2.0.119",
|
"syn 2.0.119",
|
||||||
@@ -9040,7 +9027,6 @@ dependencies = [
|
|||||||
"const-str",
|
"const-str",
|
||||||
"futures",
|
"futures",
|
||||||
"hashbrown 0.17.1",
|
"hashbrown 0.17.1",
|
||||||
"hotpath",
|
|
||||||
"metrics",
|
"metrics",
|
||||||
"rustfs-config",
|
"rustfs-config",
|
||||||
"rustfs-s3-types",
|
"rustfs-s3-types",
|
||||||
@@ -9061,7 +9047,6 @@ dependencies = [
|
|||||||
"base64-simd",
|
"base64-simd",
|
||||||
"bytes",
|
"bytes",
|
||||||
"crc-fast",
|
"crc-fast",
|
||||||
"hotpath",
|
|
||||||
"http 1.5.0",
|
"http 1.5.0",
|
||||||
"md-5 0.11.0",
|
"md-5 0.11.0",
|
||||||
"pretty_assertions",
|
"pretty_assertions",
|
||||||
@@ -9075,7 +9060,6 @@ name = "rustfs-common"
|
|||||||
version = "1.0.0-beta.12"
|
version = "1.0.0-beta.12"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"chrono",
|
"chrono",
|
||||||
"hotpath",
|
|
||||||
"metrics",
|
"metrics",
|
||||||
"rmp-serde",
|
"rmp-serde",
|
||||||
"s3s",
|
"s3s",
|
||||||
@@ -9090,7 +9074,6 @@ dependencies = [
|
|||||||
name = "rustfs-concurrency"
|
name = "rustfs-concurrency"
|
||||||
version = "1.0.0-beta.12"
|
version = "1.0.0-beta.12"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"hotpath",
|
|
||||||
"insta",
|
"insta",
|
||||||
"rustfs-io-core",
|
"rustfs-io-core",
|
||||||
"serde",
|
"serde",
|
||||||
@@ -9104,7 +9087,6 @@ name = "rustfs-config"
|
|||||||
version = "1.0.0-beta.12"
|
version = "1.0.0-beta.12"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"const-str",
|
"const-str",
|
||||||
"hotpath",
|
|
||||||
"serde",
|
"serde",
|
||||||
"serde_json",
|
"serde_json",
|
||||||
]
|
]
|
||||||
@@ -9115,7 +9097,6 @@ version = "1.0.0-beta.12"
|
|||||||
dependencies = [
|
dependencies = [
|
||||||
"base64-simd",
|
"base64-simd",
|
||||||
"hmac 0.13.0",
|
"hmac 0.13.0",
|
||||||
"hotpath",
|
|
||||||
"rand 0.10.2",
|
"rand 0.10.2",
|
||||||
"serde",
|
"serde",
|
||||||
"serde_json",
|
"serde_json",
|
||||||
@@ -9131,7 +9112,6 @@ dependencies = [
|
|||||||
"argon2",
|
"argon2",
|
||||||
"base64-simd",
|
"base64-simd",
|
||||||
"chacha20poly1305",
|
"chacha20poly1305",
|
||||||
"hotpath",
|
|
||||||
"jsonwebtoken 11.0.0",
|
"jsonwebtoken 11.0.0",
|
||||||
"pbkdf2 0.13.0",
|
"pbkdf2 0.13.0",
|
||||||
"rand 0.10.2",
|
"rand 0.10.2",
|
||||||
@@ -9149,7 +9129,6 @@ name = "rustfs-data-usage"
|
|||||||
version = "1.0.0-beta.12"
|
version = "1.0.0-beta.12"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"async-trait",
|
"async-trait",
|
||||||
"hotpath",
|
|
||||||
"rmp-serde",
|
"rmp-serde",
|
||||||
"rustfs-filemeta",
|
"rustfs-filemeta",
|
||||||
"serde",
|
"serde",
|
||||||
@@ -9295,7 +9274,6 @@ dependencies = [
|
|||||||
name = "rustfs-extension-schema"
|
name = "rustfs-extension-schema"
|
||||||
version = "1.0.0-beta.12"
|
version = "1.0.0-beta.12"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"hotpath",
|
|
||||||
"serde",
|
"serde",
|
||||||
"serde_json",
|
"serde_json",
|
||||||
"thiserror 2.0.19",
|
"thiserror 2.0.19",
|
||||||
@@ -9334,7 +9312,6 @@ dependencies = [
|
|||||||
"async-trait",
|
"async-trait",
|
||||||
"base64 0.23.0",
|
"base64 0.23.0",
|
||||||
"futures",
|
"futures",
|
||||||
"hotpath",
|
|
||||||
"http 1.5.0",
|
"http 1.5.0",
|
||||||
"metrics",
|
"metrics",
|
||||||
"rustfs-common",
|
"rustfs-common",
|
||||||
@@ -9366,7 +9343,6 @@ dependencies = [
|
|||||||
"async-trait",
|
"async-trait",
|
||||||
"base64-simd",
|
"base64-simd",
|
||||||
"futures",
|
"futures",
|
||||||
"hotpath",
|
|
||||||
"http 1.5.0",
|
"http 1.5.0",
|
||||||
"jsonwebtoken 11.0.0",
|
"jsonwebtoken 11.0.0",
|
||||||
"moka",
|
"moka",
|
||||||
@@ -9401,7 +9377,6 @@ name = "rustfs-io-core"
|
|||||||
version = "1.0.0-beta.12"
|
version = "1.0.0-beta.12"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"bytes",
|
"bytes",
|
||||||
"hotpath",
|
|
||||||
"memmap2",
|
"memmap2",
|
||||||
"rustfs-io-metrics",
|
"rustfs-io-metrics",
|
||||||
"thiserror 2.0.19",
|
"thiserror 2.0.19",
|
||||||
@@ -9414,7 +9389,6 @@ name = "rustfs-io-metrics"
|
|||||||
version = "1.0.0-beta.12"
|
version = "1.0.0-beta.12"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"criterion",
|
"criterion",
|
||||||
"hotpath",
|
|
||||||
"metrics",
|
"metrics",
|
||||||
"metrics-util",
|
"metrics-util",
|
||||||
"num_cpus",
|
"num_cpus",
|
||||||
@@ -9481,7 +9455,6 @@ version = "1.0.0-beta.12"
|
|||||||
dependencies = [
|
dependencies = [
|
||||||
"bytes",
|
"bytes",
|
||||||
"futures",
|
"futures",
|
||||||
"hotpath",
|
|
||||||
"http 1.5.0",
|
"http 1.5.0",
|
||||||
"http-body 1.1.0",
|
"http-body 1.1.0",
|
||||||
"http-body-util",
|
"http-body-util",
|
||||||
@@ -9514,12 +9487,9 @@ dependencies = [
|
|||||||
"base64 0.23.0",
|
"base64 0.23.0",
|
||||||
"chacha20poly1305",
|
"chacha20poly1305",
|
||||||
"hex",
|
"hex",
|
||||||
"hotpath",
|
|
||||||
"insta",
|
"insta",
|
||||||
"jiff",
|
"jiff",
|
||||||
"md-5 0.11.0",
|
"md-5 0.11.0",
|
||||||
"metrics",
|
|
||||||
"metrics-util",
|
|
||||||
"moka",
|
"moka",
|
||||||
"rand 0.10.2",
|
"rand 0.10.2",
|
||||||
"reqwest",
|
"reqwest",
|
||||||
@@ -9547,7 +9517,6 @@ name = "rustfs-lifecycle"
|
|||||||
version = "1.0.0-beta.12"
|
version = "1.0.0-beta.12"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"async-trait",
|
"async-trait",
|
||||||
"hotpath",
|
|
||||||
"metrics",
|
"metrics",
|
||||||
"metrics-util",
|
"metrics-util",
|
||||||
"proptest",
|
"proptest",
|
||||||
@@ -9572,7 +9541,6 @@ dependencies = [
|
|||||||
"async-trait",
|
"async-trait",
|
||||||
"crossbeam-queue",
|
"crossbeam-queue",
|
||||||
"futures",
|
"futures",
|
||||||
"hotpath",
|
|
||||||
"parking_lot",
|
"parking_lot",
|
||||||
"rand 0.10.2",
|
"rand 0.10.2",
|
||||||
"rustfs-io-metrics",
|
"rustfs-io-metrics",
|
||||||
@@ -9594,7 +9562,6 @@ version = "1.0.0-beta.12"
|
|||||||
dependencies = [
|
dependencies = [
|
||||||
"chrono",
|
"chrono",
|
||||||
"flate2",
|
"flate2",
|
||||||
"hotpath",
|
|
||||||
"regex",
|
"regex",
|
||||||
"serde",
|
"serde",
|
||||||
"serde_json",
|
"serde_json",
|
||||||
@@ -9612,7 +9579,6 @@ name = "rustfs-madmin"
|
|||||||
version = "1.0.0-beta.12"
|
version = "1.0.0-beta.12"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"chrono",
|
"chrono",
|
||||||
"hotpath",
|
|
||||||
"humantime",
|
"humantime",
|
||||||
"hyper",
|
"hyper",
|
||||||
"rmp-serde",
|
"rmp-serde",
|
||||||
@@ -9633,7 +9599,6 @@ dependencies = [
|
|||||||
"criterion",
|
"criterion",
|
||||||
"form_urlencoded",
|
"form_urlencoded",
|
||||||
"hashbrown 0.17.1",
|
"hashbrown 0.17.1",
|
||||||
"hotpath",
|
|
||||||
"metrics",
|
"metrics",
|
||||||
"percent-encoding",
|
"percent-encoding",
|
||||||
"quick-xml",
|
"quick-xml",
|
||||||
@@ -9663,7 +9628,6 @@ version = "1.0.0-beta.12"
|
|||||||
dependencies = [
|
dependencies = [
|
||||||
"criterion",
|
"criterion",
|
||||||
"futures",
|
"futures",
|
||||||
"hotpath",
|
|
||||||
"rustfs-config",
|
"rustfs-config",
|
||||||
"rustfs-io-metrics",
|
"rustfs-io-metrics",
|
||||||
"rustfs-utils",
|
"rustfs-utils",
|
||||||
@@ -9682,7 +9646,6 @@ version = "1.0.0-beta.12"
|
|||||||
dependencies = [
|
dependencies = [
|
||||||
"bytes",
|
"bytes",
|
||||||
"criterion",
|
"criterion",
|
||||||
"hotpath",
|
|
||||||
"metrics",
|
"metrics",
|
||||||
"metrics-util",
|
"metrics-util",
|
||||||
"moka",
|
"moka",
|
||||||
@@ -9705,7 +9668,6 @@ dependencies = [
|
|||||||
"flate2",
|
"flate2",
|
||||||
"futures-util",
|
"futures-util",
|
||||||
"glob",
|
"glob",
|
||||||
"hotpath",
|
|
||||||
"jiff",
|
"jiff",
|
||||||
"libc",
|
"libc",
|
||||||
"metrics",
|
"metrics",
|
||||||
@@ -9754,7 +9716,6 @@ dependencies = [
|
|||||||
"base64-simd",
|
"base64-simd",
|
||||||
"chrono",
|
"chrono",
|
||||||
"futures",
|
"futures",
|
||||||
"hotpath",
|
|
||||||
"ipnetwork",
|
"ipnetwork",
|
||||||
"jsonwebtoken 11.0.0",
|
"jsonwebtoken 11.0.0",
|
||||||
"moka",
|
"moka",
|
||||||
@@ -9791,7 +9752,6 @@ dependencies = [
|
|||||||
"futures-util",
|
"futures-util",
|
||||||
"hex",
|
"hex",
|
||||||
"hmac 0.13.0",
|
"hmac 0.13.0",
|
||||||
"hotpath",
|
|
||||||
"http 1.5.0",
|
"http 1.5.0",
|
||||||
"http-body-util",
|
"http-body-util",
|
||||||
"hyper",
|
"hyper",
|
||||||
@@ -9844,7 +9804,6 @@ name = "rustfs-protos"
|
|||||||
version = "1.0.0-beta.12"
|
version = "1.0.0-beta.12"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"flatbuffers",
|
"flatbuffers",
|
||||||
"hotpath",
|
|
||||||
"prost 0.14.4",
|
"prost 0.14.4",
|
||||||
"rmp-serde",
|
"rmp-serde",
|
||||||
"rustfs-common",
|
"rustfs-common",
|
||||||
@@ -9869,7 +9828,6 @@ version = "1.0.0-beta.12"
|
|||||||
dependencies = [
|
dependencies = [
|
||||||
"byteorder",
|
"byteorder",
|
||||||
"bytes",
|
"bytes",
|
||||||
"hotpath",
|
|
||||||
"regex",
|
"regex",
|
||||||
"rmp",
|
"rmp",
|
||||||
"rmp-serde",
|
"rmp-serde",
|
||||||
@@ -9928,7 +9886,6 @@ dependencies = [
|
|||||||
"chacha20poly1305",
|
"chacha20poly1305",
|
||||||
"hex",
|
"hex",
|
||||||
"hmac 0.13.0",
|
"hmac 0.13.0",
|
||||||
"hotpath",
|
|
||||||
"minlz",
|
"minlz",
|
||||||
"pin-project-lite",
|
"pin-project-lite",
|
||||||
"rand 0.10.2",
|
"rand 0.10.2",
|
||||||
@@ -9946,7 +9903,6 @@ dependencies = [
|
|||||||
name = "rustfs-s3-ops"
|
name = "rustfs-s3-ops"
|
||||||
version = "1.0.0-beta.12"
|
version = "1.0.0-beta.12"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"hotpath",
|
|
||||||
"rustfs-s3-types",
|
"rustfs-s3-types",
|
||||||
]
|
]
|
||||||
|
|
||||||
@@ -9954,7 +9910,6 @@ dependencies = [
|
|||||||
name = "rustfs-s3-types"
|
name = "rustfs-s3-types"
|
||||||
version = "1.0.0-beta.12"
|
version = "1.0.0-beta.12"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"hotpath",
|
|
||||||
"serde",
|
"serde",
|
||||||
"serde_json",
|
"serde_json",
|
||||||
]
|
]
|
||||||
@@ -9969,7 +9924,6 @@ dependencies = [
|
|||||||
"datafusion",
|
"datafusion",
|
||||||
"futures",
|
"futures",
|
||||||
"futures-core",
|
"futures-core",
|
||||||
"hotpath",
|
|
||||||
"http 1.5.0",
|
"http 1.5.0",
|
||||||
"metrics",
|
"metrics",
|
||||||
"parking_lot",
|
"parking_lot",
|
||||||
@@ -9998,7 +9952,6 @@ dependencies = [
|
|||||||
"datafusion",
|
"datafusion",
|
||||||
"derive_builder",
|
"derive_builder",
|
||||||
"futures",
|
"futures",
|
||||||
"hotpath",
|
|
||||||
"parking_lot",
|
"parking_lot",
|
||||||
"rustfs-s3select-api",
|
"rustfs-s3select-api",
|
||||||
"s3s",
|
"s3s",
|
||||||
@@ -10016,7 +9969,6 @@ dependencies = [
|
|||||||
"futures",
|
"futures",
|
||||||
"hex-simd",
|
"hex-simd",
|
||||||
"hmac 0.13.0",
|
"hmac 0.13.0",
|
||||||
"hotpath",
|
|
||||||
"http 1.5.0",
|
"http 1.5.0",
|
||||||
"metrics",
|
"metrics",
|
||||||
"rand 0.10.2",
|
"rand 0.10.2",
|
||||||
@@ -10049,7 +10001,6 @@ dependencies = [
|
|||||||
name = "rustfs-security-governance"
|
name = "rustfs-security-governance"
|
||||||
version = "1.0.0-beta.12"
|
version = "1.0.0-beta.12"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"hotpath",
|
|
||||||
"thiserror 2.0.19",
|
"thiserror 2.0.19",
|
||||||
]
|
]
|
||||||
|
|
||||||
@@ -10059,7 +10010,6 @@ version = "1.0.0-beta.12"
|
|||||||
dependencies = [
|
dependencies = [
|
||||||
"base64-simd",
|
"base64-simd",
|
||||||
"bytes",
|
"bytes",
|
||||||
"hotpath",
|
|
||||||
"http 1.5.0",
|
"http 1.5.0",
|
||||||
"hyper",
|
"hyper",
|
||||||
"rustfs-utils",
|
"rustfs-utils",
|
||||||
@@ -10076,7 +10026,6 @@ name = "rustfs-storage-api"
|
|||||||
version = "1.0.0-beta.12"
|
version = "1.0.0-beta.12"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"async-trait",
|
"async-trait",
|
||||||
"hotpath",
|
|
||||||
"insta",
|
"insta",
|
||||||
"rustfs-filemeta",
|
"rustfs-filemeta",
|
||||||
"serde",
|
"serde",
|
||||||
@@ -10098,7 +10047,6 @@ dependencies = [
|
|||||||
"deadpool-postgres",
|
"deadpool-postgres",
|
||||||
"futures-util",
|
"futures-util",
|
||||||
"hashbrown 0.17.1",
|
"hashbrown 0.17.1",
|
||||||
"hotpath",
|
|
||||||
"hyper",
|
"hyper",
|
||||||
"hyper-rustls",
|
"hyper-rustls",
|
||||||
"lapin",
|
"lapin",
|
||||||
@@ -10144,7 +10092,6 @@ dependencies = [
|
|||||||
name = "rustfs-test-utils"
|
name = "rustfs-test-utils"
|
||||||
version = "1.0.0-beta.12"
|
version = "1.0.0-beta.12"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"hotpath",
|
|
||||||
"rustfs-data-usage",
|
"rustfs-data-usage",
|
||||||
"rustfs-ecstore",
|
"rustfs-ecstore",
|
||||||
"rustfs-storage-api",
|
"rustfs-storage-api",
|
||||||
@@ -10161,7 +10108,6 @@ name = "rustfs-tls-runtime"
|
|||||||
version = "1.0.0-beta.12"
|
version = "1.0.0-beta.12"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"arc-swap",
|
"arc-swap",
|
||||||
"hotpath",
|
|
||||||
"metrics",
|
"metrics",
|
||||||
"rcgen",
|
"rcgen",
|
||||||
"rustfs-common",
|
"rustfs-common",
|
||||||
@@ -10183,7 +10129,6 @@ version = "1.0.0-beta.12"
|
|||||||
dependencies = [
|
dependencies = [
|
||||||
"async-trait",
|
"async-trait",
|
||||||
"axum",
|
"axum",
|
||||||
"hotpath",
|
|
||||||
"http 1.5.0",
|
"http 1.5.0",
|
||||||
"ipnetwork",
|
"ipnetwork",
|
||||||
"metrics",
|
"metrics",
|
||||||
@@ -10230,7 +10175,6 @@ dependencies = [
|
|||||||
"hex-simd",
|
"hex-simd",
|
||||||
"highway",
|
"highway",
|
||||||
"hmac 0.13.0",
|
"hmac 0.13.0",
|
||||||
"hotpath",
|
|
||||||
"http 1.5.0",
|
"http 1.5.0",
|
||||||
"hyper",
|
"hyper",
|
||||||
"local-ip-address",
|
"local-ip-address",
|
||||||
@@ -10263,7 +10207,6 @@ dependencies = [
|
|||||||
"astral-tokio-tar",
|
"astral-tokio-tar",
|
||||||
"async-compression",
|
"async-compression",
|
||||||
"criterion",
|
"criterion",
|
||||||
"hotpath",
|
|
||||||
"tempfile",
|
"tempfile",
|
||||||
"thiserror 2.0.19",
|
"thiserror 2.0.19",
|
||||||
"tokio",
|
"tokio",
|
||||||
@@ -10607,7 +10550,7 @@ checksum = "d3e97a565f76233a6003f9f5c54be1d9c5bdfa3eccfb189469f11ec4901c47dc"
|
|||||||
dependencies = [
|
dependencies = [
|
||||||
"base16ct 0.2.0",
|
"base16ct 0.2.0",
|
||||||
"der 0.7.10",
|
"der 0.7.10",
|
||||||
"generic-array 0.14.7",
|
"generic-array 0.14.9",
|
||||||
"pkcs8 0.10.2",
|
"pkcs8 0.10.2",
|
||||||
"subtle",
|
"subtle",
|
||||||
"zeroize",
|
"zeroize",
|
||||||
@@ -11597,7 +11540,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
|
|||||||
checksum = "32497e9a4c7b38532efcdebeef879707aa9f794296a4f0244f6f69e9bc8574bd"
|
checksum = "32497e9a4c7b38532efcdebeef879707aa9f794296a4f0244f6f69e9bc8574bd"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"fastrand",
|
"fastrand",
|
||||||
"getrandom 0.4.3",
|
"getrandom 0.3.4",
|
||||||
"once_cell",
|
"once_cell",
|
||||||
"rustix",
|
"rustix",
|
||||||
"windows-sys 0.61.2",
|
"windows-sys 0.61.2",
|
||||||
|
|||||||
+2
-2
@@ -250,8 +250,8 @@ enumset = "1.1.14"
|
|||||||
faster-hex = "0.10.0"
|
faster-hex = "0.10.0"
|
||||||
flate2 = "1.1.9"
|
flate2 = "1.1.9"
|
||||||
glob = "0.3.4"
|
glob = "0.3.4"
|
||||||
google-cloud-storage = "1.17.0"
|
google-cloud-storage = "1.16.0"
|
||||||
google-cloud-auth = "1.15.0"
|
google-cloud-auth = "1.14.0"
|
||||||
hashbrown = { version = "0.17.1" }
|
hashbrown = { version = "0.17.1" }
|
||||||
hex = "0.4.3"
|
hex = "0.4.3"
|
||||||
hex-simd = "0.8.0"
|
hex-simd = "0.8.0"
|
||||||
|
|||||||
@@ -25,33 +25,7 @@ documentation = "https://docs.rs/rustfs-audit/latest/rustfs_audit/"
|
|||||||
keywords = ["audit", "target", "management", "fan-out", "RustFS"]
|
keywords = ["audit", "target", "management", "fan-out", "RustFS"]
|
||||||
categories = ["web-programming", "development-tools", "asynchronous", "api-bindings"]
|
categories = ["web-programming", "development-tools", "asynchronous", "api-bindings"]
|
||||||
|
|
||||||
[features]
|
|
||||||
default = []
|
|
||||||
hotpath = [
|
|
||||||
"hotpath/hotpath",
|
|
||||||
"hotpath/tokio",
|
|
||||||
"hotpath/futures",
|
|
||||||
"rustfs-config/hotpath",
|
|
||||||
"rustfs-s3-types/hotpath",
|
|
||||||
"rustfs-targets/hotpath",
|
|
||||||
]
|
|
||||||
hotpath-alloc = [
|
|
||||||
"hotpath",
|
|
||||||
"hotpath/hotpath-alloc",
|
|
||||||
"rustfs-config/hotpath-alloc",
|
|
||||||
"rustfs-s3-types/hotpath-alloc",
|
|
||||||
"rustfs-targets/hotpath-alloc",
|
|
||||||
]
|
|
||||||
hotpath-cpu = [
|
|
||||||
"hotpath",
|
|
||||||
"hotpath/hotpath-cpu",
|
|
||||||
"rustfs-config/hotpath-cpu",
|
|
||||||
"rustfs-s3-types/hotpath-cpu",
|
|
||||||
"rustfs-targets/hotpath-cpu",
|
|
||||||
]
|
|
||||||
|
|
||||||
[dependencies]
|
[dependencies]
|
||||||
hotpath.workspace = true
|
|
||||||
rustfs-targets = { workspace = true }
|
rustfs-targets = { workspace = true }
|
||||||
rustfs-config = { workspace = true, features = ["audit", "server-config-model"] }
|
rustfs-config = { workspace = true, features = ["audit", "server-config-model"] }
|
||||||
rustfs-s3-types = { workspace = true }
|
rustfs-s3-types = { workspace = true }
|
||||||
|
|||||||
@@ -28,14 +28,7 @@ documentation = "https://docs.rs/rustfs-checksums/latest/rustfs_checksum/"
|
|||||||
[lints]
|
[lints]
|
||||||
workspace = true
|
workspace = true
|
||||||
|
|
||||||
[features]
|
|
||||||
default = []
|
|
||||||
hotpath = ["hotpath/hotpath"]
|
|
||||||
hotpath-alloc = ["hotpath", "hotpath/hotpath-alloc"]
|
|
||||||
hotpath-cpu = ["hotpath", "hotpath/hotpath-cpu"]
|
|
||||||
|
|
||||||
[dependencies]
|
[dependencies]
|
||||||
hotpath.workspace = true
|
|
||||||
bytes = { workspace = true, features = ["serde"] }
|
bytes = { workspace = true, features = ["serde"] }
|
||||||
crc-fast = { workspace = true }
|
crc-fast = { workspace = true }
|
||||||
http = { workspace = true }
|
http = { workspace = true }
|
||||||
|
|||||||
@@ -27,14 +27,7 @@ categories = ["web-programming", "development-tools", "data-structures"]
|
|||||||
[lints]
|
[lints]
|
||||||
workspace = true
|
workspace = true
|
||||||
|
|
||||||
[features]
|
|
||||||
default = []
|
|
||||||
hotpath = ["hotpath/hotpath", "hotpath/tokio"]
|
|
||||||
hotpath-alloc = ["hotpath", "hotpath/hotpath-alloc"]
|
|
||||||
hotpath-cpu = ["hotpath", "hotpath/hotpath-cpu"]
|
|
||||||
|
|
||||||
[dependencies]
|
[dependencies]
|
||||||
hotpath.workspace = true
|
|
||||||
tokio = { workspace = true, features = ["fs", "rt-multi-thread"] }
|
tokio = { workspace = true, features = ["fs", "rt-multi-thread"] }
|
||||||
tonic = { workspace = true, features = ["gzip", "deflate"] }
|
tonic = { workspace = true, features = ["gzip", "deflate"] }
|
||||||
uuid = { workspace = true, features = ["v4", "fast-rng", "macro-diagnostics"] }
|
uuid = { workspace = true, features = ["v4", "fast-rng", "macro-diagnostics"] }
|
||||||
|
|||||||
@@ -13,14 +13,7 @@ categories = ["concurrency", "filesystem"]
|
|||||||
[lints]
|
[lints]
|
||||||
workspace = true
|
workspace = true
|
||||||
|
|
||||||
[features]
|
|
||||||
default = []
|
|
||||||
hotpath = ["hotpath/hotpath", "hotpath/tokio", "rustfs-io-core/hotpath"]
|
|
||||||
hotpath-alloc = ["hotpath", "hotpath/hotpath-alloc", "rustfs-io-core/hotpath-alloc"]
|
|
||||||
hotpath-cpu = ["hotpath", "hotpath/hotpath-cpu", "rustfs-io-core/hotpath-cpu"]
|
|
||||||
|
|
||||||
[dependencies]
|
[dependencies]
|
||||||
hotpath.workspace = true
|
|
||||||
# Internal crates
|
# Internal crates
|
||||||
rustfs-io-core = { workspace = true }
|
rustfs-io-core = { workspace = true }
|
||||||
serde = { workspace = true, features = ["derive"] }
|
serde = { workspace = true, features = ["derive"] }
|
||||||
|
|||||||
@@ -25,7 +25,6 @@ keywords = ["configuration", "settings", "management", "rustfs", "Minio"]
|
|||||||
categories = ["web-programming", "development-tools", "config"]
|
categories = ["web-programming", "development-tools", "config"]
|
||||||
|
|
||||||
[dependencies]
|
[dependencies]
|
||||||
hotpath.workspace = true
|
|
||||||
const-str = { workspace = true, optional = true, features = ["std", "proc"] }
|
const-str = { workspace = true, optional = true, features = ["std", "proc"] }
|
||||||
serde = { workspace = true, optional = true, features = ["derive"] }
|
serde = { workspace = true, optional = true, features = ["derive"] }
|
||||||
serde_json = { workspace = true, optional = true, features = ["raw_value"] }
|
serde_json = { workspace = true, optional = true, features = ["raw_value"] }
|
||||||
@@ -35,9 +34,6 @@ workspace = true
|
|||||||
|
|
||||||
[features]
|
[features]
|
||||||
default = ["constants"]
|
default = ["constants"]
|
||||||
hotpath = ["hotpath/hotpath"]
|
|
||||||
hotpath-alloc = ["hotpath", "hotpath/hotpath-alloc"]
|
|
||||||
hotpath-cpu = ["hotpath", "hotpath/hotpath-cpu"]
|
|
||||||
audit = ["dep:const-str", "constants"]
|
audit = ["dep:const-str", "constants"]
|
||||||
constants = ["dep:const-str"]
|
constants = ["dep:const-str"]
|
||||||
notify = ["dep:const-str", "constants"]
|
notify = ["dep:const-str", "constants"]
|
||||||
|
|||||||
@@ -24,14 +24,7 @@ description = "Credentials management utilities for RustFS, enabling secure hand
|
|||||||
keywords = ["rustfs", "Minio", "credentials", "authentication", "authorization"]
|
keywords = ["rustfs", "Minio", "credentials", "authentication", "authorization"]
|
||||||
categories = ["web-programming", "development-tools", "data-structures", "security"]
|
categories = ["web-programming", "development-tools", "data-structures", "security"]
|
||||||
|
|
||||||
[features]
|
|
||||||
default = []
|
|
||||||
hotpath = ["hotpath/hotpath"]
|
|
||||||
hotpath-alloc = ["hotpath", "hotpath/hotpath-alloc"]
|
|
||||||
hotpath-cpu = ["hotpath", "hotpath/hotpath-cpu"]
|
|
||||||
|
|
||||||
[dependencies]
|
[dependencies]
|
||||||
hotpath.workspace = true
|
|
||||||
base64-simd = { workspace = true }
|
base64-simd = { workspace = true }
|
||||||
hmac = { workspace = true }
|
hmac = { workspace = true }
|
||||||
rand = { workspace = true, features = ["serde"] }
|
rand = { workspace = true, features = ["serde"] }
|
||||||
|
|||||||
@@ -29,7 +29,6 @@ documentation = "https://docs.rs/rustfs-crypto/latest/rustfs_crypto/"
|
|||||||
workspace = true
|
workspace = true
|
||||||
|
|
||||||
[dependencies]
|
[dependencies]
|
||||||
hotpath.workspace = true
|
|
||||||
aes-gcm = { workspace = true, optional = true, features = ["rand_core"] }
|
aes-gcm = { workspace = true, optional = true, features = ["rand_core"] }
|
||||||
argon2 = { workspace = true, optional = true }
|
argon2 = { workspace = true, optional = true }
|
||||||
chacha20poly1305 = { workspace = true, optional = true }
|
chacha20poly1305 = { workspace = true, optional = true }
|
||||||
@@ -50,9 +49,6 @@ time = { workspace = true, features = ["parsing", "formatting", "macros", "serde
|
|||||||
|
|
||||||
[features]
|
[features]
|
||||||
default = ["crypto", "fips"]
|
default = ["crypto", "fips"]
|
||||||
hotpath = ["hotpath/hotpath"]
|
|
||||||
hotpath-alloc = ["hotpath", "hotpath/hotpath-alloc"]
|
|
||||||
hotpath-cpu = ["hotpath", "hotpath/hotpath-cpu"]
|
|
||||||
fips = []
|
fips = []
|
||||||
crypto = [
|
crypto = [
|
||||||
"dep:aes-gcm",
|
"dep:aes-gcm",
|
||||||
|
|||||||
@@ -27,14 +27,7 @@ categories = ["data-structures", "filesystem"]
|
|||||||
[lints]
|
[lints]
|
||||||
workspace = true
|
workspace = true
|
||||||
|
|
||||||
[features]
|
|
||||||
default = []
|
|
||||||
hotpath = ["hotpath/hotpath", "rustfs-filemeta/hotpath"]
|
|
||||||
hotpath-alloc = ["hotpath", "hotpath/hotpath-alloc", "rustfs-filemeta/hotpath-alloc"]
|
|
||||||
hotpath-cpu = ["hotpath", "hotpath/hotpath-cpu", "rustfs-filemeta/hotpath-cpu"]
|
|
||||||
|
|
||||||
[dependencies]
|
[dependencies]
|
||||||
hotpath.workspace = true
|
|
||||||
serde = { workspace = true, features = ["derive"] }
|
serde = { workspace = true, features = ["derive"] }
|
||||||
rmp-serde = { workspace = true }
|
rmp-serde = { workspace = true }
|
||||||
async-trait = { workspace = true }
|
async-trait = { workspace = true }
|
||||||
|
|||||||
@@ -25,58 +25,10 @@ workspace = true
|
|||||||
|
|
||||||
[features]
|
[features]
|
||||||
default = []
|
default = []
|
||||||
hotpath = [
|
|
||||||
"hotpath/hotpath",
|
|
||||||
"hotpath/tokio",
|
|
||||||
"hotpath/futures",
|
|
||||||
"hotpath/reqwest-0-13",
|
|
||||||
"rustfs-config/hotpath",
|
|
||||||
"rustfs-credentials/hotpath",
|
|
||||||
"rustfs-data-usage/hotpath",
|
|
||||||
"rustfs-ecstore/hotpath",
|
|
||||||
"rustfs-filemeta/hotpath",
|
|
||||||
"rustfs-lock/hotpath",
|
|
||||||
"rustfs-madmin/hotpath",
|
|
||||||
"rustfs-protos/hotpath",
|
|
||||||
"rustfs-rio/hotpath",
|
|
||||||
"rustfs-signer/hotpath",
|
|
||||||
"rustfs-utils/hotpath",
|
|
||||||
]
|
|
||||||
hotpath-alloc = [
|
|
||||||
"hotpath",
|
|
||||||
"hotpath/hotpath-alloc",
|
|
||||||
"rustfs-config/hotpath-alloc",
|
|
||||||
"rustfs-credentials/hotpath-alloc",
|
|
||||||
"rustfs-data-usage/hotpath-alloc",
|
|
||||||
"rustfs-ecstore/hotpath-alloc",
|
|
||||||
"rustfs-filemeta/hotpath-alloc",
|
|
||||||
"rustfs-lock/hotpath-alloc",
|
|
||||||
"rustfs-madmin/hotpath-alloc",
|
|
||||||
"rustfs-protos/hotpath-alloc",
|
|
||||||
"rustfs-rio/hotpath-alloc",
|
|
||||||
"rustfs-signer/hotpath-alloc",
|
|
||||||
"rustfs-utils/hotpath-alloc",
|
|
||||||
]
|
|
||||||
hotpath-cpu = [
|
|
||||||
"hotpath",
|
|
||||||
"hotpath/hotpath-cpu",
|
|
||||||
"rustfs-config/hotpath-cpu",
|
|
||||||
"rustfs-credentials/hotpath-cpu",
|
|
||||||
"rustfs-data-usage/hotpath-cpu",
|
|
||||||
"rustfs-ecstore/hotpath-cpu",
|
|
||||||
"rustfs-filemeta/hotpath-cpu",
|
|
||||||
"rustfs-lock/hotpath-cpu",
|
|
||||||
"rustfs-madmin/hotpath-cpu",
|
|
||||||
"rustfs-protos/hotpath-cpu",
|
|
||||||
"rustfs-rio/hotpath-cpu",
|
|
||||||
"rustfs-signer/hotpath-cpu",
|
|
||||||
"rustfs-utils/hotpath-cpu",
|
|
||||||
]
|
|
||||||
ftps = []
|
ftps = []
|
||||||
sftp = []
|
sftp = []
|
||||||
|
|
||||||
[dependencies]
|
[dependencies]
|
||||||
hotpath.workspace = true
|
|
||||||
rustfs-config = { workspace = true, features = ["constants"] }
|
rustfs-config = { workspace = true, features = ["constants"] }
|
||||||
rustfs-credentials.workspace = true
|
rustfs-credentials.workspace = true
|
||||||
rustfs-ecstore.workspace = true
|
rustfs-ecstore.workspace = true
|
||||||
|
|||||||
@@ -26,6 +26,7 @@
|
|||||||
|
|
||||||
use super::common::*;
|
use super::common::*;
|
||||||
use aws_sdk_s3::Client;
|
use aws_sdk_s3::Client;
|
||||||
|
use aws_sdk_s3::error::ProvideErrorMetadata;
|
||||||
use aws_sdk_s3::primitives::{ByteStream, DateTimeFormat};
|
use aws_sdk_s3::primitives::{ByteStream, DateTimeFormat};
|
||||||
use aws_sdk_s3::types::{
|
use aws_sdk_s3::types::{
|
||||||
CompletedMultipartUpload, CompletedPart, Delete, MetadataDirective, ObjectIdentifier, ObjectLockLegalHoldStatus,
|
CompletedMultipartUpload, CompletedPart, Delete, MetadataDirective, ObjectIdentifier, ObjectLockLegalHoldStatus,
|
||||||
@@ -2120,6 +2121,127 @@ async fn test_multipart_default_retention_fixed_at_create() {
|
|||||||
// Versioning Auto-Enable Tests
|
// Versioning Auto-Enable Tests
|
||||||
// ============================================================================
|
// ============================================================================
|
||||||
|
|
||||||
|
#[tokio::test]
|
||||||
|
#[serial]
|
||||||
|
async fn test_unretained_object_lock_object_delete_and_bucket_cleanup() {
|
||||||
|
init_logging();
|
||||||
|
info!("🧪 Test: Unretained Object Lock object delete and bucket cleanup (Issue #5339)");
|
||||||
|
|
||||||
|
let mut env = ObjectLockTestEnvironment::new()
|
||||||
|
.await
|
||||||
|
.expect("failed to create Object Lock test environment");
|
||||||
|
env.start_rustfs().await.expect("failed to start RustFS");
|
||||||
|
|
||||||
|
let bucket = "test-object-lock-delete-cleanup";
|
||||||
|
let key = "unretained-object";
|
||||||
|
|
||||||
|
env.create_object_lock_bucket(bucket)
|
||||||
|
.await
|
||||||
|
.expect("failed to create Object Lock bucket");
|
||||||
|
let client = env.s3_client();
|
||||||
|
|
||||||
|
let put_response = client
|
||||||
|
.put_object()
|
||||||
|
.bucket(bucket)
|
||||||
|
.key(key)
|
||||||
|
.body(ByteStream::from_static(b"unretained data"))
|
||||||
|
.send()
|
||||||
|
.await
|
||||||
|
.expect("failed to upload unretained object");
|
||||||
|
let object_version_id = put_response
|
||||||
|
.version_id()
|
||||||
|
.expect("Object Lock buckets must create versioned objects")
|
||||||
|
.to_string();
|
||||||
|
|
||||||
|
let delete_response = client
|
||||||
|
.delete_object()
|
||||||
|
.bucket(bucket)
|
||||||
|
.key(key)
|
||||||
|
.send()
|
||||||
|
.await
|
||||||
|
.expect("failed to create delete marker");
|
||||||
|
assert_eq!(delete_response.delete_marker(), Some(true));
|
||||||
|
let delete_marker_version_id = delete_response
|
||||||
|
.version_id()
|
||||||
|
.expect("Deleting without a version ID must create a delete marker")
|
||||||
|
.to_string();
|
||||||
|
|
||||||
|
let get_error = client
|
||||||
|
.get_object()
|
||||||
|
.bucket(bucket)
|
||||||
|
.key(key)
|
||||||
|
.send()
|
||||||
|
.await
|
||||||
|
.expect_err("GET must not return an object hidden by a delete marker");
|
||||||
|
assert_eq!(get_error.raw_response().map(|response| response.status().as_u16()), Some(404));
|
||||||
|
assert_eq!(get_error.as_service_error().and_then(|error| error.code()), Some("NoSuchKey"));
|
||||||
|
|
||||||
|
let listed_objects = client
|
||||||
|
.list_objects_v2()
|
||||||
|
.bucket(bucket)
|
||||||
|
.send()
|
||||||
|
.await
|
||||||
|
.expect("failed to list current objects");
|
||||||
|
assert!(
|
||||||
|
listed_objects.contents().iter().all(|object| object.key() != Some(key)),
|
||||||
|
"ListObjectsV2 must hide objects whose latest version is a delete marker"
|
||||||
|
);
|
||||||
|
|
||||||
|
let listed_versions = client
|
||||||
|
.list_object_versions()
|
||||||
|
.bucket(bucket)
|
||||||
|
.send()
|
||||||
|
.await
|
||||||
|
.expect("failed to list object versions");
|
||||||
|
assert!(
|
||||||
|
listed_versions
|
||||||
|
.versions()
|
||||||
|
.iter()
|
||||||
|
.any(|version| version.key() == Some(key) && version.version_id() == Some(object_version_id.as_str())),
|
||||||
|
"The data version must remain until it is explicitly deleted"
|
||||||
|
);
|
||||||
|
assert!(
|
||||||
|
listed_versions
|
||||||
|
.delete_markers()
|
||||||
|
.iter()
|
||||||
|
.any(|marker| marker.key() == Some(key) && marker.version_id() == Some(delete_marker_version_id.as_str())),
|
||||||
|
"ListObjectVersions must expose the delete marker"
|
||||||
|
);
|
||||||
|
|
||||||
|
client
|
||||||
|
.delete_object()
|
||||||
|
.bucket(bucket)
|
||||||
|
.key(key)
|
||||||
|
.version_id(object_version_id)
|
||||||
|
.send()
|
||||||
|
.await
|
||||||
|
.expect("failed to delete the data version");
|
||||||
|
client
|
||||||
|
.delete_object()
|
||||||
|
.bucket(bucket)
|
||||||
|
.key(key)
|
||||||
|
.version_id(delete_marker_version_id)
|
||||||
|
.send()
|
||||||
|
.await
|
||||||
|
.expect("failed to delete the delete marker");
|
||||||
|
|
||||||
|
let remaining_versions = client
|
||||||
|
.list_object_versions()
|
||||||
|
.bucket(bucket)
|
||||||
|
.send()
|
||||||
|
.await
|
||||||
|
.expect("failed to list versions after cleanup");
|
||||||
|
assert!(remaining_versions.versions().is_empty());
|
||||||
|
assert!(remaining_versions.delete_markers().is_empty());
|
||||||
|
|
||||||
|
client
|
||||||
|
.delete_bucket()
|
||||||
|
.bucket(bucket)
|
||||||
|
.send()
|
||||||
|
.await
|
||||||
|
.expect("Deleting every version must remove xl.meta so the bucket can be deleted normally");
|
||||||
|
}
|
||||||
|
|
||||||
#[tokio::test]
|
#[tokio::test]
|
||||||
#[serial]
|
#[serial]
|
||||||
async fn test_versioning_auto_enabled_with_object_lock() {
|
async fn test_versioning_auto_enabled_with_object_lock() {
|
||||||
|
|||||||
@@ -40,83 +40,13 @@ hotpath = [
|
|||||||
"hotpath/async-channel",
|
"hotpath/async-channel",
|
||||||
"hotpath/parking_lot",
|
"hotpath/parking_lot",
|
||||||
"hotpath/reqwest-0-13",
|
"hotpath/reqwest-0-13",
|
||||||
"rustfs-checksums/hotpath",
|
|
||||||
"rustfs-common/hotpath",
|
|
||||||
"rustfs-concurrency/hotpath",
|
|
||||||
"rustfs-config/hotpath",
|
|
||||||
"rustfs-credentials/hotpath",
|
|
||||||
"rustfs-data-usage/hotpath",
|
|
||||||
"rustfs-filemeta/hotpath",
|
"rustfs-filemeta/hotpath",
|
||||||
"rustfs-io-metrics/hotpath",
|
|
||||||
"rustfs-lifecycle/hotpath",
|
|
||||||
"rustfs-lock/hotpath",
|
|
||||||
"rustfs-madmin/hotpath",
|
|
||||||
"rustfs-object-capacity/hotpath",
|
|
||||||
"rustfs-policy/hotpath",
|
|
||||||
"rustfs-protos/hotpath",
|
|
||||||
"rustfs-replication/hotpath",
|
|
||||||
"rustfs-rio/hotpath",
|
"rustfs-rio/hotpath",
|
||||||
"rustfs-rio-v2?/hotpath",
|
|
||||||
"rustfs-s3-types/hotpath",
|
|
||||||
"rustfs-signer/hotpath",
|
|
||||||
"rustfs-storage-api/hotpath",
|
|
||||||
"rustfs-tls-runtime/hotpath",
|
|
||||||
"rustfs-utils/hotpath",
|
|
||||||
"rustfs-crypto/hotpath",
|
|
||||||
]
|
]
|
||||||
hotpath-alloc = [
|
hotpath-alloc = [
|
||||||
"hotpath",
|
|
||||||
"hotpath/hotpath-alloc",
|
"hotpath/hotpath-alloc",
|
||||||
"rustfs-checksums/hotpath-alloc",
|
|
||||||
"rustfs-common/hotpath-alloc",
|
|
||||||
"rustfs-concurrency/hotpath-alloc",
|
|
||||||
"rustfs-config/hotpath-alloc",
|
|
||||||
"rustfs-credentials/hotpath-alloc",
|
|
||||||
"rustfs-data-usage/hotpath-alloc",
|
|
||||||
"rustfs-filemeta/hotpath-alloc",
|
"rustfs-filemeta/hotpath-alloc",
|
||||||
"rustfs-io-metrics/hotpath-alloc",
|
|
||||||
"rustfs-lifecycle/hotpath-alloc",
|
|
||||||
"rustfs-lock/hotpath-alloc",
|
|
||||||
"rustfs-madmin/hotpath-alloc",
|
|
||||||
"rustfs-object-capacity/hotpath-alloc",
|
|
||||||
"rustfs-policy/hotpath-alloc",
|
|
||||||
"rustfs-protos/hotpath-alloc",
|
|
||||||
"rustfs-replication/hotpath-alloc",
|
|
||||||
"rustfs-rio/hotpath-alloc",
|
"rustfs-rio/hotpath-alloc",
|
||||||
"rustfs-rio-v2?/hotpath-alloc",
|
|
||||||
"rustfs-s3-types/hotpath-alloc",
|
|
||||||
"rustfs-signer/hotpath-alloc",
|
|
||||||
"rustfs-storage-api/hotpath-alloc",
|
|
||||||
"rustfs-tls-runtime/hotpath-alloc",
|
|
||||||
"rustfs-utils/hotpath-alloc",
|
|
||||||
"rustfs-crypto/hotpath-alloc",
|
|
||||||
]
|
|
||||||
hotpath-cpu = [
|
|
||||||
"hotpath",
|
|
||||||
"hotpath/hotpath-cpu",
|
|
||||||
"rustfs-checksums/hotpath-cpu",
|
|
||||||
"rustfs-common/hotpath-cpu",
|
|
||||||
"rustfs-concurrency/hotpath-cpu",
|
|
||||||
"rustfs-config/hotpath-cpu",
|
|
||||||
"rustfs-credentials/hotpath-cpu",
|
|
||||||
"rustfs-data-usage/hotpath-cpu",
|
|
||||||
"rustfs-filemeta/hotpath-cpu",
|
|
||||||
"rustfs-io-metrics/hotpath-cpu",
|
|
||||||
"rustfs-lifecycle/hotpath-cpu",
|
|
||||||
"rustfs-lock/hotpath-cpu",
|
|
||||||
"rustfs-madmin/hotpath-cpu",
|
|
||||||
"rustfs-object-capacity/hotpath-cpu",
|
|
||||||
"rustfs-policy/hotpath-cpu",
|
|
||||||
"rustfs-protos/hotpath-cpu",
|
|
||||||
"rustfs-replication/hotpath-cpu",
|
|
||||||
"rustfs-rio/hotpath-cpu",
|
|
||||||
"rustfs-rio-v2?/hotpath-cpu",
|
|
||||||
"rustfs-s3-types/hotpath-cpu",
|
|
||||||
"rustfs-signer/hotpath-cpu",
|
|
||||||
"rustfs-storage-api/hotpath-cpu",
|
|
||||||
"rustfs-tls-runtime/hotpath-cpu",
|
|
||||||
"rustfs-utils/hotpath-cpu",
|
|
||||||
"rustfs-crypto/hotpath-cpu",
|
|
||||||
]
|
]
|
||||||
# Exposes shared lifecycle/tier test utilities (MockWarmBackend, fault
|
# Exposes shared lifecycle/tier test utilities (MockWarmBackend, fault
|
||||||
# injection, xl.meta transition assertions) via `api::tier::test_util`.
|
# injection, xl.meta transition assertions) via `api::tier::test_util`.
|
||||||
|
|||||||
@@ -579,26 +579,8 @@ pub struct BucketMetadataSys {
|
|||||||
/// Serializes metadata-map commits and their derived cache updates for one
|
/// Serializes metadata-map commits and their derived cache updates for one
|
||||||
/// bucket. Namespace locks, when present, are acquired before this lock.
|
/// bucket. Namespace locks, when present, are acquired before this lock.
|
||||||
metadata_publish_locks: Arc<MetadataPublishLockRegistry>,
|
metadata_publish_locks: Arc<MetadataPublishLockRegistry>,
|
||||||
/// Deduplicates concurrent lazy loads of one bucket's metadata, so N
|
|
||||||
/// simultaneous cache misses issue a single disk read instead of N.
|
|
||||||
///
|
|
||||||
/// This is the `singleflight` that upstream applies to its own lazy
|
|
||||||
/// `GetConfig`. Without it the namespace *read* lock the load holds is no
|
|
||||||
/// help: read locks are shared, so it excludes concurrent config writers
|
|
||||||
/// but not concurrent readers, and every caller still pays a full
|
|
||||||
/// erasure-set metadata fanout. A separate registry from
|
|
||||||
/// `metadata_publish_locks`, reusing the same per-bucket lock machinery.
|
|
||||||
///
|
|
||||||
/// Lock order: this lock, then the namespace lock, then the publish lock,
|
|
||||||
/// then the metadata map. It is only ever taken as the first of those, so
|
|
||||||
/// it cannot invert against a path that already holds one of the others.
|
|
||||||
lazy_load_locks: Arc<MetadataPublishLockRegistry>,
|
|
||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
lazy_load_lock_probe: std::sync::atomic::AtomicBool,
|
lazy_load_lock_probe: std::sync::atomic::AtomicBool,
|
||||||
/// Counts disk loads taken by the lazy `get_config` path, so a test can
|
|
||||||
/// prove concurrent misses collapse into one.
|
|
||||||
#[cfg(test)]
|
|
||||||
lazy_disk_loads: std::sync::atomic::AtomicUsize,
|
|
||||||
/// Buckets recently observed to have no persisted metadata. Serving the
|
/// Buckets recently observed to have no persisted metadata. Serving the
|
||||||
/// fabricated default from here (instead of re-reading disk) keeps the
|
/// fabricated default from here (instead of re-reading disk) keeps the
|
||||||
/// per-request cost of repeated lookups for such names bounded — without
|
/// per-request cost of repeated lookups for such names bounded — without
|
||||||
@@ -617,13 +599,8 @@ impl BucketMetadataSys {
|
|||||||
metadata_publish_locks: Arc::new(MetadataPublishLockRegistry {
|
metadata_publish_locks: Arc::new(MetadataPublishLockRegistry {
|
||||||
locks: StdMutex::new(HashMap::new()),
|
locks: StdMutex::new(HashMap::new()),
|
||||||
}),
|
}),
|
||||||
lazy_load_locks: Arc::new(MetadataPublishLockRegistry {
|
|
||||||
locks: StdMutex::new(HashMap::new()),
|
|
||||||
}),
|
|
||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
lazy_load_lock_probe: std::sync::atomic::AtomicBool::new(false),
|
lazy_load_lock_probe: std::sync::atomic::AtomicBool::new(false),
|
||||||
#[cfg(test)]
|
|
||||||
lazy_disk_loads: std::sync::atomic::AtomicUsize::new(0),
|
|
||||||
absent_metadata: moka::future::Cache::builder()
|
absent_metadata: moka::future::Cache::builder()
|
||||||
.max_capacity(ABSENT_BUCKET_METADATA_MAX_ENTRIES)
|
.max_capacity(ABSENT_BUCKET_METADATA_MAX_ENTRIES)
|
||||||
.time_to_live(ABSENT_BUCKET_METADATA_TTL)
|
.time_to_live(ABSENT_BUCKET_METADATA_TTL)
|
||||||
@@ -638,22 +615,16 @@ impl BucketMetadataSys {
|
|||||||
}
|
}
|
||||||
|
|
||||||
fn metadata_publish_lock(&self, bucket: &str) -> Arc<Mutex<MetadataPublishLockState>> {
|
fn metadata_publish_lock(&self, bucket: &str) -> Arc<Mutex<MetadataPublishLockState>> {
|
||||||
Self::bucket_lock_in(&self.metadata_publish_locks, bucket)
|
let mut locks = self
|
||||||
}
|
.metadata_publish_locks
|
||||||
|
.locks
|
||||||
/// Per-bucket gate for the lazy `get_config` disk load. See
|
.lock()
|
||||||
/// [`Self::lazy_load_locks`].
|
.unwrap_or_else(|poisoned| poisoned.into_inner());
|
||||||
fn lazy_load_lock(&self, bucket: &str) -> Arc<Mutex<MetadataPublishLockState>> {
|
|
||||||
Self::bucket_lock_in(&self.lazy_load_locks, bucket)
|
|
||||||
}
|
|
||||||
|
|
||||||
fn bucket_lock_in(registry: &Arc<MetadataPublishLockRegistry>, bucket: &str) -> Arc<Mutex<MetadataPublishLockState>> {
|
|
||||||
let mut locks = registry.locks.lock().unwrap_or_else(|poisoned| poisoned.into_inner());
|
|
||||||
locks.get(bucket).and_then(Weak::upgrade).unwrap_or_else(|| {
|
locks.get(bucket).and_then(Weak::upgrade).unwrap_or_else(|| {
|
||||||
let lock = Arc::new_cyclic(|lock| {
|
let lock = Arc::new_cyclic(|lock| {
|
||||||
Mutex::new(MetadataPublishLockState {
|
Mutex::new(MetadataPublishLockState {
|
||||||
bucket: bucket.to_string(),
|
bucket: bucket.to_string(),
|
||||||
registry: Arc::downgrade(registry),
|
registry: Arc::downgrade(&self.metadata_publish_locks),
|
||||||
lock: lock.clone(),
|
lock: lock.clone(),
|
||||||
})
|
})
|
||||||
});
|
});
|
||||||
@@ -1108,27 +1079,6 @@ impl BucketMetadataSys {
|
|||||||
return Ok((Arc::new(bm), true));
|
return Ok((Arc::new(bm), true));
|
||||||
}
|
}
|
||||||
|
|
||||||
// Collapse concurrent misses for this bucket into one disk load.
|
|
||||||
// Taken before the namespace lock — see `lazy_load_locks` for the
|
|
||||||
// ordering rule.
|
|
||||||
let load_lock = self.lazy_load_lock(bucket);
|
|
||||||
let _load_guard = load_lock.lock_owned().await;
|
|
||||||
|
|
||||||
// Re-check both caches: whoever held the gate before us may have
|
|
||||||
// already answered this exact question, and repeating the fanout
|
|
||||||
// is the whole cost this gate exists to avoid.
|
|
||||||
if let Some(bm) = self.metadata_map.read().await.get(bucket).cloned() {
|
|
||||||
return Ok((bm, true));
|
|
||||||
}
|
|
||||||
if self.absent_metadata.get(bucket).await.is_some() {
|
|
||||||
let mut bm = BucketMetadata::new(bucket);
|
|
||||||
bm.default_timestamps();
|
|
||||||
return Ok((Arc::new(bm), true));
|
|
||||||
}
|
|
||||||
|
|
||||||
#[cfg(test)]
|
|
||||||
self.lazy_disk_loads.fetch_add(1, std::sync::atomic::Ordering::Relaxed);
|
|
||||||
|
|
||||||
let lock = self.api.new_ns_lock(bucket, bucket).await?;
|
let lock = self.api.new_ns_lock(bucket, bucket).await?;
|
||||||
let guard = lock.get_read_lock(crate::set_disk::get_lock_acquire_timeout()).await?;
|
let guard = lock.get_read_lock(crate::set_disk::get_lock_acquire_timeout()).await?;
|
||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
@@ -1481,43 +1431,6 @@ mod tests {
|
|||||||
use serial_test::serial;
|
use serial_test::serial;
|
||||||
use tokio::time::timeout;
|
use tokio::time::timeout;
|
||||||
|
|
||||||
/// Concurrent cache misses for one bucket must collapse into a single disk
|
|
||||||
/// load.
|
|
||||||
///
|
|
||||||
/// The namespace read lock the lazy path already holds does not provide
|
|
||||||
/// this: read locks are shared, so it excludes concurrent config writers
|
|
||||||
/// but not concurrent readers. Without the dedup gate every caller pays its
|
|
||||||
/// own namespace-lock acquisition plus a full erasure-set metadata fanout —
|
|
||||||
/// and the paths that reach `get_config` are per-request, so the multiplier
|
|
||||||
/// is request concurrency.
|
|
||||||
#[tokio::test]
|
|
||||||
async fn concurrent_lazy_loads_of_one_bucket_issue_a_single_disk_read() {
|
|
||||||
use std::sync::atomic::Ordering;
|
|
||||||
|
|
||||||
let (_dirs, ecstore) = isolated_store_over_temp_disks().await;
|
|
||||||
let sys = Arc::new(BucketMetadataSys::new(ecstore));
|
|
||||||
|
|
||||||
// A name with no persisted metadata: every caller misses the map, and
|
|
||||||
// the absent-cache entry does not exist until the first load records it.
|
|
||||||
let bucket = "singleflight-bucket";
|
|
||||||
let waiters = 8;
|
|
||||||
|
|
||||||
let results = futures::future::join_all((0..waiters).map(|_| {
|
|
||||||
let sys = Arc::clone(&sys);
|
|
||||||
async move { sys.get_config(bucket).await.map(|(bm, _)| bm.name.clone()) }
|
|
||||||
}))
|
|
||||||
.await;
|
|
||||||
|
|
||||||
for result in results {
|
|
||||||
assert_eq!(result.expect("every caller must get an answer"), bucket);
|
|
||||||
}
|
|
||||||
assert_eq!(
|
|
||||||
sys.lazy_disk_loads.load(Ordering::Relaxed),
|
|
||||||
1,
|
|
||||||
"concurrent misses for one bucket must share a single disk load"
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Pins the fail-closed caching contract of the lazy `get_config` path
|
/// Pins the fail-closed caching contract of the lazy `get_config` path
|
||||||
/// and the refresh no-replace rule: fabricated defaults are returned but
|
/// and the refresh no-replace rule: fabricated defaults are returned but
|
||||||
/// never served by the map-only `get()`, persisted metadata is cached on
|
/// never served by the map-only `get()`, persisted metadata is cached on
|
||||||
|
|||||||
@@ -12,8 +12,6 @@
|
|||||||
// See the License for the specific language governing permissions and
|
// See the License for the specific language governing permissions and
|
||||||
// limitations under the License.
|
// limitations under the License.
|
||||||
|
|
||||||
#![recursion_limit = "256"]
|
|
||||||
|
|
||||||
/// Scope-based hotpath measurement for `#[async_trait]` methods, where
|
/// Scope-based hotpath measurement for `#[async_trait]` methods, where
|
||||||
/// `#[hotpath::measure]` would only time the boxed-future construction.
|
/// `#[hotpath::measure]` would only time the boxed-future construction.
|
||||||
/// The guard records wall time from this statement until the enclosing
|
/// The guard records wall time from this statement until the enclosing
|
||||||
|
|||||||
@@ -27,14 +27,7 @@ categories = ["web-programming", "development-tools"]
|
|||||||
[lib]
|
[lib]
|
||||||
doctest = false
|
doctest = false
|
||||||
|
|
||||||
[features]
|
|
||||||
default = []
|
|
||||||
hotpath = ["hotpath/hotpath"]
|
|
||||||
hotpath-alloc = ["hotpath", "hotpath/hotpath-alloc"]
|
|
||||||
hotpath-cpu = ["hotpath", "hotpath/hotpath-cpu"]
|
|
||||||
|
|
||||||
[dependencies]
|
[dependencies]
|
||||||
hotpath.workspace = true
|
|
||||||
serde = { workspace = true, features = ["derive"] }
|
serde = { workspace = true, features = ["derive"] }
|
||||||
thiserror.workspace = true
|
thiserror.workspace = true
|
||||||
|
|
||||||
|
|||||||
@@ -27,9 +27,8 @@ documentation = "https://docs.rs/rustfs-filemeta/latest/rustfs_filemeta/"
|
|||||||
|
|
||||||
[features]
|
[features]
|
||||||
default = []
|
default = []
|
||||||
hotpath = ["hotpath/hotpath", "hotpath/tokio", "rustfs-utils/hotpath"]
|
hotpath = ["hotpath/hotpath", "hotpath/tokio"]
|
||||||
hotpath-alloc = ["hotpath", "hotpath/hotpath-alloc", "rustfs-utils/hotpath-alloc"]
|
hotpath-alloc = ["hotpath/hotpath-alloc"]
|
||||||
hotpath-cpu = ["hotpath", "hotpath/hotpath-cpu", "rustfs-utils/hotpath-cpu"]
|
|
||||||
|
|
||||||
[dependencies]
|
[dependencies]
|
||||||
hotpath.workspace = true
|
hotpath.workspace = true
|
||||||
|
|||||||
@@ -29,48 +29,7 @@ categories = ["web-programming", "development-tools", "filesystem"]
|
|||||||
[lints]
|
[lints]
|
||||||
workspace = true
|
workspace = true
|
||||||
|
|
||||||
[features]
|
|
||||||
default = []
|
|
||||||
hotpath = [
|
|
||||||
"hotpath/hotpath",
|
|
||||||
"hotpath/tokio",
|
|
||||||
"hotpath/futures",
|
|
||||||
"rustfs-common/hotpath",
|
|
||||||
"rustfs-concurrency/hotpath",
|
|
||||||
"rustfs-config/hotpath",
|
|
||||||
"rustfs-ecstore/hotpath",
|
|
||||||
"rustfs-madmin/hotpath",
|
|
||||||
"rustfs-storage-api/hotpath",
|
|
||||||
"rustfs-utils/hotpath",
|
|
||||||
"rustfs-test-utils/hotpath",
|
|
||||||
]
|
|
||||||
hotpath-alloc = [
|
|
||||||
"hotpath",
|
|
||||||
"hotpath/hotpath-alloc",
|
|
||||||
"rustfs-common/hotpath-alloc",
|
|
||||||
"rustfs-concurrency/hotpath-alloc",
|
|
||||||
"rustfs-config/hotpath-alloc",
|
|
||||||
"rustfs-ecstore/hotpath-alloc",
|
|
||||||
"rustfs-madmin/hotpath-alloc",
|
|
||||||
"rustfs-storage-api/hotpath-alloc",
|
|
||||||
"rustfs-utils/hotpath-alloc",
|
|
||||||
"rustfs-test-utils/hotpath-alloc",
|
|
||||||
]
|
|
||||||
hotpath-cpu = [
|
|
||||||
"hotpath",
|
|
||||||
"hotpath/hotpath-cpu",
|
|
||||||
"rustfs-common/hotpath-cpu",
|
|
||||||
"rustfs-concurrency/hotpath-cpu",
|
|
||||||
"rustfs-config/hotpath-cpu",
|
|
||||||
"rustfs-ecstore/hotpath-cpu",
|
|
||||||
"rustfs-madmin/hotpath-cpu",
|
|
||||||
"rustfs-storage-api/hotpath-cpu",
|
|
||||||
"rustfs-utils/hotpath-cpu",
|
|
||||||
"rustfs-test-utils/hotpath-cpu",
|
|
||||||
]
|
|
||||||
|
|
||||||
[dependencies]
|
[dependencies]
|
||||||
hotpath.workspace = true
|
|
||||||
rustfs-config = { workspace = true }
|
rustfs-config = { workspace = true }
|
||||||
rustfs-concurrency = { workspace = true }
|
rustfs-concurrency = { workspace = true }
|
||||||
rustfs-ecstore = { workspace = true }
|
rustfs-ecstore = { workspace = true }
|
||||||
|
|||||||
@@ -159,7 +159,6 @@ impl ErasureSetHealer {
|
|||||||
|
|
||||||
/// execute erasure set heal with resume
|
/// execute erasure set heal with resume
|
||||||
#[tracing::instrument(skip(self, buckets), fields(set_disk_id = %set_disk_id, bucket_count = buckets.len()))]
|
#[tracing::instrument(skip(self, buckets), fields(set_disk_id = %set_disk_id, bucket_count = buckets.len()))]
|
||||||
#[hotpath::measure]
|
|
||||||
pub async fn heal_erasure_set(&self, buckets: &[String], set_disk_id: &str) -> Result<()> {
|
pub async fn heal_erasure_set(&self, buckets: &[String], set_disk_id: &str) -> Result<()> {
|
||||||
debug!(
|
debug!(
|
||||||
target: "rustfs::heal::erasure_healer",
|
target: "rustfs::heal::erasure_healer",
|
||||||
|
|||||||
@@ -584,7 +584,6 @@ impl HealTask {
|
|||||||
}
|
}
|
||||||
|
|
||||||
#[tracing::instrument(skip(self), fields(task_id = %self.id, heal_type = ?self.heal_type))]
|
#[tracing::instrument(skip(self), fields(task_id = %self.id, heal_type = ?self.heal_type))]
|
||||||
#[hotpath::measure]
|
|
||||||
pub async fn execute(&self) -> Result<()> {
|
pub async fn execute(&self) -> Result<()> {
|
||||||
// update status and timestamps atomically to avoid race conditions
|
// update status and timestamps atomically to avoid race conditions
|
||||||
let now = SystemTime::now();
|
let now = SystemTime::now();
|
||||||
@@ -760,7 +759,6 @@ impl HealTask {
|
|||||||
|
|
||||||
// specific heal implementation method
|
// specific heal implementation method
|
||||||
#[tracing::instrument(skip(self), fields(bucket = %bucket, object = %object, version_id = ?version_id))]
|
#[tracing::instrument(skip(self), fields(bucket = %bucket, object = %object, version_id = ?version_id))]
|
||||||
#[hotpath::measure]
|
|
||||||
async fn heal_object(&self, bucket: &str, object: &str, version_id: Option<&str>) -> Result<()> {
|
async fn heal_object(&self, bucket: &str, object: &str, version_id: Option<&str>) -> Result<()> {
|
||||||
debug!(
|
debug!(
|
||||||
target: "rustfs::heal::task",
|
target: "rustfs::heal::task",
|
||||||
@@ -1406,7 +1404,6 @@ impl HealTask {
|
|||||||
self.heal_bucket_objects(bucket, prefix).await
|
self.heal_bucket_objects(bucket, prefix).await
|
||||||
}
|
}
|
||||||
|
|
||||||
#[hotpath::measure]
|
|
||||||
async fn heal_bucket_objects(&self, bucket: &str, prefix: &str) -> Result<()> {
|
async fn heal_bucket_objects(&self, bucket: &str, prefix: &str) -> Result<()> {
|
||||||
let mut continuation_token: Option<String> = None;
|
let mut continuation_token: Option<String> = None;
|
||||||
let mut scanned = 0u64;
|
let mut scanned = 0u64;
|
||||||
|
|||||||
@@ -28,55 +28,7 @@ documentation = "https://docs.rs/rustfs-iam/latest/rustfs_iam/"
|
|||||||
[lints]
|
[lints]
|
||||||
workspace = true
|
workspace = true
|
||||||
|
|
||||||
[features]
|
|
||||||
default = []
|
|
||||||
hotpath = [
|
|
||||||
"hotpath/hotpath",
|
|
||||||
"hotpath/tokio",
|
|
||||||
"hotpath/futures",
|
|
||||||
"hotpath/reqwest-0-13",
|
|
||||||
"rustfs-config/hotpath",
|
|
||||||
"rustfs-credentials/hotpath",
|
|
||||||
"rustfs-crypto/hotpath",
|
|
||||||
"rustfs-ecstore/hotpath",
|
|
||||||
"rustfs-io-metrics/hotpath",
|
|
||||||
"rustfs-madmin/hotpath",
|
|
||||||
"rustfs-policy/hotpath",
|
|
||||||
"rustfs-storage-api/hotpath",
|
|
||||||
"rustfs-utils/hotpath",
|
|
||||||
"rustfs-test-utils/hotpath",
|
|
||||||
]
|
|
||||||
hotpath-alloc = [
|
|
||||||
"hotpath",
|
|
||||||
"hotpath/hotpath-alloc",
|
|
||||||
"rustfs-config/hotpath-alloc",
|
|
||||||
"rustfs-credentials/hotpath-alloc",
|
|
||||||
"rustfs-crypto/hotpath-alloc",
|
|
||||||
"rustfs-ecstore/hotpath-alloc",
|
|
||||||
"rustfs-io-metrics/hotpath-alloc",
|
|
||||||
"rustfs-madmin/hotpath-alloc",
|
|
||||||
"rustfs-policy/hotpath-alloc",
|
|
||||||
"rustfs-storage-api/hotpath-alloc",
|
|
||||||
"rustfs-utils/hotpath-alloc",
|
|
||||||
"rustfs-test-utils/hotpath-alloc",
|
|
||||||
]
|
|
||||||
hotpath-cpu = [
|
|
||||||
"hotpath",
|
|
||||||
"hotpath/hotpath-cpu",
|
|
||||||
"rustfs-config/hotpath-cpu",
|
|
||||||
"rustfs-credentials/hotpath-cpu",
|
|
||||||
"rustfs-crypto/hotpath-cpu",
|
|
||||||
"rustfs-ecstore/hotpath-cpu",
|
|
||||||
"rustfs-io-metrics/hotpath-cpu",
|
|
||||||
"rustfs-madmin/hotpath-cpu",
|
|
||||||
"rustfs-policy/hotpath-cpu",
|
|
||||||
"rustfs-storage-api/hotpath-cpu",
|
|
||||||
"rustfs-utils/hotpath-cpu",
|
|
||||||
"rustfs-test-utils/hotpath-cpu",
|
|
||||||
]
|
|
||||||
|
|
||||||
[dependencies]
|
[dependencies]
|
||||||
hotpath.workspace = true
|
|
||||||
rustfs-credentials = { workspace = true }
|
rustfs-credentials = { workspace = true }
|
||||||
rustfs-config = { workspace = true, features = ["server-config-model"] }
|
rustfs-config = { workspace = true, features = ["server-config-model"] }
|
||||||
tokio = { workspace = true, features = ["fs", "rt-multi-thread"] }
|
tokio = { workspace = true, features = ["fs", "rt-multi-thread"] }
|
||||||
|
|||||||
@@ -469,7 +469,6 @@ impl ObjectStore {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
#[hotpath::measure]
|
|
||||||
async fn list_all_iamconfig_items(&self) -> Result<HashMap<String, Vec<String>>> {
|
async fn list_all_iamconfig_items(&self) -> Result<HashMap<String, Vec<String>>> {
|
||||||
let (tx, mut rx) = mpsc::channel::<StringOrErr>(100);
|
let (tx, mut rx) = mpsc::channel::<StringOrErr>(100);
|
||||||
|
|
||||||
@@ -509,7 +508,6 @@ impl ObjectStore {
|
|||||||
Ok(res)
|
Ok(res)
|
||||||
}
|
}
|
||||||
|
|
||||||
#[hotpath::measure]
|
|
||||||
async fn load_policy_doc_concurrent(&self, names: &[String], mode: LoadMode) -> Result<Vec<PolicyDoc>> {
|
async fn load_policy_doc_concurrent(&self, names: &[String], mode: LoadMode) -> Result<Vec<PolicyDoc>> {
|
||||||
let mut futures = Vec::with_capacity(names.len());
|
let mut futures = Vec::with_capacity(names.len());
|
||||||
|
|
||||||
|
|||||||
@@ -27,14 +27,7 @@ categories = ["development-tools", "filesystem"]
|
|||||||
[lints]
|
[lints]
|
||||||
workspace = true
|
workspace = true
|
||||||
|
|
||||||
[features]
|
|
||||||
default = []
|
|
||||||
hotpath = ["hotpath/hotpath", "hotpath/tokio", "rustfs-io-metrics/hotpath"]
|
|
||||||
hotpath-alloc = ["hotpath", "hotpath/hotpath-alloc", "rustfs-io-metrics/hotpath-alloc"]
|
|
||||||
hotpath-cpu = ["hotpath", "hotpath/hotpath-cpu", "rustfs-io-metrics/hotpath-cpu"]
|
|
||||||
|
|
||||||
[dependencies]
|
[dependencies]
|
||||||
hotpath.workspace = true
|
|
||||||
bytes = { workspace = true, features = ["serde"] }
|
bytes = { workspace = true, features = ["serde"] }
|
||||||
thiserror = { workspace = true }
|
thiserror = { workspace = true }
|
||||||
tokio = { workspace = true, features = ["io-util", "fs", "sync", "rt-multi-thread"] }
|
tokio = { workspace = true, features = ["io-util", "fs", "sync", "rt-multi-thread"] }
|
||||||
|
|||||||
@@ -28,32 +28,7 @@ categories = ["development-tools", "filesystem"]
|
|||||||
name = "metrics_pipeline"
|
name = "metrics_pipeline"
|
||||||
harness = false
|
harness = false
|
||||||
|
|
||||||
[features]
|
|
||||||
default = []
|
|
||||||
hotpath = [
|
|
||||||
"hotpath/hotpath",
|
|
||||||
"hotpath/tokio",
|
|
||||||
"rustfs-common/hotpath",
|
|
||||||
"rustfs-s3-ops/hotpath",
|
|
||||||
"rustfs-utils/hotpath",
|
|
||||||
]
|
|
||||||
hotpath-alloc = [
|
|
||||||
"hotpath",
|
|
||||||
"hotpath/hotpath-alloc",
|
|
||||||
"rustfs-common/hotpath-alloc",
|
|
||||||
"rustfs-s3-ops/hotpath-alloc",
|
|
||||||
"rustfs-utils/hotpath-alloc",
|
|
||||||
]
|
|
||||||
hotpath-cpu = [
|
|
||||||
"hotpath",
|
|
||||||
"hotpath/hotpath-cpu",
|
|
||||||
"rustfs-common/hotpath-cpu",
|
|
||||||
"rustfs-s3-ops/hotpath-cpu",
|
|
||||||
"rustfs-utils/hotpath-cpu",
|
|
||||||
]
|
|
||||||
|
|
||||||
[dependencies]
|
[dependencies]
|
||||||
hotpath.workspace = true
|
|
||||||
metrics = { workspace = true }
|
metrics = { workspace = true }
|
||||||
rustfs-common = { workspace = true }
|
rustfs-common = { workspace = true }
|
||||||
rustfs-s3-ops = { workspace = true }
|
rustfs-s3-ops = { workspace = true }
|
||||||
|
|||||||
@@ -28,34 +28,7 @@ authors.workspace = true
|
|||||||
[lints]
|
[lints]
|
||||||
workspace = true
|
workspace = true
|
||||||
|
|
||||||
[features]
|
|
||||||
default = []
|
|
||||||
hotpath = [
|
|
||||||
"hotpath/hotpath",
|
|
||||||
"hotpath/tokio",
|
|
||||||
"hotpath/futures",
|
|
||||||
"hotpath/reqwest-0-13",
|
|
||||||
"rustfs-credentials/hotpath",
|
|
||||||
"rustfs-policy/hotpath",
|
|
||||||
"rustfs-utils/hotpath",
|
|
||||||
]
|
|
||||||
hotpath-alloc = [
|
|
||||||
"hotpath",
|
|
||||||
"hotpath/hotpath-alloc",
|
|
||||||
"rustfs-credentials/hotpath-alloc",
|
|
||||||
"rustfs-policy/hotpath-alloc",
|
|
||||||
"rustfs-utils/hotpath-alloc",
|
|
||||||
]
|
|
||||||
hotpath-cpu = [
|
|
||||||
"hotpath",
|
|
||||||
"hotpath/hotpath-cpu",
|
|
||||||
"rustfs-credentials/hotpath-cpu",
|
|
||||||
"rustfs-policy/hotpath-cpu",
|
|
||||||
"rustfs-utils/hotpath-cpu",
|
|
||||||
]
|
|
||||||
|
|
||||||
[dependencies]
|
[dependencies]
|
||||||
hotpath.workspace = true
|
|
||||||
tokio = { workspace = true, features = ["rt", "sync"] }
|
tokio = { workspace = true, features = ["rt", "sync"] }
|
||||||
reqwest = { workspace = true, features = ["json"] }
|
reqwest = { workspace = true, features = ["json"] }
|
||||||
serde = { workspace = true, features = ["derive"] }
|
serde = { workspace = true, features = ["derive"] }
|
||||||
|
|||||||
@@ -95,7 +95,6 @@ impl KeystoneClient {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// Validate a Keystone token
|
/// Validate a Keystone token
|
||||||
#[hotpath::measure]
|
|
||||||
pub async fn validate_token(&self, token: &str) -> Result<KeystoneToken> {
|
pub async fn validate_token(&self, token: &str) -> Result<KeystoneToken> {
|
||||||
match self.version {
|
match self.version {
|
||||||
KeystoneVersion::V3 => self.validate_token_v3(token).await,
|
KeystoneVersion::V3 => self.validate_token_v3(token).await,
|
||||||
@@ -239,7 +238,6 @@ impl KeystoneClient {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// Get EC2 credentials for a user
|
/// Get EC2 credentials for a user
|
||||||
#[hotpath::measure]
|
|
||||||
pub async fn get_ec2_credentials(&self, user_id: &str, project_id: Option<&str>) -> Result<Vec<EC2Credential>> {
|
pub async fn get_ec2_credentials(&self, user_id: &str, project_id: Option<&str>) -> Result<Vec<EC2Credential>> {
|
||||||
let admin_token = self.get_admin_token().await?;
|
let admin_token = self.get_admin_token().await?;
|
||||||
|
|
||||||
|
|||||||
@@ -28,7 +28,6 @@ categories = ["cryptography", "web-programming", "authentication"]
|
|||||||
workspace = true
|
workspace = true
|
||||||
|
|
||||||
[dependencies]
|
[dependencies]
|
||||||
hotpath.workspace = true
|
|
||||||
# Core dependencies
|
# Core dependencies
|
||||||
async-trait = { workspace = true }
|
async-trait = { workspace = true }
|
||||||
tokio = { workspace = true, features = ["fs", "io-util", "macros", "rt-multi-thread", "sync", "time"] }
|
tokio = { workspace = true, features = ["fs", "io-util", "macros", "rt-multi-thread", "sync", "time"] }
|
||||||
@@ -38,8 +37,6 @@ serde = { workspace = true, features = ["derive"] }
|
|||||||
serde_json = { workspace = true, features = ["raw_value"] }
|
serde_json = { workspace = true, features = ["raw_value"] }
|
||||||
tracing = { workspace = true }
|
tracing = { workspace = true }
|
||||||
thiserror = { workspace = true }
|
thiserror = { workspace = true }
|
||||||
# Operation metrics emitted by the retry policy engine (crate::policy).
|
|
||||||
metrics = { workspace = true }
|
|
||||||
|
|
||||||
# Cryptography
|
# Cryptography
|
||||||
aes-gcm = { workspace = true, features = ["rand_core"] }
|
aes-gcm = { workspace = true, features = ["rand_core"] }
|
||||||
@@ -75,8 +72,6 @@ tokio-util = { workspace = true }
|
|||||||
|
|
||||||
[dev-dependencies]
|
[dev-dependencies]
|
||||||
anyhow = { workspace = true }
|
anyhow = { workspace = true }
|
||||||
# Debugging recorder for asserting emitted metrics in tests.
|
|
||||||
metrics-util = { version = "0.20", features = ["debugging"] }
|
|
||||||
insta = { workspace = true, features = ["yaml", "json"] }
|
insta = { workspace = true, features = ["yaml", "json"] }
|
||||||
tempfile = { workspace = true }
|
tempfile = { workspace = true }
|
||||||
temp-env = { workspace = true }
|
temp-env = { workspace = true }
|
||||||
@@ -85,17 +80,3 @@ tokio = { workspace = true, features = ["net", "test-util"] }
|
|||||||
|
|
||||||
[features]
|
[features]
|
||||||
default = []
|
default = []
|
||||||
hotpath = [
|
|
||||||
"hotpath/hotpath",
|
|
||||||
"hotpath/tokio",
|
|
||||||
"hotpath/reqwest-0-13",
|
|
||||||
"rustfs-security-governance/hotpath",
|
|
||||||
"rustfs-utils/hotpath",
|
|
||||||
]
|
|
||||||
hotpath-alloc = [
|
|
||||||
"hotpath",
|
|
||||||
"hotpath/hotpath-alloc",
|
|
||||||
"rustfs-security-governance/hotpath-alloc",
|
|
||||||
"rustfs-utils/hotpath-alloc",
|
|
||||||
]
|
|
||||||
hotpath-cpu = ["hotpath", "hotpath/hotpath-cpu", "rustfs-security-governance/hotpath-cpu", "rustfs-utils/hotpath-cpu"]
|
|
||||||
|
|||||||
@@ -27,11 +27,11 @@
|
|||||||
//! server, so they are `#[ignore]`d in CI. Static is covered by its own
|
//! server, so they are `#[ignore]`d in CI. Static is covered by its own
|
||||||
//! stateless contract below.
|
//! stateless contract below.
|
||||||
|
|
||||||
use super::KmsBackend;
|
|
||||||
use super::local::LocalKmsBackend;
|
use super::local::LocalKmsBackend;
|
||||||
use super::static_kms::StaticKmsBackend;
|
use super::static_kms::StaticKmsBackend;
|
||||||
use super::vault::VaultKmsBackend;
|
use super::vault::VaultKmsBackend;
|
||||||
use super::vault_transit::VaultTransitKmsBackend;
|
use super::vault_transit::VaultTransitKmsBackend;
|
||||||
|
use super::{KmsBackend, KmsClient};
|
||||||
use crate::config::KmsConfig;
|
use crate::config::KmsConfig;
|
||||||
use crate::error::{KmsError, Result};
|
use crate::error::{KmsError, Result};
|
||||||
use crate::manager::KmsManager;
|
use crate::manager::KmsManager;
|
||||||
@@ -46,24 +46,6 @@ use rand::RngExt as _;
|
|||||||
use std::collections::HashMap;
|
use std::collections::HashMap;
|
||||||
use std::sync::Arc;
|
use std::sync::Arc;
|
||||||
|
|
||||||
fn expect_unsupported<T: std::fmt::Debug>(result: Result<T>) {
|
|
||||||
match result {
|
|
||||||
Err(KmsError::UnsupportedCapability { .. }) => {}
|
|
||||||
other => panic!("expected UnsupportedCapability, got {other:?}"),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Rotation while not Enabled: backends with rotation support must reject it
|
|
||||||
/// through the state machine; backends without it report the capability gap.
|
|
||||||
async fn expect_rotate_rejected(backend: &dyn KmsBackend, key_id: &str) {
|
|
||||||
let result = backend.rotate_key(key_id).await;
|
|
||||||
if backend.capabilities().rotate {
|
|
||||||
expect_invalid_key_state(result, "");
|
|
||||||
} else {
|
|
||||||
expect_unsupported(result);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fn expect_invalid_key_state<T: std::fmt::Debug>(result: Result<T>, expected_fragment: &str) {
|
fn expect_invalid_key_state<T: std::fmt::Debug>(result: Result<T>, expected_fragment: &str) {
|
||||||
match result {
|
match result {
|
||||||
Err(KmsError::InvalidOperation { message }) => assert!(
|
Err(KmsError::InvalidOperation { message }) => assert!(
|
||||||
@@ -135,9 +117,11 @@ async fn assert_key_state(backend: &dyn KmsBackend, key_id: &str, expected: KeyS
|
|||||||
assert_eq!(described.key_metadata.key_state, expected, "unexpected state for key {key_id}");
|
assert_eq!(described.key_metadata.key_state, expected, "unexpected state for key {key_id}");
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Drives one freshly created (Enabled) key through the full state matrix,
|
/// Drives one freshly created (Enabled) key through the full state matrix.
|
||||||
/// entirely through the `KmsBackend` product surface.
|
///
|
||||||
async fn assert_state_machine_contract(backend: &dyn KmsBackend, key_id: &str) {
|
/// `backend` is the product surface; `client` drives the lifecycle
|
||||||
|
/// transitions not yet exposed through `KmsBackend`.
|
||||||
|
async fn assert_state_machine_contract(backend: &dyn KmsBackend, client: &dyn KmsClient, key_id: &str) {
|
||||||
// Enabled: cryptographic use is allowed. Keep an envelope around to prove
|
// Enabled: cryptographic use is allowed. Keep an envelope around to prove
|
||||||
// decryption keeps working in later states.
|
// decryption keeps working in later states.
|
||||||
let data_key = backend
|
let data_key = backend
|
||||||
@@ -150,13 +134,16 @@ async fn assert_state_machine_contract(backend: &dyn KmsBackend, key_id: &str) {
|
|||||||
.expect("Enabled key must encrypt");
|
.expect("Enabled key must encrypt");
|
||||||
|
|
||||||
// Enabled -> Disabled.
|
// Enabled -> Disabled.
|
||||||
backend.disable_key(key_id).await.expect("disable from Enabled must succeed");
|
client
|
||||||
|
.disable_key(key_id, None)
|
||||||
|
.await
|
||||||
|
.expect("disable from Enabled must succeed");
|
||||||
assert_key_state(backend, key_id, KeyState::Disabled).await;
|
assert_key_state(backend, key_id, KeyState::Disabled).await;
|
||||||
|
|
||||||
// Disabled: new cryptographic use and rotation are rejected...
|
// Disabled: new cryptographic use and rotation are rejected...
|
||||||
expect_invalid_key_state(backend.encrypt(encrypt_request(key_id)).await, "disabled");
|
expect_invalid_key_state(backend.encrypt(encrypt_request(key_id)).await, "disabled");
|
||||||
expect_invalid_key_state(backend.generate_data_key(generate_request(key_id)).await, "disabled");
|
expect_invalid_key_state(backend.generate_data_key(generate_request(key_id)).await, "disabled");
|
||||||
expect_rotate_rejected(backend, key_id).await;
|
expect_invalid_key_state(client.rotate_key(key_id, None).await, "");
|
||||||
// ...but decryption of existing data keeps working (explicit AWS deviation)...
|
// ...but decryption of existing data keeps working (explicit AWS deviation)...
|
||||||
let decrypted = backend
|
let decrypted = backend
|
||||||
.decrypt(decrypt_request(data_key.ciphertext_blob.clone()))
|
.decrypt(decrypt_request(data_key.ciphertext_blob.clone()))
|
||||||
@@ -164,14 +151,17 @@ async fn assert_state_machine_contract(backend: &dyn KmsBackend, key_id: &str) {
|
|||||||
.expect("decrypt with a disabled key must keep working");
|
.expect("decrypt with a disabled key must keep working");
|
||||||
assert_eq!(decrypted.plaintext, data_key.plaintext_key, "decrypt must recover the original data key");
|
assert_eq!(decrypted.plaintext, data_key.plaintext_key, "decrypt must recover the original data key");
|
||||||
// ...disable stays idempotent, cancel has nothing to cancel, and enable recovers.
|
// ...disable stays idempotent, cancel has nothing to cancel, and enable recovers.
|
||||||
backend.disable_key(key_id).await.expect("disable must be idempotent");
|
client.disable_key(key_id, None).await.expect("disable must be idempotent");
|
||||||
expect_invalid_key_state(backend.cancel_key_deletion(cancel_request(key_id)).await, "not pending deletion");
|
expect_invalid_key_state(backend.cancel_key_deletion(cancel_request(key_id)).await, "not pending deletion");
|
||||||
backend.enable_key(key_id).await.expect("enable from Disabled must succeed");
|
client
|
||||||
|
.enable_key(key_id, None)
|
||||||
|
.await
|
||||||
|
.expect("enable from Disabled must succeed");
|
||||||
assert_key_state(backend, key_id, KeyState::Enabled).await;
|
assert_key_state(backend, key_id, KeyState::Enabled).await;
|
||||||
|
|
||||||
// Disabled keys may still be scheduled for deletion.
|
// Disabled keys may still be scheduled for deletion.
|
||||||
backend
|
client
|
||||||
.disable_key(key_id)
|
.disable_key(key_id, None)
|
||||||
.await
|
.await
|
||||||
.expect("disable before scheduling must succeed");
|
.expect("disable before scheduling must succeed");
|
||||||
backend
|
backend
|
||||||
@@ -183,9 +173,10 @@ async fn assert_state_machine_contract(backend: &dyn KmsBackend, key_id: &str) {
|
|||||||
// PendingDeletion: everything except decryption and cancellation is rejected.
|
// PendingDeletion: everything except decryption and cancellation is rejected.
|
||||||
expect_invalid_key_state(backend.encrypt(encrypt_request(key_id)).await, "pending deletion");
|
expect_invalid_key_state(backend.encrypt(encrypt_request(key_id)).await, "pending deletion");
|
||||||
expect_invalid_key_state(backend.generate_data_key(generate_request(key_id)).await, "pending deletion");
|
expect_invalid_key_state(backend.generate_data_key(generate_request(key_id)).await, "pending deletion");
|
||||||
expect_invalid_key_state(backend.enable_key(key_id).await, "pending deletion");
|
expect_invalid_key_state(client.enable_key(key_id, None).await, "pending deletion");
|
||||||
expect_invalid_key_state(backend.disable_key(key_id).await, "pending deletion");
|
expect_invalid_key_state(client.disable_key(key_id, None).await, "pending deletion");
|
||||||
expect_rotate_rejected(backend, key_id).await;
|
expect_invalid_key_state(client.rotate_key(key_id, None).await, "");
|
||||||
|
expect_invalid_key_state(client.schedule_key_deletion(key_id, 7, None).await, "pending deletion");
|
||||||
expect_invalid_key_state(backend.delete_key(schedule_request(key_id)).await, "pending deletion");
|
expect_invalid_key_state(backend.delete_key(schedule_request(key_id)).await, "pending deletion");
|
||||||
let decrypted = backend
|
let decrypted = backend
|
||||||
.decrypt(decrypt_request(data_key.ciphertext_blob.clone()))
|
.decrypt(decrypt_request(data_key.ciphertext_blob.clone()))
|
||||||
@@ -224,7 +215,7 @@ async fn local_fixture() -> (tempfile::TempDir, KmsConfig, LocalKmsBackend, Stri
|
|||||||
#[tokio::test]
|
#[tokio::test]
|
||||||
async fn local_backend_state_machine_contract() {
|
async fn local_backend_state_machine_contract() {
|
||||||
let (_temp_dir, _config, backend, key_id) = local_fixture().await;
|
let (_temp_dir, _config, backend, key_id) = local_fixture().await;
|
||||||
assert_state_machine_contract(&backend, &key_id).await;
|
assert_state_machine_contract(&backend, backend.lifecycle_client(), &key_id).await;
|
||||||
}
|
}
|
||||||
|
|
||||||
/// SSE-shaped regression: disabling a key must not break decryption of data
|
/// SSE-shaped regression: disabling a key must not break decryption of data
|
||||||
@@ -265,7 +256,10 @@ async fn static_backend_stateless_contract() {
|
|||||||
rand::rng().fill(&mut raw_key[..]);
|
rand::rng().fill(&mut raw_key[..]);
|
||||||
let config = KmsConfig::static_kms(key_id.to_string(), BASE64.encode(raw_key));
|
let config = KmsConfig::static_kms(key_id.to_string(), BASE64.encode(raw_key));
|
||||||
let static_backend = StaticKmsBackend::new(config).await.expect("static backend should build");
|
let static_backend = StaticKmsBackend::new(config).await.expect("static backend should build");
|
||||||
|
// StaticKmsBackend implements both traits with overlapping method names,
|
||||||
|
// so pin each surface once instead of qualifying every call.
|
||||||
let backend: &dyn KmsBackend = &static_backend;
|
let backend: &dyn KmsBackend = &static_backend;
|
||||||
|
let client: &dyn KmsClient = &static_backend;
|
||||||
|
|
||||||
let data_key = backend
|
let data_key = backend
|
||||||
.generate_data_key(generate_request(key_id))
|
.generate_data_key(generate_request(key_id))
|
||||||
@@ -281,11 +275,9 @@ async fn static_backend_stateless_contract() {
|
|||||||
expect_invalid_key_state(backend.create_key(create_request("another-key".to_string())).await, "read-only");
|
expect_invalid_key_state(backend.create_key(create_request("another-key".to_string())).await, "read-only");
|
||||||
expect_invalid_key_state(backend.delete_key(schedule_request(key_id)).await, "read-only");
|
expect_invalid_key_state(backend.delete_key(schedule_request(key_id)).await, "read-only");
|
||||||
expect_invalid_key_state(backend.cancel_key_deletion(cancel_request(key_id)).await, "read-only");
|
expect_invalid_key_state(backend.cancel_key_deletion(cancel_request(key_id)).await, "read-only");
|
||||||
// Enable/disable and rotation are capability gaps at the product
|
expect_invalid_key_state(client.disable_key(key_id, None).await, "read-only");
|
||||||
// surface, not state-machine rejections.
|
expect_invalid_key_state(client.schedule_key_deletion(key_id, 7, None).await, "read-only");
|
||||||
expect_unsupported(backend.enable_key(key_id).await);
|
expect_invalid_key_state(client.rotate_key(key_id, None).await, "read-only");
|
||||||
expect_unsupported(backend.disable_key(key_id).await);
|
|
||||||
expect_unsupported(backend.rotate_key(key_id).await);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
fn vault_dev_config(constructor: fn(url::Url, String) -> KmsConfig) -> KmsConfig {
|
fn vault_dev_config(constructor: fn(url::Url, String) -> KmsConfig) -> KmsConfig {
|
||||||
@@ -306,15 +298,7 @@ async fn vault_kv2_backend_state_machine_contract() {
|
|||||||
.await
|
.await
|
||||||
.expect("key should be created");
|
.expect("key should be created");
|
||||||
|
|
||||||
assert_state_machine_contract(&backend, &created.key_id).await;
|
assert_state_machine_contract(&backend, backend.lifecycle_client(), &created.key_id).await;
|
||||||
|
|
||||||
// KV2 additionally supports version-retaining rotation, which must only
|
|
||||||
// work while the key is Enabled (the shared matrix covered the
|
|
||||||
// rejections).
|
|
||||||
backend
|
|
||||||
.rotate_key(&created.key_id)
|
|
||||||
.await
|
|
||||||
.expect("rotation of an Enabled KV2 key must succeed");
|
|
||||||
|
|
||||||
// Cleanup: leave the key pending deletion so repeated runs stay tidy.
|
// Cleanup: leave the key pending deletion so repeated runs stay tidy.
|
||||||
let _ = backend.delete_key(schedule_request(&created.key_id)).await;
|
let _ = backend.delete_key(schedule_request(&created.key_id)).await;
|
||||||
@@ -332,12 +316,13 @@ async fn vault_transit_backend_state_machine_contract() {
|
|||||||
.await
|
.await
|
||||||
.expect("key should be created");
|
.expect("key should be created");
|
||||||
|
|
||||||
assert_state_machine_contract(&backend, &created.key_id).await;
|
assert_state_machine_contract(&backend, backend.lifecycle_client(), &created.key_id).await;
|
||||||
|
|
||||||
// Transit additionally supports rotation, which must only work while the
|
// Transit additionally supports rotation, which must only work while the
|
||||||
// key is Enabled (the shared matrix already covered the rejections).
|
// key is Enabled (the shared matrix already covered the rejections).
|
||||||
backend
|
backend
|
||||||
.rotate_key(&created.key_id)
|
.lifecycle_client()
|
||||||
|
.rotate_key(&created.key_id, None)
|
||||||
.await
|
.await
|
||||||
.expect("rotation of an Enabled transit key must succeed");
|
.expect("rotation of an Enabled transit key must succeed");
|
||||||
|
|
||||||
|
|||||||
@@ -14,7 +14,9 @@
|
|||||||
|
|
||||||
//! Local file-based KMS backend implementation
|
//! Local file-based KMS backend implementation
|
||||||
|
|
||||||
use crate::backends::{BackendCapabilities, ExpiredKeyRemoval, KmsBackend, StateGatedOperation, ensure_key_status_permits};
|
use crate::backends::{
|
||||||
|
BackendCapabilities, BackendInfo, ExpiredKeyRemoval, KmsBackend, KmsClient, StateGatedOperation, ensure_key_status_permits,
|
||||||
|
};
|
||||||
use crate::config::KmsConfig;
|
use crate::config::KmsConfig;
|
||||||
use crate::config::LocalConfig;
|
use crate::config::LocalConfig;
|
||||||
use crate::encryption::{AesDekCrypto, DataKeyEnvelope, DekCrypto, generate_key_material};
|
use crate::encryption::{AesDekCrypto, DataKeyEnvelope, DekCrypto, generate_key_material};
|
||||||
@@ -935,12 +937,9 @@ impl LocalKmsClient {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl LocalKmsClient {
|
#[async_trait]
|
||||||
pub(crate) async fn generate_data_key(
|
impl KmsClient for LocalKmsClient {
|
||||||
&self,
|
async fn generate_data_key(&self, request: &GenerateKeyRequest, context: Option<&OperationContext>) -> Result<DataKeyInfo> {
|
||||||
request: &GenerateKeyRequest,
|
|
||||||
context: Option<&OperationContext>,
|
|
||||||
) -> Result<DataKeyInfo> {
|
|
||||||
debug!("Generating data key for master key: {}", request.master_key_id);
|
debug!("Generating data key for master key: {}", request.master_key_id);
|
||||||
|
|
||||||
let key_info = self.describe_key(&request.master_key_id, context).await?;
|
let key_info = self.describe_key(&request.master_key_id, context).await?;
|
||||||
@@ -981,7 +980,7 @@ impl LocalKmsClient {
|
|||||||
Ok(data_key)
|
Ok(data_key)
|
||||||
}
|
}
|
||||||
|
|
||||||
pub(crate) async fn encrypt(&self, request: &EncryptRequest, context: Option<&OperationContext>) -> Result<EncryptResponse> {
|
async fn encrypt(&self, request: &EncryptRequest, context: Option<&OperationContext>) -> Result<EncryptResponse> {
|
||||||
debug!("Encrypting data with key: {}", request.key_id);
|
debug!("Encrypting data with key: {}", request.key_id);
|
||||||
|
|
||||||
// Verify key exists and its state allows encryption
|
// Verify key exists and its state allows encryption
|
||||||
@@ -998,7 +997,7 @@ impl LocalKmsClient {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
pub(crate) async fn decrypt(&self, request: &DecryptRequest, _context: Option<&OperationContext>) -> Result<Vec<u8>> {
|
async fn decrypt(&self, request: &DecryptRequest, _context: Option<&OperationContext>) -> Result<Vec<u8>> {
|
||||||
debug!("Decrypting data");
|
debug!("Decrypting data");
|
||||||
|
|
||||||
// Parse the data key envelope from ciphertext
|
// Parse the data key envelope from ciphertext
|
||||||
@@ -1032,14 +1031,7 @@ impl LocalKmsClient {
|
|||||||
Ok(plaintext)
|
Ok(plaintext)
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Test-only lifecycle driver: the product path goes through [`KmsBackend`].
|
async fn create_key(&self, key_id: &str, algorithm: &str, context: Option<&OperationContext>) -> Result<MasterKeyInfo> {
|
||||||
#[cfg(test)]
|
|
||||||
pub(crate) async fn create_key(
|
|
||||||
&self,
|
|
||||||
key_id: &str,
|
|
||||||
algorithm: &str,
|
|
||||||
context: Option<&OperationContext>,
|
|
||||||
) -> Result<MasterKeyInfo> {
|
|
||||||
debug!("Creating master key: {}", key_id);
|
debug!("Creating master key: {}", key_id);
|
||||||
|
|
||||||
// Check if key already exists
|
// Check if key already exists
|
||||||
@@ -1068,18 +1060,14 @@ impl LocalKmsClient {
|
|||||||
Ok(master_key)
|
Ok(master_key)
|
||||||
}
|
}
|
||||||
|
|
||||||
pub(crate) async fn describe_key(&self, key_id: &str, _context: Option<&OperationContext>) -> Result<KeyInfo> {
|
async fn describe_key(&self, key_id: &str, _context: Option<&OperationContext>) -> Result<KeyInfo> {
|
||||||
debug!("Describing key: {}", key_id);
|
debug!("Describing key: {}", key_id);
|
||||||
|
|
||||||
let master_key = self.load_master_key(key_id).await?;
|
let master_key = self.load_master_key(key_id).await?;
|
||||||
Ok(master_key.into())
|
Ok(master_key.into())
|
||||||
}
|
}
|
||||||
|
|
||||||
pub(crate) async fn list_keys(
|
async fn list_keys(&self, request: &ListKeysRequest, _context: Option<&OperationContext>) -> Result<ListKeysResponse> {
|
||||||
&self,
|
|
||||||
request: &ListKeysRequest,
|
|
||||||
_context: Option<&OperationContext>,
|
|
||||||
) -> Result<ListKeysResponse> {
|
|
||||||
debug!("Listing keys");
|
debug!("Listing keys");
|
||||||
|
|
||||||
let mut keys = Vec::new();
|
let mut keys = Vec::new();
|
||||||
@@ -1123,7 +1111,7 @@ impl LocalKmsClient {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
pub(crate) async fn enable_key(&self, key_id: &str, _context: Option<&OperationContext>) -> Result<()> {
|
async fn enable_key(&self, key_id: &str, _context: Option<&OperationContext>) -> Result<()> {
|
||||||
debug!("Enabling key: {}", key_id);
|
debug!("Enabling key: {}", key_id);
|
||||||
|
|
||||||
let _write_guard = self.lock_key_for_write(key_id).await;
|
let _write_guard = self.lock_key_for_write(key_id).await;
|
||||||
@@ -1141,7 +1129,7 @@ impl LocalKmsClient {
|
|||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
pub(crate) async fn disable_key(&self, key_id: &str, _context: Option<&OperationContext>) -> Result<()> {
|
async fn disable_key(&self, key_id: &str, _context: Option<&OperationContext>) -> Result<()> {
|
||||||
debug!("Disabling key: {}", key_id);
|
debug!("Disabling key: {}", key_id);
|
||||||
|
|
||||||
let _write_guard = self.lock_key_for_write(key_id).await;
|
let _write_guard = self.lock_key_for_write(key_id).await;
|
||||||
@@ -1158,9 +1146,7 @@ impl LocalKmsClient {
|
|||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Test-only lifecycle driver: the product path goes through [`KmsBackend`].
|
async fn schedule_key_deletion(
|
||||||
#[cfg(test)]
|
|
||||||
pub(crate) async fn schedule_key_deletion(
|
|
||||||
&self,
|
&self,
|
||||||
key_id: &str,
|
key_id: &str,
|
||||||
pending_window_days: u32,
|
pending_window_days: u32,
|
||||||
@@ -1184,9 +1170,7 @@ impl LocalKmsClient {
|
|||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Test-only lifecycle driver: the product path goes through [`KmsBackend`].
|
async fn cancel_key_deletion(&self, key_id: &str, _context: Option<&OperationContext>) -> Result<()> {
|
||||||
#[cfg(test)]
|
|
||||||
pub(crate) async fn cancel_key_deletion(&self, key_id: &str, _context: Option<&OperationContext>) -> Result<()> {
|
|
||||||
debug!("Canceling deletion for key: {}", key_id);
|
debug!("Canceling deletion for key: {}", key_id);
|
||||||
|
|
||||||
let _write_guard = self.lock_key_for_write(key_id).await;
|
let _write_guard = self.lock_key_for_write(key_id).await;
|
||||||
@@ -1206,9 +1190,7 @@ impl LocalKmsClient {
|
|||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Test-only lifecycle driver: the product path goes through [`KmsBackend`].
|
async fn rotate_key(&self, key_id: &str, _context: Option<&OperationContext>) -> Result<MasterKeyInfo> {
|
||||||
#[cfg(test)]
|
|
||||||
pub(crate) async fn rotate_key(&self, key_id: &str, _context: Option<&OperationContext>) -> Result<MasterKeyInfo> {
|
|
||||||
if !fs::try_exists(self.master_key_path(key_id)?).await? {
|
if !fs::try_exists(self.master_key_path(key_id)?).await? {
|
||||||
return Err(KmsError::key_not_found(key_id));
|
return Err(KmsError::key_not_found(key_id));
|
||||||
}
|
}
|
||||||
@@ -1217,7 +1199,7 @@ impl LocalKmsClient {
|
|||||||
))
|
))
|
||||||
}
|
}
|
||||||
|
|
||||||
pub(crate) async fn health_check(&self) -> Result<()> {
|
async fn health_check(&self) -> Result<()> {
|
||||||
// Check if key directory is accessible
|
// Check if key directory is accessible
|
||||||
if !self.config.key_dir.exists() {
|
if !self.config.key_dir.exists() {
|
||||||
return Err(KmsError::backend_error("Key directory does not exist"));
|
return Err(KmsError::backend_error("Key directory does not exist"));
|
||||||
@@ -1228,6 +1210,17 @@ impl LocalKmsClient {
|
|||||||
|
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn backend_info(&self) -> BackendInfo {
|
||||||
|
BackendInfo::new(
|
||||||
|
"local".to_string(),
|
||||||
|
env!("CARGO_PKG_VERSION").to_string(),
|
||||||
|
self.config.key_dir.to_string_lossy().to_string(),
|
||||||
|
true, // We'll assume healthy for now
|
||||||
|
)
|
||||||
|
.with_metadata("key_dir".to_string(), self.config.key_dir.to_string_lossy().to_string())
|
||||||
|
.with_metadata("encrypted_at_rest".to_string(), self.master_cipher.is_some().to_string())
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// LocalKmsBackend wraps LocalKmsClient and implements the KmsBackend trait
|
/// LocalKmsBackend wraps LocalKmsClient and implements the KmsBackend trait
|
||||||
|
|||||||
@@ -19,6 +19,7 @@ use crate::types::*;
|
|||||||
use async_trait::async_trait;
|
use async_trait::async_trait;
|
||||||
use jiff::Zoned;
|
use jiff::Zoned;
|
||||||
use serde::{Deserialize, Serialize};
|
use serde::{Deserialize, Serialize};
|
||||||
|
use std::collections::HashMap;
|
||||||
|
|
||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
mod contract_tests;
|
mod contract_tests;
|
||||||
@@ -98,6 +99,136 @@ pub(crate) fn ensure_key_status_permits(key_id: &str, status: &KeyStatus, operat
|
|||||||
ensure_key_state_permits(key_id, &state, operation)
|
ensure_key_state_permits(key_id, &state, operation)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Abstract KMS client interface that all backends must implement
|
||||||
|
#[async_trait]
|
||||||
|
pub trait KmsClient: Send + Sync {
|
||||||
|
/// Generate a new data encryption key (DEK)
|
||||||
|
///
|
||||||
|
/// Creates a new data key using the specified master key. The returned DataKey
|
||||||
|
/// contains both the plaintext and encrypted versions of the key.
|
||||||
|
///
|
||||||
|
/// # Arguments
|
||||||
|
/// * `request` - The key generation request
|
||||||
|
/// * `context` - Optional operation context for auditing
|
||||||
|
///
|
||||||
|
/// # Returns
|
||||||
|
/// Returns a DataKey containing both plaintext and encrypted key material
|
||||||
|
async fn generate_data_key(&self, request: &GenerateKeyRequest, context: Option<&OperationContext>) -> Result<DataKeyInfo>;
|
||||||
|
|
||||||
|
/// Encrypt data directly using a master key
|
||||||
|
///
|
||||||
|
/// Encrypts the provided plaintext using the specified master key.
|
||||||
|
/// This is different from generate_data_key as it encrypts user data directly.
|
||||||
|
///
|
||||||
|
/// # Arguments
|
||||||
|
/// * `request` - The encryption request containing plaintext and key ID
|
||||||
|
/// * `context` - Optional operation context for auditing
|
||||||
|
async fn encrypt(&self, request: &EncryptRequest, context: Option<&OperationContext>) -> Result<EncryptResponse>;
|
||||||
|
|
||||||
|
/// Decrypt data using a master key
|
||||||
|
///
|
||||||
|
/// Decrypts the provided ciphertext. The KMS automatically determines
|
||||||
|
/// which key was used for encryption based on the ciphertext metadata.
|
||||||
|
///
|
||||||
|
/// # Arguments
|
||||||
|
/// * `request` - The decryption request containing ciphertext
|
||||||
|
/// * `context` - Optional operation context for auditing
|
||||||
|
async fn decrypt(&self, request: &DecryptRequest, context: Option<&OperationContext>) -> Result<Vec<u8>>;
|
||||||
|
|
||||||
|
/// Create a new master key
|
||||||
|
///
|
||||||
|
/// Creates a new master key in the KMS with the specified ID.
|
||||||
|
/// Returns an error if a key with the same ID already exists.
|
||||||
|
///
|
||||||
|
/// # Arguments
|
||||||
|
/// * `key_id` - Unique identifier for the new key
|
||||||
|
/// * `algorithm` - Key algorithm (e.g., "AES_256")
|
||||||
|
/// * `context` - Optional operation context for auditing
|
||||||
|
async fn create_key(&self, key_id: &str, algorithm: &str, context: Option<&OperationContext>) -> Result<MasterKeyInfo>;
|
||||||
|
|
||||||
|
/// Get information about a specific key
|
||||||
|
///
|
||||||
|
/// Returns metadata and information about the specified key.
|
||||||
|
///
|
||||||
|
/// # Arguments
|
||||||
|
/// * `key_id` - The key identifier
|
||||||
|
/// * `context` - Optional operation context for auditing
|
||||||
|
async fn describe_key(&self, key_id: &str, context: Option<&OperationContext>) -> Result<KeyInfo>;
|
||||||
|
|
||||||
|
/// List available keys
|
||||||
|
///
|
||||||
|
/// Returns a paginated list of keys available in the KMS.
|
||||||
|
///
|
||||||
|
/// # Arguments
|
||||||
|
/// * `request` - List request parameters (pagination, filters)
|
||||||
|
/// * `context` - Optional operation context for auditing
|
||||||
|
async fn list_keys(&self, request: &ListKeysRequest, context: Option<&OperationContext>) -> Result<ListKeysResponse>;
|
||||||
|
|
||||||
|
/// Enable a key
|
||||||
|
///
|
||||||
|
/// Enables a previously disabled key, allowing it to be used for cryptographic operations.
|
||||||
|
///
|
||||||
|
/// # Arguments
|
||||||
|
/// * `key_id` - The key identifier
|
||||||
|
/// * `context` - Optional operation context for auditing
|
||||||
|
async fn enable_key(&self, key_id: &str, context: Option<&OperationContext>) -> Result<()>;
|
||||||
|
|
||||||
|
/// Disable a key
|
||||||
|
///
|
||||||
|
/// Disables a key, preventing it from being used for new cryptographic operations.
|
||||||
|
/// Existing encrypted data can still be decrypted.
|
||||||
|
///
|
||||||
|
/// # Arguments
|
||||||
|
/// * `key_id` - The key identifier
|
||||||
|
/// * `context` - Optional operation context for auditing
|
||||||
|
async fn disable_key(&self, key_id: &str, context: Option<&OperationContext>) -> Result<()>;
|
||||||
|
|
||||||
|
/// Schedule key deletion
|
||||||
|
///
|
||||||
|
/// Schedules a key for deletion after a specified number of days.
|
||||||
|
/// This allows for a grace period to recover the key if needed.
|
||||||
|
///
|
||||||
|
/// # Arguments
|
||||||
|
/// * `key_id` - The key identifier
|
||||||
|
/// * `pending_window_days` - Number of days before actual deletion
|
||||||
|
/// * `context` - Optional operation context for auditing
|
||||||
|
async fn schedule_key_deletion(
|
||||||
|
&self,
|
||||||
|
key_id: &str,
|
||||||
|
pending_window_days: u32,
|
||||||
|
context: Option<&OperationContext>,
|
||||||
|
) -> Result<()>;
|
||||||
|
|
||||||
|
/// Cancel key deletion
|
||||||
|
///
|
||||||
|
/// Cancels a previously scheduled key deletion.
|
||||||
|
///
|
||||||
|
/// # Arguments
|
||||||
|
/// * `key_id` - The key identifier
|
||||||
|
/// * `context` - Optional operation context for auditing
|
||||||
|
async fn cancel_key_deletion(&self, key_id: &str, context: Option<&OperationContext>) -> Result<()>;
|
||||||
|
|
||||||
|
/// Rotate a key
|
||||||
|
///
|
||||||
|
/// Creates a new version of the specified key. Previous versions remain
|
||||||
|
/// available for decryption but new operations will use the new version.
|
||||||
|
///
|
||||||
|
/// # Arguments
|
||||||
|
/// * `key_id` - The key identifier
|
||||||
|
/// * `context` - Optional operation context for auditing
|
||||||
|
async fn rotate_key(&self, key_id: &str, context: Option<&OperationContext>) -> Result<MasterKeyInfo>;
|
||||||
|
|
||||||
|
/// Health check
|
||||||
|
///
|
||||||
|
/// Performs a health check on the KMS backend to ensure it's operational.
|
||||||
|
async fn health_check(&self) -> Result<()>;
|
||||||
|
|
||||||
|
/// Get backend information
|
||||||
|
///
|
||||||
|
/// Returns information about the KMS backend (type, version, etc.).
|
||||||
|
fn backend_info(&self) -> BackendInfo;
|
||||||
|
}
|
||||||
|
|
||||||
/// Simplified KMS backend interface for manager
|
/// Simplified KMS backend interface for manager
|
||||||
#[async_trait]
|
#[async_trait]
|
||||||
pub trait KmsBackend: Send + Sync {
|
pub trait KmsBackend: Send + Sync {
|
||||||
@@ -196,6 +327,58 @@ pub enum ExpiredKeyRemoval {
|
|||||||
NotExpired,
|
NotExpired,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Information about a KMS backend
|
||||||
|
#[derive(Debug, Clone)]
|
||||||
|
pub struct BackendInfo {
|
||||||
|
/// Backend type name (e.g., "local", "vault")
|
||||||
|
pub backend_type: String,
|
||||||
|
/// Backend version
|
||||||
|
pub version: String,
|
||||||
|
/// Backend endpoint or location
|
||||||
|
pub endpoint: String,
|
||||||
|
/// Whether the backend is currently healthy
|
||||||
|
pub healthy: bool,
|
||||||
|
/// Additional metadata about the backend
|
||||||
|
pub metadata: HashMap<String, String>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl BackendInfo {
|
||||||
|
/// Create a new backend info
|
||||||
|
///
|
||||||
|
/// # Arguments
|
||||||
|
/// * `backend_type` - The type of the backend
|
||||||
|
/// * `version` - The version of the backend
|
||||||
|
/// * `endpoint` - The endpoint or location of the backend
|
||||||
|
/// * `healthy` - Whether the backend is healthy
|
||||||
|
///
|
||||||
|
/// # Returns
|
||||||
|
/// A new BackendInfo instance
|
||||||
|
///
|
||||||
|
pub fn new(backend_type: String, version: String, endpoint: String, healthy: bool) -> Self {
|
||||||
|
Self {
|
||||||
|
backend_type,
|
||||||
|
version,
|
||||||
|
endpoint,
|
||||||
|
healthy,
|
||||||
|
metadata: HashMap::new(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Add metadata to the backend info
|
||||||
|
///
|
||||||
|
/// # Arguments
|
||||||
|
/// * `key` - Metadata key
|
||||||
|
/// * `value` - Metadata value
|
||||||
|
///
|
||||||
|
/// # Returns
|
||||||
|
/// Updated BackendInfo instance
|
||||||
|
///
|
||||||
|
pub fn with_metadata(mut self, key: String, value: String) -> Self {
|
||||||
|
self.metadata.insert(key, value);
|
||||||
|
self
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/// Set of operations a KMS backend supports.
|
/// Set of operations a KMS backend supports.
|
||||||
///
|
///
|
||||||
/// Reported by [`KmsBackend::capabilities`] so callers (manager, admin API)
|
/// Reported by [`KmsBackend::capabilities`] so callers (manager, admin API)
|
||||||
|
|||||||
+2
-2
@@ -8,7 +8,7 @@ expression: capabilities_snapshot(backend.capabilities())
|
|||||||
"encrypt": true,
|
"encrypt": true,
|
||||||
"generate_data_key": true,
|
"generate_data_key": true,
|
||||||
"physical_delete": true,
|
"physical_delete": true,
|
||||||
"rotate": true,
|
"rotate": false,
|
||||||
"schedule_deletion": true,
|
"schedule_deletion": true,
|
||||||
"versioning": true
|
"versioning": false
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -21,7 +21,7 @@
|
|||||||
//!
|
//!
|
||||||
//! encrypted_data(plaintext_len+16) || nonce (12 bytes)
|
//! encrypted_data(plaintext_len+16) || nonce (12 bytes)
|
||||||
|
|
||||||
use crate::backends::{BackendCapabilities, KmsBackend};
|
use crate::backends::{BackendCapabilities, BackendInfo, KmsBackend, KmsClient};
|
||||||
use crate::config::{BackendConfig, KmsConfig};
|
use crate::config::{BackendConfig, KmsConfig};
|
||||||
use crate::encryption::DataKeyEnvelope;
|
use crate::encryption::DataKeyEnvelope;
|
||||||
use crate::error::{KmsError, Result};
|
use crate::error::{KmsError, Result};
|
||||||
@@ -98,10 +98,9 @@ impl StaticKmsBackend {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl StaticKmsBackend {
|
#[async_trait]
|
||||||
/// Generate a fresh data key and wrap it in the standard KMS envelope,
|
impl KmsClient for StaticKmsBackend {
|
||||||
/// authenticated against the canonical encryption context.
|
async fn generate_data_key(&self, request: &GenerateKeyRequest, _context: Option<&OperationContext>) -> Result<DataKeyInfo> {
|
||||||
pub(crate) fn generate_data_key_envelope(&self, request: &GenerateKeyRequest) -> Result<DataKeyInfo> {
|
|
||||||
if request.master_key_id != self.key_id {
|
if request.master_key_id != self.key_id {
|
||||||
return Err(KmsError::key_not_found(&request.master_key_id));
|
return Err(KmsError::key_not_found(&request.master_key_id));
|
||||||
}
|
}
|
||||||
@@ -152,8 +151,7 @@ impl StaticKmsBackend {
|
|||||||
))
|
))
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Encrypt caller-provided plaintext into the standard KMS envelope.
|
async fn encrypt(&self, request: &EncryptRequest, _context: Option<&OperationContext>) -> Result<EncryptResponse> {
|
||||||
pub(crate) fn encrypt_to_envelope(&self, request: &EncryptRequest) -> Result<EncryptResponse> {
|
|
||||||
if request.key_id != self.key_id {
|
if request.key_id != self.key_id {
|
||||||
return Err(KmsError::key_not_found(&request.key_id));
|
return Err(KmsError::key_not_found(&request.key_id));
|
||||||
}
|
}
|
||||||
@@ -198,8 +196,7 @@ impl StaticKmsBackend {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Open a KMS envelope produced by this backend.
|
async fn decrypt(&self, request: &DecryptRequest, _context: Option<&OperationContext>) -> Result<Vec<u8>> {
|
||||||
pub(crate) fn decrypt_envelope(&self, request: &DecryptRequest) -> Result<Vec<u8>> {
|
|
||||||
let envelope: DataKeyEnvelope = serde_json::from_slice(&request.ciphertext)
|
let envelope: DataKeyEnvelope = serde_json::from_slice(&request.ciphertext)
|
||||||
.map_err(|error| KmsError::cryptographic_error("parse", format!("Failed to parse data key envelope: {error}")))?;
|
.map_err(|error| KmsError::cryptographic_error("parse", format!("Failed to parse data key envelope: {error}")))?;
|
||||||
if envelope.master_key_id != self.key_id {
|
if envelope.master_key_id != self.key_id {
|
||||||
@@ -238,8 +235,14 @@ impl StaticKmsBackend {
|
|||||||
Ok(plaintext)
|
Ok(plaintext)
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Describe the single configured key.
|
async fn create_key(&self, key_id: &str, _algorithm: &str, _context: Option<&OperationContext>) -> Result<MasterKeyInfo> {
|
||||||
pub(crate) fn configured_key_info(&self, key_id: &str) -> Result<KeyInfo> {
|
if key_id == self.key_id {
|
||||||
|
return Err(KmsError::key_already_exists(key_id));
|
||||||
|
}
|
||||||
|
Err(KmsError::invalid_operation("Static KMS is read-only: cannot create new keys"))
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn describe_key(&self, key_id: &str, _context: Option<&OperationContext>) -> Result<KeyInfo> {
|
||||||
if key_id != self.key_id {
|
if key_id != self.key_id {
|
||||||
return Err(KmsError::key_not_found(key_id));
|
return Err(KmsError::key_not_found(key_id));
|
||||||
}
|
}
|
||||||
@@ -260,8 +263,7 @@ impl StaticKmsBackend {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
/// List the single configured key, honouring the pagination marker.
|
async fn list_keys(&self, request: &ListKeysRequest, _context: Option<&OperationContext>) -> Result<ListKeysResponse> {
|
||||||
pub(crate) fn list_configured_key(&self, request: &ListKeysRequest) -> Result<ListKeysResponse> {
|
|
||||||
let key_info = KeyInfo {
|
let key_info = KeyInfo {
|
||||||
key_id: self.key_id.clone(),
|
key_id: self.key_id.clone(),
|
||||||
description: Some("Static single-key KMS backend".to_string()),
|
description: Some("Static single-key KMS backend".to_string()),
|
||||||
@@ -293,6 +295,57 @@ impl StaticKmsBackend {
|
|||||||
truncated: false,
|
truncated: false,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async fn enable_key(&self, key_id: &str, _context: Option<&OperationContext>) -> Result<()> {
|
||||||
|
if key_id != self.key_id {
|
||||||
|
return Err(KmsError::key_not_found(key_id));
|
||||||
|
}
|
||||||
|
// Static KMS key is always enabled
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn disable_key(&self, key_id: &str, _context: Option<&OperationContext>) -> Result<()> {
|
||||||
|
if key_id != self.key_id {
|
||||||
|
return Err(KmsError::key_not_found(key_id));
|
||||||
|
}
|
||||||
|
Err(KmsError::invalid_operation("Static KMS is read-only: cannot disable keys"))
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn schedule_key_deletion(
|
||||||
|
&self,
|
||||||
|
key_id: &str,
|
||||||
|
_pending_window_days: u32,
|
||||||
|
_context: Option<&OperationContext>,
|
||||||
|
) -> Result<()> {
|
||||||
|
if key_id != self.key_id {
|
||||||
|
return Err(KmsError::key_not_found(key_id));
|
||||||
|
}
|
||||||
|
Err(KmsError::invalid_operation("Static KMS is read-only: cannot schedule key deletion"))
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn cancel_key_deletion(&self, key_id: &str, _context: Option<&OperationContext>) -> Result<()> {
|
||||||
|
if key_id != self.key_id {
|
||||||
|
return Err(KmsError::key_not_found(key_id));
|
||||||
|
}
|
||||||
|
Err(KmsError::invalid_operation("Static KMS is read-only: cannot cancel key deletion"))
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn rotate_key(&self, key_id: &str, _context: Option<&OperationContext>) -> Result<MasterKeyInfo> {
|
||||||
|
if key_id != self.key_id {
|
||||||
|
return Err(KmsError::key_not_found(key_id));
|
||||||
|
}
|
||||||
|
Err(KmsError::invalid_operation("Static KMS is read-only: cannot rotate keys"))
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn health_check(&self) -> Result<()> {
|
||||||
|
// Static KMS is always healthy if it was successfully initialized
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
fn backend_info(&self) -> BackendInfo {
|
||||||
|
BackendInfo::new("static".to_string(), env!("CARGO_PKG_VERSION").to_string(), "local".to_string(), true)
|
||||||
|
.with_metadata("key_id".to_string(), self.key_id.clone())
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#[async_trait]
|
#[async_trait]
|
||||||
@@ -306,12 +359,12 @@ impl KmsBackend for StaticKmsBackend {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async fn encrypt(&self, request: EncryptRequest) -> Result<EncryptResponse> {
|
async fn encrypt(&self, request: EncryptRequest) -> Result<EncryptResponse> {
|
||||||
self.encrypt_to_envelope(&request)
|
<Self as KmsClient>::encrypt(self, &request, None).await
|
||||||
}
|
}
|
||||||
|
|
||||||
async fn decrypt(&self, request: DecryptRequest) -> Result<DecryptResponse> {
|
async fn decrypt(&self, request: DecryptRequest) -> Result<DecryptResponse> {
|
||||||
let key_id = self.key_id.clone();
|
let key_id = self.key_id.clone();
|
||||||
let plaintext = self.decrypt_envelope(&request)?;
|
let plaintext = <Self as KmsClient>::decrypt(self, &request, None).await?;
|
||||||
Ok(DecryptResponse {
|
Ok(DecryptResponse {
|
||||||
plaintext,
|
plaintext,
|
||||||
key_id,
|
key_id,
|
||||||
@@ -327,7 +380,7 @@ impl KmsBackend for StaticKmsBackend {
|
|||||||
encryption_context: request.encryption_context,
|
encryption_context: request.encryption_context,
|
||||||
grant_tokens: Vec::new(),
|
grant_tokens: Vec::new(),
|
||||||
};
|
};
|
||||||
let data_key = self.generate_data_key_envelope(&gen_req)?;
|
let data_key = <Self as KmsClient>::generate_data_key(self, &gen_req, None).await?;
|
||||||
|
|
||||||
let plaintext_key = data_key
|
let plaintext_key = data_key
|
||||||
.plaintext
|
.plaintext
|
||||||
@@ -342,7 +395,7 @@ impl KmsBackend for StaticKmsBackend {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async fn describe_key(&self, request: DescribeKeyRequest) -> Result<DescribeKeyResponse> {
|
async fn describe_key(&self, request: DescribeKeyRequest) -> Result<DescribeKeyResponse> {
|
||||||
let key_info = self.configured_key_info(&request.key_id)?;
|
let key_info = <Self as KmsClient>::describe_key(self, &request.key_id, None).await?;
|
||||||
let key_metadata = KeyMetadata {
|
let key_metadata = KeyMetadata {
|
||||||
key_id: key_info.key_id.clone(),
|
key_id: key_info.key_id.clone(),
|
||||||
key_state: if key_info.status == KeyStatus::Active {
|
key_state: if key_info.status == KeyStatus::Active {
|
||||||
@@ -362,7 +415,7 @@ impl KmsBackend for StaticKmsBackend {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async fn list_keys(&self, request: ListKeysRequest) -> Result<ListKeysResponse> {
|
async fn list_keys(&self, request: ListKeysRequest) -> Result<ListKeysResponse> {
|
||||||
self.list_configured_key(&request)
|
<Self as KmsClient>::list_keys(self, &request, None).await
|
||||||
}
|
}
|
||||||
|
|
||||||
async fn delete_key(&self, request: DeleteKeyRequest) -> Result<DeleteKeyResponse> {
|
async fn delete_key(&self, request: DeleteKeyRequest) -> Result<DeleteKeyResponse> {
|
||||||
@@ -393,7 +446,7 @@ impl KmsBackend for StaticKmsBackend {
|
|||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
mod tests {
|
mod tests {
|
||||||
use super::*;
|
use super::*;
|
||||||
use crate::backends::KmsBackend as KmsBackendTrait;
|
use crate::backends::{KmsBackend as KmsBackendTrait, KmsClient};
|
||||||
use crate::config::{BackendConfig, KmsBackend, StaticConfig};
|
use crate::config::{BackendConfig, KmsBackend, StaticConfig};
|
||||||
use crate::encryption::is_data_key_envelope;
|
use crate::encryption::is_data_key_envelope;
|
||||||
use base64::Engine as _;
|
use base64::Engine as _;
|
||||||
@@ -438,8 +491,8 @@ mod tests {
|
|||||||
// Generate data key
|
// Generate data key
|
||||||
let request = GenerateKeyRequest::new(key_id.clone(), "AES_256".to_string())
|
let request = GenerateKeyRequest::new(key_id.clone(), "AES_256".to_string())
|
||||||
.with_context("bucket".to_string(), "test-bucket".to_string());
|
.with_context("bucket".to_string(), "test-bucket".to_string());
|
||||||
let data_key = backend
|
let data_key = KmsClient::generate_data_key(&backend, &request, None)
|
||||||
.generate_data_key_envelope(&request)
|
.await
|
||||||
.expect("Failed to generate data key");
|
.expect("Failed to generate data key");
|
||||||
|
|
||||||
assert_eq!(data_key.key_id, key_id);
|
assert_eq!(data_key.key_id, key_id);
|
||||||
@@ -456,7 +509,9 @@ mod tests {
|
|||||||
// Decrypt the data key
|
// Decrypt the data key
|
||||||
let decrypt_request =
|
let decrypt_request =
|
||||||
DecryptRequest::new(data_key.ciphertext.clone()).with_context("bucket".to_string(), "test-bucket".to_string());
|
DecryptRequest::new(data_key.ciphertext.clone()).with_context("bucket".to_string(), "test-bucket".to_string());
|
||||||
let decrypted = backend.decrypt_envelope(&decrypt_request).expect("Failed to decrypt");
|
let decrypted = KmsClient::decrypt(&backend, &decrypt_request, None)
|
||||||
|
.await
|
||||||
|
.expect("Failed to decrypt");
|
||||||
|
|
||||||
assert_eq!(decrypted.as_slice(), data_key.plaintext.as_deref().expect("plaintext should exist"));
|
assert_eq!(decrypted.as_slice(), data_key.plaintext.as_deref().expect("plaintext should exist"));
|
||||||
}
|
}
|
||||||
@@ -468,8 +523,8 @@ mod tests {
|
|||||||
.with_context("bucket".to_string(), "source-bucket".to_string())
|
.with_context("bucket".to_string(), "source-bucket".to_string())
|
||||||
.with_context("object".to_string(), "source-object".to_string());
|
.with_context("object".to_string(), "source-object".to_string());
|
||||||
|
|
||||||
let data_key = backend
|
let data_key = KmsClient::generate_data_key(&backend, &request, None)
|
||||||
.generate_data_key_envelope(&request)
|
.await
|
||||||
.expect("generate static KMS data key");
|
.expect("generate static KMS data key");
|
||||||
|
|
||||||
assert!(
|
assert!(
|
||||||
@@ -513,8 +568,8 @@ mod tests {
|
|||||||
let (backend, key_id, _key) = create_test_backend().await;
|
let (backend, key_id, _key) = create_test_backend().await;
|
||||||
let request = GenerateKeyRequest::new(key_id, "AES_256".to_string())
|
let request = GenerateKeyRequest::new(key_id, "AES_256".to_string())
|
||||||
.with_context("bucket".to_string(), "source-bucket".to_string());
|
.with_context("bucket".to_string(), "source-bucket".to_string());
|
||||||
let generated = backend
|
let generated = KmsClient::generate_data_key(&backend, &request, None)
|
||||||
.generate_data_key_envelope(&request)
|
.await
|
||||||
.expect("generate context-bound data key");
|
.expect("generate context-bound data key");
|
||||||
let mut envelope: DataKeyEnvelope = serde_json::from_slice(&generated.ciphertext).expect("parse static KMS envelope");
|
let mut envelope: DataKeyEnvelope = serde_json::from_slice(&generated.ciphertext).expect("parse static KMS envelope");
|
||||||
envelope
|
envelope
|
||||||
@@ -523,8 +578,8 @@ mod tests {
|
|||||||
let decrypt_request = DecryptRequest::new(serde_json::to_vec(&envelope).expect("serialize tampered envelope"))
|
let decrypt_request = DecryptRequest::new(serde_json::to_vec(&envelope).expect("serialize tampered envelope"))
|
||||||
.with_context("bucket".to_string(), "different-bucket".to_string());
|
.with_context("bucket".to_string(), "different-bucket".to_string());
|
||||||
|
|
||||||
let error = backend
|
let error = KmsClient::decrypt(&backend, &decrypt_request, None)
|
||||||
.decrypt_envelope(&decrypt_request)
|
.await
|
||||||
.expect_err("tampering with authenticated envelope context must fail");
|
.expect_err("tampering with authenticated envelope context must fail");
|
||||||
|
|
||||||
assert!(matches!(error, KmsError::CryptographicError { .. }));
|
assert!(matches!(error, KmsError::CryptographicError { .. }));
|
||||||
@@ -535,7 +590,7 @@ mod tests {
|
|||||||
let (backend, _key_id, _key) = create_test_backend().await;
|
let (backend, _key_id, _key) = create_test_backend().await;
|
||||||
|
|
||||||
let request = GenerateKeyRequest::new("wrong-key-id".to_string(), "AES_256".to_string());
|
let request = GenerateKeyRequest::new("wrong-key-id".to_string(), "AES_256".to_string());
|
||||||
let result = backend.generate_data_key_envelope(&request);
|
let result = KmsClient::generate_data_key(&backend, &request, None).await;
|
||||||
assert!(result.is_err());
|
assert!(result.is_err());
|
||||||
assert!(result.expect_err("should be Err").to_string().contains("wrong-key-id"));
|
assert!(result.expect_err("should be Err").to_string().contains("wrong-key-id"));
|
||||||
}
|
}
|
||||||
@@ -547,7 +602,7 @@ mod tests {
|
|||||||
// Ciphertext too short
|
// Ciphertext too short
|
||||||
let short = vec![0u8; 10];
|
let short = vec![0u8; 10];
|
||||||
let request = DecryptRequest::new(short);
|
let request = DecryptRequest::new(short);
|
||||||
let result = backend.decrypt_envelope(&request);
|
let result = KmsClient::decrypt(&backend, &request, None).await;
|
||||||
assert!(result.is_err());
|
assert!(result.is_err());
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -557,7 +612,9 @@ mod tests {
|
|||||||
|
|
||||||
// Generate a valid ciphertext first
|
// Generate a valid ciphertext first
|
||||||
let gen_request = GenerateKeyRequest::new(key_id, "AES_256".to_string());
|
let gen_request = GenerateKeyRequest::new(key_id, "AES_256".to_string());
|
||||||
let data_key = backend.generate_data_key_envelope(&gen_request).expect("generate");
|
let data_key = KmsClient::generate_data_key(&backend, &gen_request, None)
|
||||||
|
.await
|
||||||
|
.expect("generate");
|
||||||
|
|
||||||
// Tamper with the ciphertext (flip a bit in the encrypted portion)
|
// Tamper with the ciphertext (flip a bit in the encrypted portion)
|
||||||
let mut tampered = data_key.ciphertext.clone();
|
let mut tampered = data_key.ciphertext.clone();
|
||||||
@@ -566,7 +623,7 @@ mod tests {
|
|||||||
}
|
}
|
||||||
|
|
||||||
let request = DecryptRequest::new(tampered);
|
let request = DecryptRequest::new(tampered);
|
||||||
let result = backend.decrypt_envelope(&request);
|
let result = KmsClient::decrypt(&backend, &request, None).await;
|
||||||
assert!(result.is_err());
|
assert!(result.is_err());
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -575,14 +632,7 @@ mod tests {
|
|||||||
let (backend, key_id, _key) = create_test_backend().await;
|
let (backend, key_id, _key) = create_test_backend().await;
|
||||||
|
|
||||||
// Creating the pre-configured key should return KeyAlreadyExists
|
// Creating the pre-configured key should return KeyAlreadyExists
|
||||||
let result = KmsBackendTrait::create_key(
|
let result = KmsClient::create_key(&backend, &key_id, "AES_256", None).await;
|
||||||
&backend,
|
|
||||||
CreateKeyRequest {
|
|
||||||
key_name: Some(key_id.clone()),
|
|
||||||
..Default::default()
|
|
||||||
},
|
|
||||||
)
|
|
||||||
.await;
|
|
||||||
assert!(result.is_err());
|
assert!(result.is_err());
|
||||||
assert!(result.expect_err("should be Err").to_string().contains("already exists"));
|
assert!(result.expect_err("should be Err").to_string().contains("already exists"));
|
||||||
}
|
}
|
||||||
@@ -592,14 +642,7 @@ mod tests {
|
|||||||
let (backend, _key_id, _key) = create_test_backend().await;
|
let (backend, _key_id, _key) = create_test_backend().await;
|
||||||
|
|
||||||
// Creating any other key should return invalid operation (read-only)
|
// Creating any other key should return invalid operation (read-only)
|
||||||
let result = KmsBackendTrait::create_key(
|
let result = KmsClient::create_key(&backend, "other-key", "AES_256", None).await;
|
||||||
&backend,
|
|
||||||
CreateKeyRequest {
|
|
||||||
key_name: Some("other-key".to_string()),
|
|
||||||
..Default::default()
|
|
||||||
},
|
|
||||||
)
|
|
||||||
.await;
|
|
||||||
assert!(result.is_err());
|
assert!(result.is_err());
|
||||||
let err_msg = result.expect_err("should be Err").to_string();
|
let err_msg = result.expect_err("should be Err").to_string();
|
||||||
assert!(err_msg.contains("read-only") || err_msg.contains("cannot create"));
|
assert!(err_msg.contains("read-only") || err_msg.contains("cannot create"));
|
||||||
@@ -609,13 +652,15 @@ mod tests {
|
|||||||
async fn test_describe_key() {
|
async fn test_describe_key() {
|
||||||
let (backend, key_id, _key) = create_test_backend().await;
|
let (backend, key_id, _key) = create_test_backend().await;
|
||||||
|
|
||||||
let key_info = backend.configured_key_info(&key_id).expect("describe_key should succeed");
|
let key_info = KmsClient::describe_key(&backend, &key_id, None)
|
||||||
|
.await
|
||||||
|
.expect("describe_key should succeed");
|
||||||
assert_eq!(key_info.key_id, key_id);
|
assert_eq!(key_info.key_id, key_id);
|
||||||
assert_eq!(key_info.status, KeyStatus::Active);
|
assert_eq!(key_info.status, KeyStatus::Active);
|
||||||
assert_eq!(key_info.algorithm, "AES_256");
|
assert_eq!(key_info.algorithm, "AES_256");
|
||||||
|
|
||||||
// Wrong key ID
|
// Wrong key ID
|
||||||
let result = backend.configured_key_info("nonexistent");
|
let result = KmsClient::describe_key(&backend, "nonexistent", None).await;
|
||||||
assert!(result.is_err());
|
assert!(result.is_err());
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -623,8 +668,8 @@ mod tests {
|
|||||||
async fn test_list_keys() {
|
async fn test_list_keys() {
|
||||||
let (backend, key_id, _key) = create_test_backend().await;
|
let (backend, key_id, _key) = create_test_backend().await;
|
||||||
|
|
||||||
let response = backend
|
let response = KmsClient::list_keys(&backend, &ListKeysRequest::default(), None)
|
||||||
.list_configured_key(&ListKeysRequest::default())
|
.await
|
||||||
.expect("list_keys should succeed");
|
.expect("list_keys should succeed");
|
||||||
assert_eq!(response.keys.len(), 1);
|
assert_eq!(response.keys.len(), 1);
|
||||||
assert_eq!(response.keys[0].key_id, key_id);
|
assert_eq!(response.keys[0].key_id, key_id);
|
||||||
@@ -632,45 +677,61 @@ mod tests {
|
|||||||
}
|
}
|
||||||
|
|
||||||
#[tokio::test]
|
#[tokio::test]
|
||||||
async fn lifecycle_mutations_are_unsupported_at_the_product_surface() {
|
async fn test_disable_key_returns_error() {
|
||||||
let (backend, key_id, _key) = create_test_backend().await;
|
let (backend, key_id, _key) = create_test_backend().await;
|
||||||
|
|
||||||
// The static backend advertises no enable/disable or rotation
|
let result = KmsClient::disable_key(&backend, &key_id, None).await;
|
||||||
// capability, so the shared KmsBackend defaults reject all three.
|
assert!(result.is_err());
|
||||||
for result in [
|
assert!(result.expect_err("should be Err").to_string().contains("read-only"));
|
||||||
KmsBackendTrait::enable_key(&backend, &key_id).await,
|
}
|
||||||
KmsBackendTrait::disable_key(&backend, &key_id).await,
|
|
||||||
KmsBackendTrait::rotate_key(&backend, &key_id).await,
|
#[tokio::test]
|
||||||
] {
|
async fn test_enable_key_is_noop() {
|
||||||
let error = result.expect_err("static lifecycle mutations must be rejected");
|
let (backend, key_id, _key) = create_test_backend().await;
|
||||||
assert!(matches!(error, KmsError::UnsupportedCapability { .. }), "got {error:?}");
|
|
||||||
}
|
// Enable should succeed (no-op for static KMS)
|
||||||
|
KmsClient::enable_key(&backend, &key_id, None)
|
||||||
|
.await
|
||||||
|
.expect("enable_key should be no-op");
|
||||||
|
|
||||||
|
// Wrong key should still fail
|
||||||
|
let result = KmsClient::enable_key(&backend, "wrong", None).await;
|
||||||
|
assert!(result.is_err());
|
||||||
}
|
}
|
||||||
|
|
||||||
#[tokio::test]
|
#[tokio::test]
|
||||||
async fn test_delete_key_returns_error() {
|
async fn test_delete_key_returns_error() {
|
||||||
let (backend, key_id, _key) = create_test_backend().await;
|
let (backend, key_id, _key) = create_test_backend().await;
|
||||||
|
|
||||||
let result = KmsBackendTrait::delete_key(
|
let result = KmsClient::schedule_key_deletion(&backend, &key_id, 7, None).await;
|
||||||
&backend,
|
|
||||||
DeleteKeyRequest {
|
|
||||||
key_id: key_id.clone(),
|
|
||||||
pending_window_in_days: Some(7),
|
|
||||||
force_immediate: None,
|
|
||||||
},
|
|
||||||
)
|
|
||||||
.await;
|
|
||||||
assert!(result.is_err());
|
assert!(result.is_err());
|
||||||
assert!(result.expect_err("should be Err").to_string().contains("read-only"));
|
assert!(result.expect_err("should be Err").to_string().contains("read-only"));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[tokio::test]
|
||||||
|
async fn test_rotate_key_returns_error() {
|
||||||
|
let (backend, key_id, _key) = create_test_backend().await;
|
||||||
|
|
||||||
|
let result = KmsClient::rotate_key(&backend, &key_id, None).await;
|
||||||
|
assert!(result.is_err());
|
||||||
|
}
|
||||||
|
|
||||||
#[tokio::test]
|
#[tokio::test]
|
||||||
async fn test_health_check() {
|
async fn test_health_check() {
|
||||||
let (backend, _key_id, _key) = create_test_backend().await;
|
let (backend, _key_id, _key) = create_test_backend().await;
|
||||||
|
|
||||||
KmsBackendTrait::health_check(&backend)
|
KmsClient::health_check(&backend).await.expect("health_check should succeed");
|
||||||
.await
|
}
|
||||||
.expect("health_check should succeed");
|
|
||||||
|
#[tokio::test]
|
||||||
|
async fn test_backend_info() {
|
||||||
|
let (backend, key_id, _key) = create_test_backend().await;
|
||||||
|
|
||||||
|
let info = KmsClient::backend_info(&backend);
|
||||||
|
assert_eq!(info.backend_type, "static");
|
||||||
|
assert_eq!(info.endpoint, "local");
|
||||||
|
assert!(info.healthy);
|
||||||
|
assert_eq!(info.metadata.get("key_id"), Some(&key_id));
|
||||||
}
|
}
|
||||||
|
|
||||||
#[tokio::test]
|
#[tokio::test]
|
||||||
@@ -679,13 +740,17 @@ mod tests {
|
|||||||
|
|
||||||
let plaintext = b"Hello, static KMS world!";
|
let plaintext = b"Hello, static KMS world!";
|
||||||
let enc_request = EncryptRequest::new(key_id.clone(), plaintext.to_vec());
|
let enc_request = EncryptRequest::new(key_id.clone(), plaintext.to_vec());
|
||||||
let enc_response = backend.encrypt_to_envelope(&enc_request).expect("encrypt should succeed");
|
let enc_response = KmsClient::encrypt(&backend, &enc_request, None)
|
||||||
|
.await
|
||||||
|
.expect("encrypt should succeed");
|
||||||
|
|
||||||
assert_eq!(enc_response.key_id, key_id);
|
assert_eq!(enc_response.key_id, key_id);
|
||||||
assert!(!enc_response.ciphertext.is_empty());
|
assert!(!enc_response.ciphertext.is_empty());
|
||||||
|
|
||||||
let dec_request = DecryptRequest::new(enc_response.ciphertext);
|
let dec_request = DecryptRequest::new(enc_response.ciphertext);
|
||||||
let decrypted = backend.decrypt_envelope(&dec_request).expect("decrypt should succeed");
|
let decrypted = KmsClient::decrypt(&backend, &dec_request, None)
|
||||||
|
.await
|
||||||
|
.expect("decrypt should succeed");
|
||||||
|
|
||||||
assert_eq!(decrypted, plaintext);
|
assert_eq!(decrypted, plaintext);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -19,7 +19,8 @@ use crate::backends::vault_credentials::{
|
|||||||
token_source_for,
|
token_source_for,
|
||||||
};
|
};
|
||||||
use crate::backends::{
|
use crate::backends::{
|
||||||
BackendCapabilities, ExpiredKeyRemoval, KmsBackend, StateGatedOperation, ensure_key_state_permits, ensure_key_status_permits,
|
BackendCapabilities, BackendInfo, ExpiredKeyRemoval, KmsBackend, KmsClient, StateGatedOperation, ensure_key_state_permits,
|
||||||
|
ensure_key_status_permits,
|
||||||
};
|
};
|
||||||
use crate::config::{KmsConfig, VaultConfig};
|
use crate::config::{KmsConfig, VaultConfig};
|
||||||
use crate::encryption::{AesDekCrypto, DataKeyEnvelope, DekCrypto, generate_key_material};
|
use crate::encryption::{AesDekCrypto, DataKeyEnvelope, DekCrypto, generate_key_material};
|
||||||
@@ -41,6 +42,7 @@ use vaultrs::{api::kv2::requests::SetSecretRequestOptions, error::ClientError, k
|
|||||||
/// Vault KMS client implementation
|
/// Vault KMS client implementation
|
||||||
pub struct VaultKmsClient {
|
pub struct VaultKmsClient {
|
||||||
credentials: Arc<VaultCredentialProvider>,
|
credentials: Arc<VaultCredentialProvider>,
|
||||||
|
config: VaultConfig,
|
||||||
/// Mount path for the KV engine (typically "kv" or "secret")
|
/// Mount path for the KV engine (typically "kv" or "secret")
|
||||||
kv_mount: String,
|
kv_mount: String,
|
||||||
/// Path prefix for storing keys
|
/// Path prefix for storing keys
|
||||||
@@ -198,6 +200,7 @@ impl VaultKmsClient {
|
|||||||
credentials,
|
credentials,
|
||||||
kv_mount: config.kv_mount.clone(),
|
kv_mount: config.kv_mount.clone(),
|
||||||
key_path_prefix: config.key_path_prefix.clone(),
|
key_path_prefix: config.key_path_prefix.clone(),
|
||||||
|
config,
|
||||||
dek_crypto: AesDekCrypto::new(),
|
dek_crypto: AesDekCrypto::new(),
|
||||||
retry: RetryPolicy::from_config(kms_config),
|
retry: RetryPolicy::from_config(kms_config),
|
||||||
cancel: CancellationToken::new(),
|
cancel: CancellationToken::new(),
|
||||||
@@ -251,6 +254,13 @@ impl VaultKmsClient {
|
|||||||
Ok(general_purpose::STANDARD.encode(key_material))
|
Ok(general_purpose::STANDARD.encode(key_material))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Decode key material from KV2 storage (plain Base64, see `encrypt_key_material`).
|
||||||
|
async fn decrypt_key_material(&self, encrypted_material: &str) -> Result<Vec<u8>> {
|
||||||
|
general_purpose::STANDARD
|
||||||
|
.decode(encrypted_material)
|
||||||
|
.map_err(|e| KmsError::cryptographic_error("decrypt", e.to_string()))
|
||||||
|
}
|
||||||
|
|
||||||
/// Read the immutable material record of one key version.
|
/// Read the immutable material record of one key version.
|
||||||
///
|
///
|
||||||
/// A missing record fails closed with [`KmsError::KeyVersionNotFound`]; falling
|
/// A missing record fails closed with [`KmsError::KeyVersionNotFound`]; falling
|
||||||
@@ -579,12 +589,9 @@ impl VaultKmsClient {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl VaultKmsClient {
|
#[async_trait]
|
||||||
pub(crate) async fn generate_data_key(
|
impl KmsClient for VaultKmsClient {
|
||||||
&self,
|
async fn generate_data_key(&self, request: &GenerateKeyRequest, _context: Option<&OperationContext>) -> Result<DataKeyInfo> {
|
||||||
request: &GenerateKeyRequest,
|
|
||||||
_context: Option<&OperationContext>,
|
|
||||||
) -> Result<DataKeyInfo> {
|
|
||||||
debug!("Generating data key for master key: {}", request.master_key_id);
|
debug!("Generating data key for master key: {}", request.master_key_id);
|
||||||
|
|
||||||
let key_data = self.get_key_data(&request.master_key_id).await?;
|
let key_data = self.get_key_data(&request.master_key_id).await?;
|
||||||
@@ -625,32 +632,20 @@ impl VaultKmsClient {
|
|||||||
Ok(data_key)
|
Ok(data_key)
|
||||||
}
|
}
|
||||||
|
|
||||||
pub(crate) async fn encrypt(&self, request: &EncryptRequest, _context: Option<&OperationContext>) -> Result<EncryptResponse> {
|
async fn encrypt(&self, request: &EncryptRequest, _context: Option<&OperationContext>) -> Result<EncryptResponse> {
|
||||||
debug!("Encrypting data with key: {}", request.key_id);
|
debug!("Encrypting data with key: {}", request.key_id);
|
||||||
|
|
||||||
// Single read of the key record: the material we wrap with and the
|
// Get the master key and verify its state allows encryption
|
||||||
// version stamped into the envelope must come from the same snapshot
|
|
||||||
// (see generate_data_key).
|
|
||||||
let key_data = self.get_key_data(&request.key_id).await?;
|
let key_data = self.get_key_data(&request.key_id).await?;
|
||||||
ensure_key_status_permits(&request.key_id, &key_data.status, StateGatedOperation::Encrypt)?;
|
ensure_key_status_permits(&request.key_id, &key_data.status, StateGatedOperation::Encrypt)?;
|
||||||
let key_material = decode_stored_key_material(&request.key_id, &key_data.encrypted_key_material)
|
let key_material = self.decrypt_key_material(&key_data.encrypted_key_material).await?;
|
||||||
.inspect_err(|error| warn!(key_id = %request.key_id, %error, "Vault KMS key material failed validation"))?;
|
|
||||||
let (encrypted_key, nonce) = self.dek_crypto.encrypt(&key_material, &request.plaintext).await?;
|
|
||||||
|
|
||||||
// Wrap the ciphertext in the same authenticated envelope that
|
// For simplicity, we'll use a basic encryption approach
|
||||||
// generate_data_key emits, so decrypt() round-trips it and resolves
|
// In practice, you'd use proper AEAD encryption
|
||||||
// the wrapping master key version after rotations.
|
let mut ciphertext = request.plaintext.clone();
|
||||||
let envelope = DataKeyEnvelope {
|
for (i, byte) in ciphertext.iter_mut().enumerate() {
|
||||||
key_id: uuid::Uuid::new_v4().to_string(),
|
*byte ^= key_material[i % key_material.len()];
|
||||||
master_key_id: request.key_id.clone(),
|
}
|
||||||
key_spec: "AES_256".to_string(),
|
|
||||||
encrypted_key,
|
|
||||||
nonce,
|
|
||||||
encryption_context: request.encryption_context.clone(),
|
|
||||||
created_at: Zoned::now(),
|
|
||||||
master_key_version: Some(key_data.version),
|
|
||||||
};
|
|
||||||
let ciphertext = serde_json::to_vec(&envelope)?;
|
|
||||||
|
|
||||||
Ok(EncryptResponse {
|
Ok(EncryptResponse {
|
||||||
ciphertext,
|
ciphertext,
|
||||||
@@ -660,7 +655,7 @@ impl VaultKmsClient {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
pub(crate) async fn decrypt(&self, request: &DecryptRequest, _context: Option<&OperationContext>) -> Result<Vec<u8>> {
|
async fn decrypt(&self, request: &DecryptRequest, _context: Option<&OperationContext>) -> Result<Vec<u8>> {
|
||||||
debug!("Decrypting data");
|
debug!("Decrypting data");
|
||||||
|
|
||||||
// Parse the data key envelope from ciphertext
|
// Parse the data key envelope from ciphertext
|
||||||
@@ -702,12 +697,7 @@ impl VaultKmsClient {
|
|||||||
Ok(plaintext)
|
Ok(plaintext)
|
||||||
}
|
}
|
||||||
|
|
||||||
pub(crate) async fn create_key(
|
async fn create_key(&self, key_id: &str, algorithm: &str, _context: Option<&OperationContext>) -> Result<MasterKeyInfo> {
|
||||||
&self,
|
|
||||||
key_id: &str,
|
|
||||||
algorithm: &str,
|
|
||||||
_context: Option<&OperationContext>,
|
|
||||||
) -> Result<MasterKeyInfo> {
|
|
||||||
debug!("Creating master key: {} with algorithm: {}", key_id, algorithm);
|
debug!("Creating master key: {} with algorithm: {}", key_id, algorithm);
|
||||||
|
|
||||||
// Existence pre-check with read-confirm recovery: a create whose
|
// Existence pre-check with read-confirm recovery: a create whose
|
||||||
@@ -789,7 +779,7 @@ impl VaultKmsClient {
|
|||||||
Ok(master_key)
|
Ok(master_key)
|
||||||
}
|
}
|
||||||
|
|
||||||
pub(crate) async fn describe_key(&self, key_id: &str, _context: Option<&OperationContext>) -> Result<KeyInfo> {
|
async fn describe_key(&self, key_id: &str, _context: Option<&OperationContext>) -> Result<KeyInfo> {
|
||||||
debug!("Describing key: {}", key_id);
|
debug!("Describing key: {}", key_id);
|
||||||
|
|
||||||
let key_data = self.get_key_data(key_id).await?;
|
let key_data = self.get_key_data(key_id).await?;
|
||||||
@@ -809,11 +799,7 @@ impl VaultKmsClient {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
pub(crate) async fn list_keys(
|
async fn list_keys(&self, request: &ListKeysRequest, _context: Option<&OperationContext>) -> Result<ListKeysResponse> {
|
||||||
&self,
|
|
||||||
request: &ListKeysRequest,
|
|
||||||
_context: Option<&OperationContext>,
|
|
||||||
) -> Result<ListKeysResponse> {
|
|
||||||
debug!("Listing keys with limit: {:?}", request.limit);
|
debug!("Listing keys with limit: {:?}", request.limit);
|
||||||
|
|
||||||
let all_keys = self.list_vault_keys().await?;
|
let all_keys = self.list_vault_keys().await?;
|
||||||
@@ -850,7 +836,7 @@ impl VaultKmsClient {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
pub(crate) async fn enable_key(&self, key_id: &str, _context: Option<&OperationContext>) -> Result<()> {
|
async fn enable_key(&self, key_id: &str, _context: Option<&OperationContext>) -> Result<()> {
|
||||||
debug!("Enabling key: {}", key_id);
|
debug!("Enabling key: {}", key_id);
|
||||||
|
|
||||||
let mut key_data = self.get_key_data(key_id).await?;
|
let mut key_data = self.get_key_data(key_id).await?;
|
||||||
@@ -862,7 +848,7 @@ impl VaultKmsClient {
|
|||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
pub(crate) async fn disable_key(&self, key_id: &str, _context: Option<&OperationContext>) -> Result<()> {
|
async fn disable_key(&self, key_id: &str, _context: Option<&OperationContext>) -> Result<()> {
|
||||||
debug!("Disabling key: {}", key_id);
|
debug!("Disabling key: {}", key_id);
|
||||||
|
|
||||||
let mut key_data = self.get_key_data(key_id).await?;
|
let mut key_data = self.get_key_data(key_id).await?;
|
||||||
@@ -874,6 +860,39 @@ impl VaultKmsClient {
|
|||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async fn schedule_key_deletion(
|
||||||
|
&self,
|
||||||
|
key_id: &str,
|
||||||
|
pending_window_days: u32,
|
||||||
|
_context: Option<&OperationContext>,
|
||||||
|
) -> Result<()> {
|
||||||
|
debug!("Scheduling key deletion: {}", key_id);
|
||||||
|
|
||||||
|
let mut key_data = self.get_key_data(key_id).await?;
|
||||||
|
ensure_key_status_permits(key_id, &key_data.status, StateGatedOperation::ScheduleDeletion)?;
|
||||||
|
key_data.status = KeyStatus::PendingDeletion;
|
||||||
|
key_data.deletion_date = Some(Zoned::now() + Duration::from_secs(pending_window_days as u64 * 86400));
|
||||||
|
self.store_key_data(key_id, &key_data).await?;
|
||||||
|
|
||||||
|
debug!(key_id, "Vault KMS key deletion scheduled");
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn cancel_key_deletion(&self, key_id: &str, _context: Option<&OperationContext>) -> Result<()> {
|
||||||
|
debug!("Canceling key deletion: {}", key_id);
|
||||||
|
|
||||||
|
let mut key_data = self.get_key_data(key_id).await?;
|
||||||
|
if key_data.status != KeyStatus::PendingDeletion {
|
||||||
|
return Err(KmsError::invalid_key_state(format!("Key {key_id} is not pending deletion")));
|
||||||
|
}
|
||||||
|
key_data.status = KeyStatus::Active;
|
||||||
|
key_data.deletion_date = None;
|
||||||
|
self.store_key_data(key_id, &key_data).await?;
|
||||||
|
|
||||||
|
debug!(key_id, "Vault KMS key deletion canceled");
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
/// Rotate the master key while keeping every historical version decryptable.
|
/// Rotate the master key while keeping every historical version decryptable.
|
||||||
///
|
///
|
||||||
/// Commit protocol (all writes check-and-set, in this order):
|
/// Commit protocol (all writes check-and-set, in this order):
|
||||||
@@ -889,11 +908,10 @@ impl VaultKmsClient {
|
|||||||
/// or interrupted rotation never exposes half-committed material. Concurrent
|
/// or interrupted rotation never exposes half-committed material. Concurrent
|
||||||
/// rotations are serialized by the check-and-set writes: at most one caller
|
/// rotations are serialized by the check-and-set writes: at most one caller
|
||||||
/// commits each version and the losers fail without side effects on current.
|
/// commits each version and the losers fail without side effects on current.
|
||||||
pub(crate) async fn rotate_key(&self, key_id: &str, _context: Option<&OperationContext>) -> Result<MasterKeyInfo> {
|
async fn rotate_key(&self, key_id: &str, _context: Option<&OperationContext>) -> Result<MasterKeyInfo> {
|
||||||
debug!("Rotating master key: {}", key_id);
|
debug!("Rotating master key: {}", key_id);
|
||||||
|
|
||||||
let (mut cas, mut key_data) = self.get_key_data_versioned(key_id).await?;
|
let (mut cas, mut key_data) = self.get_key_data_versioned(key_id).await?;
|
||||||
ensure_key_status_permits(key_id, &key_data.status, StateGatedOperation::Rotate)?;
|
|
||||||
|
|
||||||
// The material about to be frozen must be decodable: freezing poisoned
|
// The material about to be frozen must be decodable: freezing poisoned
|
||||||
// material would give legacy envelopes a permanently broken baseline. This
|
// material would give legacy envelopes a permanently broken baseline. This
|
||||||
@@ -974,7 +992,7 @@ impl VaultKmsClient {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
pub(crate) async fn health_check(&self) -> Result<()> {
|
async fn health_check(&self) -> Result<()> {
|
||||||
debug!("Performing Vault health check");
|
debug!("Performing Vault health check");
|
||||||
|
|
||||||
// Use list_vault_keys but handle the case where no keys exist (which is normal)
|
// Use list_vault_keys but handle the case where no keys exist (which is normal)
|
||||||
@@ -996,6 +1014,15 @@ impl VaultKmsClient {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn backend_info(&self) -> BackendInfo {
|
||||||
|
BackendInfo::new("vault-kv2".to_string(), "0.1.0".to_string(), self.config.address.clone(), true)
|
||||||
|
.with_metadata("kv_mount".to_string(), self.kv_mount.clone())
|
||||||
|
.with_metadata("key_prefix".to_string(), self.key_path_prefix.clone())
|
||||||
|
// Master key material is protected only by Vault ACLs and KV2 at-rest
|
||||||
|
// encryption; there is no additional cryptographic wrapping.
|
||||||
|
.with_metadata("at_rest_protection".to_string(), "vault-kv2-acl".to_string())
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// VaultKmsBackend wraps VaultKmsClient and implements the KmsBackend trait
|
/// VaultKmsBackend wraps VaultKmsClient and implements the KmsBackend trait
|
||||||
@@ -1004,6 +1031,12 @@ pub struct VaultKmsBackend {
|
|||||||
}
|
}
|
||||||
|
|
||||||
impl VaultKmsBackend {
|
impl VaultKmsBackend {
|
||||||
|
/// Lifecycle driver for the shared state-machine contract tests.
|
||||||
|
#[cfg(test)]
|
||||||
|
pub(crate) fn lifecycle_client(&self) -> &VaultKmsClient {
|
||||||
|
&self.client
|
||||||
|
}
|
||||||
|
|
||||||
/// Create a new VaultKmsBackend
|
/// Create a new VaultKmsBackend
|
||||||
pub async fn new(config: KmsConfig) -> Result<Self> {
|
pub async fn new(config: KmsConfig) -> Result<Self> {
|
||||||
config.validate()?;
|
config.validate()?;
|
||||||
@@ -1263,32 +1296,17 @@ impl KmsBackend for VaultKmsBackend {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
async fn enable_key(&self, key_id: &str) -> Result<()> {
|
|
||||||
self.client.enable_key(key_id, None).await
|
|
||||||
}
|
|
||||||
|
|
||||||
async fn disable_key(&self, key_id: &str) -> Result<()> {
|
|
||||||
self.client.disable_key(key_id, None).await
|
|
||||||
}
|
|
||||||
|
|
||||||
async fn rotate_key(&self, key_id: &str) -> Result<()> {
|
|
||||||
self.client.rotate_key(key_id, None).await.map(|_| ())
|
|
||||||
}
|
|
||||||
|
|
||||||
async fn health_check(&self) -> Result<bool> {
|
async fn health_check(&self) -> Result<bool> {
|
||||||
self.client.health_check().await.map(|_| true)
|
self.client.health_check().await.map(|_| true)
|
||||||
}
|
}
|
||||||
|
|
||||||
fn capabilities(&self) -> BackendCapabilities {
|
fn capabilities(&self) -> BackendCapabilities {
|
||||||
// Rotation freezes the outgoing material as an immutable version
|
// Rotation is unadvertised: the KV2 backend cannot rotate without
|
||||||
// record before switching the current pointer, and envelopes resolve
|
// replacing key material in place, and no historical versions are
|
||||||
// their wrapping version on decrypt, so every historical version
|
// retained, so versioning is unsupported as well.
|
||||||
// stays decryptable after a rotation.
|
|
||||||
BackendCapabilities::minimal()
|
BackendCapabilities::minimal()
|
||||||
.with_rotate(true)
|
|
||||||
.with_enable_disable(true)
|
.with_enable_disable(true)
|
||||||
.with_schedule_deletion(true)
|
.with_schedule_deletion(true)
|
||||||
.with_versioning(true)
|
|
||||||
.with_physical_delete(true)
|
.with_physical_delete(true)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1702,6 +1720,19 @@ mod tests {
|
|||||||
assert!(!is_cas_conflict(¬_found));
|
assert!(!is_cas_conflict(¬_found));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[tokio::test]
|
||||||
|
async fn test_vault_kv2_backend_info_reports_at_rest_protection() {
|
||||||
|
let client = VaultKmsClient::new(integration_vault_config(), &KmsConfig::default())
|
||||||
|
.await
|
||||||
|
.expect("client");
|
||||||
|
|
||||||
|
let info = client.backend_info();
|
||||||
|
assert_eq!(info.backend_type, "vault-kv2");
|
||||||
|
assert_eq!(info.metadata.get("at_rest_protection").map(String::as_str), Some("vault-kv2-acl"));
|
||||||
|
// The KV2 backend must not present itself as Transit-backed.
|
||||||
|
assert!(!format!("{info:?}").contains("Transit"));
|
||||||
|
}
|
||||||
|
|
||||||
fn integration_generate_request(key_id: &str) -> GenerateKeyRequest {
|
fn integration_generate_request(key_id: &str) -> GenerateKeyRequest {
|
||||||
GenerateKeyRequest {
|
GenerateKeyRequest {
|
||||||
master_key_id: key_id.to_string(),
|
master_key_id: key_id.to_string(),
|
||||||
@@ -2050,150 +2081,4 @@ mod tests {
|
|||||||
let legacy: VaultKeyData = serde_json::from_value(value).expect("legacy record must deserialize");
|
let legacy: VaultKeyData = serde_json::from_value(value).expect("legacy record must deserialize");
|
||||||
assert!(legacy.deletion_date.is_none());
|
assert!(legacy.deletion_date.is_none());
|
||||||
}
|
}
|
||||||
|
|
||||||
/// KV2 write acknowledgement (`SecretVersionMetadata`) for `kv2::set`.
|
|
||||||
fn kv2_write_ack() -> serde_json::Value {
|
|
||||||
serde_json::json!({
|
|
||||||
"created_time": "2026-01-01T00:00:00Z",
|
|
||||||
"custom_metadata": null,
|
|
||||||
"deletion_time": "",
|
|
||||||
"destroyed": false,
|
|
||||||
"version": 2,
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
#[tokio::test]
|
|
||||||
async fn wired_kv2_encrypt_round_trips_through_decrypt() {
|
|
||||||
// One key-record read for the encrypt, one for the decrypt.
|
|
||||||
let (_vault, client) = scripted_client(vec![
|
|
||||||
ScriptedResponse::ok(kv2_read_data(&healthy_key_data())),
|
|
||||||
ScriptedResponse::ok(kv2_read_data(&healthy_key_data())),
|
|
||||||
])
|
|
||||||
.await;
|
|
||||||
let context = HashMap::from([("bucket".to_string(), "kv2".to_string())]);
|
|
||||||
|
|
||||||
let encrypted = client
|
|
||||||
.encrypt(
|
|
||||||
&EncryptRequest {
|
|
||||||
key_id: "wired-key".to_string(),
|
|
||||||
plaintext: b"kv2-direct-encrypt".to_vec(),
|
|
||||||
encryption_context: context.clone(),
|
|
||||||
grant_tokens: Vec::new(),
|
|
||||||
},
|
|
||||||
None,
|
|
||||||
)
|
|
||||||
.await
|
|
||||||
.expect("encrypt must produce an envelope");
|
|
||||||
|
|
||||||
// The ciphertext is a real KMS envelope wrapping AEAD output that
|
|
||||||
// decrypt() can open, not an XOR of the plaintext with the master key
|
|
||||||
// material.
|
|
||||||
let envelope: DataKeyEnvelope = serde_json::from_slice(&encrypted.ciphertext).expect("envelope must parse");
|
|
||||||
assert_eq!(envelope.master_key_id, "wired-key");
|
|
||||||
assert_eq!(envelope.master_key_version, Some(1));
|
|
||||||
|
|
||||||
let decrypted = client
|
|
||||||
.decrypt(
|
|
||||||
&DecryptRequest {
|
|
||||||
ciphertext: encrypted.ciphertext.clone(),
|
|
||||||
encryption_context: context,
|
|
||||||
grant_tokens: Vec::new(),
|
|
||||||
},
|
|
||||||
None,
|
|
||||||
)
|
|
||||||
.await
|
|
||||||
.expect("decrypt must round-trip the envelope");
|
|
||||||
assert_eq!(decrypted, b"kv2-direct-encrypt".to_vec());
|
|
||||||
|
|
||||||
// A different object context must not decrypt (checked before any
|
|
||||||
// Vault read, so no scripted response is consumed).
|
|
||||||
let error = client
|
|
||||||
.decrypt(
|
|
||||||
&DecryptRequest {
|
|
||||||
ciphertext: encrypted.ciphertext,
|
|
||||||
encryption_context: HashMap::from([("bucket".to_string(), "other".to_string())]),
|
|
||||||
grant_tokens: Vec::new(),
|
|
||||||
},
|
|
||||||
None,
|
|
||||||
)
|
|
||||||
.await
|
|
||||||
.expect_err("a different context must not decrypt");
|
|
||||||
assert!(matches!(error, KmsError::ContextMismatch { .. }), "got {error:?}");
|
|
||||||
}
|
|
||||||
|
|
||||||
/// KV2 secret-metadata read payload (`kv2::read_metadata`) pinning the
|
|
||||||
/// current secret version used as the rotation check-and-set base.
|
|
||||||
fn kv2_metadata_read_data(current_version: u64) -> serde_json::Value {
|
|
||||||
serde_json::json!({
|
|
||||||
"cas_required": false,
|
|
||||||
"created_time": "2026-01-01T00:00:00Z",
|
|
||||||
"current_version": current_version,
|
|
||||||
"delete_version_after": "0s",
|
|
||||||
"max_versions": 0,
|
|
||||||
"oldest_version": 0,
|
|
||||||
"updated_time": "2026-01-01T00:00:00Z",
|
|
||||||
"custom_metadata": null,
|
|
||||||
"versions": {},
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
#[tokio::test]
|
|
||||||
async fn wired_kv2_rotate_rejected_while_disabled() {
|
|
||||||
let mut key_data = healthy_key_data();
|
|
||||||
key_data.status = KeyStatus::Disabled;
|
|
||||||
let (vault, client) = scripted_client(vec![
|
|
||||||
ScriptedResponse::ok(kv2_metadata_read_data(1)),
|
|
||||||
ScriptedResponse::ok(kv2_read_data(&key_data)),
|
|
||||||
])
|
|
||||||
.await;
|
|
||||||
|
|
||||||
let error = client
|
|
||||||
.rotate_key("wired-key", None)
|
|
||||||
.await
|
|
||||||
.expect_err("rotation of a disabled key must be rejected");
|
|
||||||
assert!(matches!(error, KmsError::InvalidOperation { .. }), "got {error:?}");
|
|
||||||
|
|
||||||
let requests = vault.requests();
|
|
||||||
assert_eq!(
|
|
||||||
requests.len(),
|
|
||||||
2,
|
|
||||||
"the state gate must reject after the versioned read, before any write: {requests:?}"
|
|
||||||
);
|
|
||||||
assert!(requests.iter().all(|line| line.starts_with("GET ")), "{requests:?}");
|
|
||||||
}
|
|
||||||
|
|
||||||
#[tokio::test]
|
|
||||||
async fn wired_backend_lifecycle_overrides_reach_the_client() {
|
|
||||||
let mut disabled = healthy_key_data();
|
|
||||||
disabled.status = KeyStatus::Disabled;
|
|
||||||
let vault = ScriptedVault::serve(vec![
|
|
||||||
// disable: read the Active record, persist it Disabled.
|
|
||||||
ScriptedResponse::ok(kv2_read_data(&healthy_key_data())),
|
|
||||||
ScriptedResponse::ok(kv2_write_ack()),
|
|
||||||
// enable: read the Disabled record, persist it Active.
|
|
||||||
ScriptedResponse::ok(kv2_read_data(&disabled)),
|
|
||||||
ScriptedResponse::ok(kv2_write_ack()),
|
|
||||||
])
|
|
||||||
.await;
|
|
||||||
let config = KmsConfig::vault(
|
|
||||||
url::Url::parse(&vault.address).expect("scripted vault address should parse"),
|
|
||||||
"scripted-token".to_string(),
|
|
||||||
)
|
|
||||||
.with_insecure_development_defaults();
|
|
||||||
let backend = VaultKmsBackend::new(config).await.expect("vault kv2 backend should build");
|
|
||||||
|
|
||||||
backend
|
|
||||||
.disable_key("wired-key")
|
|
||||||
.await
|
|
||||||
.expect("KmsBackend::disable_key must persist through the client");
|
|
||||||
backend
|
|
||||||
.enable_key("wired-key")
|
|
||||||
.await
|
|
||||||
.expect("KmsBackend::enable_key must persist through the client");
|
|
||||||
|
|
||||||
let requests = vault.requests();
|
|
||||||
assert_eq!(requests.len(), 4, "each transition is one read plus one write: {requests:?}");
|
|
||||||
assert!(requests[0].starts_with("GET ") && requests[2].starts_with("GET "), "{requests:?}");
|
|
||||||
assert!(requests[1].starts_with("POST ") && requests[3].starts_with("POST "), "{requests:?}");
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -18,7 +18,9 @@ use crate::backends::vault_credentials::{
|
|||||||
CredentialTaskHandle, VaultClientHandle, VaultConnectionSettings, VaultCredentialPolicy, VaultCredentialProvider,
|
CredentialTaskHandle, VaultClientHandle, VaultConnectionSettings, VaultCredentialPolicy, VaultCredentialProvider,
|
||||||
token_source_for,
|
token_source_for,
|
||||||
};
|
};
|
||||||
use crate::backends::{BackendCapabilities, ExpiredKeyRemoval, KmsBackend, StateGatedOperation, ensure_key_state_permits};
|
use crate::backends::{
|
||||||
|
BackendCapabilities, BackendInfo, ExpiredKeyRemoval, KmsBackend, KmsClient, StateGatedOperation, ensure_key_state_permits,
|
||||||
|
};
|
||||||
use crate::config::{KmsConfig, VaultTransitConfig};
|
use crate::config::{KmsConfig, VaultTransitConfig};
|
||||||
use crate::encryption::{DataKeyEnvelope, generate_key_material};
|
use crate::encryption::{DataKeyEnvelope, generate_key_material};
|
||||||
use crate::error::{KmsError, Result};
|
use crate::error::{KmsError, Result};
|
||||||
@@ -504,12 +506,9 @@ impl VaultTransitKmsClient {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl VaultTransitKmsClient {
|
#[async_trait]
|
||||||
pub(crate) async fn generate_data_key(
|
impl KmsClient for VaultTransitKmsClient {
|
||||||
&self,
|
async fn generate_data_key(&self, request: &GenerateKeyRequest, _context: Option<&OperationContext>) -> Result<DataKeyInfo> {
|
||||||
request: &GenerateKeyRequest,
|
|
||||||
_context: Option<&OperationContext>,
|
|
||||||
) -> Result<DataKeyInfo> {
|
|
||||||
self.ensure_key_state_allows(&request.master_key_id, StateGatedOperation::GenerateDataKey)
|
self.ensure_key_state_allows(&request.master_key_id, StateGatedOperation::GenerateDataKey)
|
||||||
.await?;
|
.await?;
|
||||||
|
|
||||||
@@ -541,7 +540,7 @@ impl VaultTransitKmsClient {
|
|||||||
))
|
))
|
||||||
}
|
}
|
||||||
|
|
||||||
pub(crate) async fn encrypt(&self, request: &EncryptRequest, _context: Option<&OperationContext>) -> Result<EncryptResponse> {
|
async fn encrypt(&self, request: &EncryptRequest, _context: Option<&OperationContext>) -> Result<EncryptResponse> {
|
||||||
let metadata = self
|
let metadata = self
|
||||||
.ensure_key_state_allows(&request.key_id, StateGatedOperation::Encrypt)
|
.ensure_key_state_allows(&request.key_id, StateGatedOperation::Encrypt)
|
||||||
.await?;
|
.await?;
|
||||||
@@ -557,7 +556,7 @@ impl VaultTransitKmsClient {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
pub(crate) async fn decrypt(&self, request: &DecryptRequest, _context: Option<&OperationContext>) -> Result<Vec<u8>> {
|
async fn decrypt(&self, request: &DecryptRequest, _context: Option<&OperationContext>) -> Result<Vec<u8>> {
|
||||||
let envelope: DataKeyEnvelope = serde_json::from_slice(&request.ciphertext)
|
let envelope: DataKeyEnvelope = serde_json::from_slice(&request.ciphertext)
|
||||||
.map_err(|e| KmsError::cryptographic_error("parse", format!("Failed to parse data key envelope: {e}")))?;
|
.map_err(|e| KmsError::cryptographic_error("parse", format!("Failed to parse data key envelope: {e}")))?;
|
||||||
|
|
||||||
@@ -579,14 +578,7 @@ impl VaultTransitKmsClient {
|
|||||||
.await
|
.await
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Test-only lifecycle driver: the product path goes through [`KmsBackend`].
|
async fn create_key(&self, key_id: &str, algorithm: &str, _context: Option<&OperationContext>) -> Result<MasterKeyInfo> {
|
||||||
#[cfg(test)]
|
|
||||||
pub(crate) async fn create_key(
|
|
||||||
&self,
|
|
||||||
key_id: &str,
|
|
||||||
algorithm: &str,
|
|
||||||
_context: Option<&OperationContext>,
|
|
||||||
) -> Result<MasterKeyInfo> {
|
|
||||||
if algorithm != "AES_256" {
|
if algorithm != "AES_256" {
|
||||||
return Err(KmsError::unsupported_algorithm(algorithm));
|
return Err(KmsError::unsupported_algorithm(algorithm));
|
||||||
}
|
}
|
||||||
@@ -653,17 +645,11 @@ impl VaultTransitKmsClient {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Test-only lifecycle driver: the product path goes through [`KmsBackend`].
|
async fn describe_key(&self, key_id: &str, _context: Option<&OperationContext>) -> Result<KeyInfo> {
|
||||||
#[cfg(test)]
|
|
||||||
pub(crate) async fn describe_key(&self, key_id: &str, _context: Option<&OperationContext>) -> Result<KeyInfo> {
|
|
||||||
self.key_info(key_id).await
|
self.key_info(key_id).await
|
||||||
}
|
}
|
||||||
|
|
||||||
pub(crate) async fn list_keys(
|
async fn list_keys(&self, request: &ListKeysRequest, _context: Option<&OperationContext>) -> Result<ListKeysResponse> {
|
||||||
&self,
|
|
||||||
request: &ListKeysRequest,
|
|
||||||
_context: Option<&OperationContext>,
|
|
||||||
) -> Result<ListKeysResponse> {
|
|
||||||
let all_keys = self
|
let all_keys = self
|
||||||
.run("vault_transit_list_keys", OpClass::ReadIdempotent, move || async move {
|
.run("vault_transit_list_keys", OpClass::ReadIdempotent, move || async move {
|
||||||
let vault = self.vault().map_err(AttemptError::fatal)?;
|
let vault = self.vault().map_err(AttemptError::fatal)?;
|
||||||
@@ -706,7 +692,7 @@ impl VaultTransitKmsClient {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
pub(crate) async fn enable_key(&self, key_id: &str, _context: Option<&OperationContext>) -> Result<()> {
|
async fn enable_key(&self, key_id: &str, _context: Option<&OperationContext>) -> Result<()> {
|
||||||
// A pending deletion must be reverted through cancel_key_deletion, not
|
// A pending deletion must be reverted through cancel_key_deletion, not
|
||||||
// silently by enabling, so the gate rejects PendingDeletion here.
|
// silently by enabling, so the gate rejects PendingDeletion here.
|
||||||
let mut metadata = self.ensure_key_state_allows(key_id, StateGatedOperation::Enable).await?;
|
let mut metadata = self.ensure_key_state_allows(key_id, StateGatedOperation::Enable).await?;
|
||||||
@@ -715,15 +701,13 @@ impl VaultTransitKmsClient {
|
|||||||
self.store_key_metadata(key_id, &metadata).await
|
self.store_key_metadata(key_id, &metadata).await
|
||||||
}
|
}
|
||||||
|
|
||||||
pub(crate) async fn disable_key(&self, key_id: &str, _context: Option<&OperationContext>) -> Result<()> {
|
async fn disable_key(&self, key_id: &str, _context: Option<&OperationContext>) -> Result<()> {
|
||||||
let mut metadata = self.ensure_key_state_allows(key_id, StateGatedOperation::Disable).await?;
|
let mut metadata = self.ensure_key_state_allows(key_id, StateGatedOperation::Disable).await?;
|
||||||
metadata.key_state = KeyState::Disabled;
|
metadata.key_state = KeyState::Disabled;
|
||||||
self.store_key_metadata(key_id, &metadata).await
|
self.store_key_metadata(key_id, &metadata).await
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Test-only lifecycle driver: the product path goes through [`KmsBackend`].
|
async fn schedule_key_deletion(
|
||||||
#[cfg(test)]
|
|
||||||
pub(crate) async fn schedule_key_deletion(
|
|
||||||
&self,
|
&self,
|
||||||
key_id: &str,
|
key_id: &str,
|
||||||
pending_window_days: u32,
|
pending_window_days: u32,
|
||||||
@@ -737,7 +721,17 @@ impl VaultTransitKmsClient {
|
|||||||
self.store_key_metadata(key_id, &metadata).await
|
self.store_key_metadata(key_id, &metadata).await
|
||||||
}
|
}
|
||||||
|
|
||||||
pub(crate) async fn rotate_key(&self, key_id: &str, _context: Option<&OperationContext>) -> Result<MasterKeyInfo> {
|
async fn cancel_key_deletion(&self, key_id: &str, _context: Option<&OperationContext>) -> Result<()> {
|
||||||
|
let mut metadata = self.get_key_metadata(key_id).await?;
|
||||||
|
if metadata.key_state != KeyState::PendingDeletion {
|
||||||
|
return Err(KmsError::invalid_key_state(format!("Key {key_id} is not pending deletion")));
|
||||||
|
}
|
||||||
|
metadata.key_state = KeyState::Enabled;
|
||||||
|
metadata.deletion_date = None;
|
||||||
|
self.store_key_metadata(key_id, &metadata).await
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn rotate_key(&self, key_id: &str, _context: Option<&OperationContext>) -> Result<MasterKeyInfo> {
|
||||||
self.ensure_key_state_allows(key_id, StateGatedOperation::Rotate).await?;
|
self.ensure_key_state_allows(key_id, StateGatedOperation::Rotate).await?;
|
||||||
|
|
||||||
// Single attempt, never retried: replaying a rotate whose response was
|
// Single attempt, never retried: replaying a rotate whose response was
|
||||||
@@ -774,7 +768,7 @@ impl VaultTransitKmsClient {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
pub(crate) async fn health_check(&self) -> Result<()> {
|
async fn health_check(&self) -> Result<()> {
|
||||||
self.run("vault_transit_health_check", OpClass::ReadIdempotent, move || async move {
|
self.run("vault_transit_health_check", OpClass::ReadIdempotent, move || async move {
|
||||||
let vault = self.vault().map_err(AttemptError::fatal)?;
|
let vault = self.vault().map_err(AttemptError::fatal)?;
|
||||||
key::list(&vault.client, &self.config.mount_path)
|
key::list(&vault.client, &self.config.mount_path)
|
||||||
@@ -786,6 +780,11 @@ impl VaultTransitKmsClient {
|
|||||||
})
|
})
|
||||||
.await
|
.await
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn backend_info(&self) -> BackendInfo {
|
||||||
|
BackendInfo::new("vault-transit".to_string(), "0.1.0".to_string(), self.config.address.clone(), true)
|
||||||
|
.with_metadata("mount_path".to_string(), self.config.mount_path.clone())
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub struct VaultTransitKmsBackend {
|
pub struct VaultTransitKmsBackend {
|
||||||
@@ -793,6 +792,14 @@ pub struct VaultTransitKmsBackend {
|
|||||||
}
|
}
|
||||||
|
|
||||||
impl VaultTransitKmsBackend {
|
impl VaultTransitKmsBackend {
|
||||||
|
/// Lifecycle driver for the shared state-machine contract tests. Using the
|
||||||
|
/// backend's own client keeps its in-process metadata cache coherent with
|
||||||
|
/// the transitions the tests perform.
|
||||||
|
#[cfg(test)]
|
||||||
|
pub(crate) fn lifecycle_client(&self) -> &VaultTransitKmsClient {
|
||||||
|
&self.client
|
||||||
|
}
|
||||||
|
|
||||||
pub async fn new(config: KmsConfig) -> Result<Self> {
|
pub async fn new(config: KmsConfig) -> Result<Self> {
|
||||||
config.validate()?;
|
config.validate()?;
|
||||||
|
|
||||||
@@ -993,18 +1000,6 @@ impl KmsBackend for VaultTransitKmsBackend {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
async fn enable_key(&self, key_id: &str) -> Result<()> {
|
|
||||||
self.client.enable_key(key_id, None).await
|
|
||||||
}
|
|
||||||
|
|
||||||
async fn disable_key(&self, key_id: &str) -> Result<()> {
|
|
||||||
self.client.disable_key(key_id, None).await
|
|
||||||
}
|
|
||||||
|
|
||||||
async fn rotate_key(&self, key_id: &str) -> Result<()> {
|
|
||||||
self.client.rotate_key(key_id, None).await.map(|_| ())
|
|
||||||
}
|
|
||||||
|
|
||||||
async fn health_check(&self) -> Result<bool> {
|
async fn health_check(&self) -> Result<bool> {
|
||||||
self.client.health_check().await.map(|_| true)
|
self.client.health_check().await.map(|_| true)
|
||||||
}
|
}
|
||||||
@@ -1442,58 +1437,4 @@ mod tests {
|
|||||||
assert_eq!(metadata.key_state, KeyState::Enabled);
|
assert_eq!(metadata.key_state, KeyState::Enabled);
|
||||||
assert!(metadata.deletion_date.is_none());
|
assert!(metadata.deletion_date.is_none());
|
||||||
}
|
}
|
||||||
|
|
||||||
/// KV2 write acknowledgement (`SecretVersionMetadata`) for `kv2::set`.
|
|
||||||
fn kv2_write_ack() -> serde_json::Value {
|
|
||||||
serde_json::json!({
|
|
||||||
"created_time": "2026-01-01T00:00:00Z",
|
|
||||||
"custom_metadata": null,
|
|
||||||
"deletion_time": "",
|
|
||||||
"destroyed": false,
|
|
||||||
"version": 2,
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
#[tokio::test]
|
|
||||||
async fn wired_backend_lifecycle_overrides_reach_the_client() {
|
|
||||||
let metadata = TransitKeyMetadata::from_create_request(&CreateKeyRequest::default());
|
|
||||||
let vault = ScriptedVault::serve(vec![
|
|
||||||
// disable: metadata cache miss reads KV, then persists Disabled.
|
|
||||||
ScriptedResponse::ok(metadata_read_data(&metadata)),
|
|
||||||
ScriptedResponse::ok(kv2_write_ack()),
|
|
||||||
// enable: the state gate hits the metadata cache, so only the
|
|
||||||
// persisting write goes out.
|
|
||||||
ScriptedResponse::ok(kv2_write_ack()),
|
|
||||||
// rotate: the gate hits the cache again; the single rotate
|
|
||||||
// attempt fails and must not be retried.
|
|
||||||
ScriptedResponse::error(503, "standby"),
|
|
||||||
])
|
|
||||||
.await;
|
|
||||||
let config = KmsConfig::vault_transit(
|
|
||||||
url::Url::parse(&vault.address).expect("scripted vault address should parse"),
|
|
||||||
"scripted-token".to_string(),
|
|
||||||
)
|
|
||||||
.with_insecure_development_defaults();
|
|
||||||
let backend = VaultTransitKmsBackend::new(config)
|
|
||||||
.await
|
|
||||||
.expect("vault transit backend should build");
|
|
||||||
|
|
||||||
backend
|
|
||||||
.disable_key("wired-key")
|
|
||||||
.await
|
|
||||||
.expect("KmsBackend::disable_key must persist through the client");
|
|
||||||
backend
|
|
||||||
.enable_key("wired-key")
|
|
||||||
.await
|
|
||||||
.expect("KmsBackend::enable_key must persist through the client");
|
|
||||||
let error = backend
|
|
||||||
.rotate_key("wired-key")
|
|
||||||
.await
|
|
||||||
.expect_err("the scripted 503 must fail the rotation");
|
|
||||||
assert!(matches!(error, KmsError::BackendError { .. }), "got {error:?}");
|
|
||||||
|
|
||||||
let requests = vault.requests();
|
|
||||||
assert_eq!(requests.len(), 4, "gated reads, two writes and one rotate attempt: {requests:?}");
|
|
||||||
assert_eq!(requests[3], "POST /v1/transit/keys/wired-key/rotate", "{requests:?}");
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -182,6 +182,7 @@ impl DeletionWorker {
|
|||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
mod tests {
|
mod tests {
|
||||||
use super::*;
|
use super::*;
|
||||||
|
use crate::backends::KmsClient as _;
|
||||||
use crate::backends::local::LocalKmsBackend;
|
use crate::backends::local::LocalKmsBackend;
|
||||||
use crate::config::KmsConfig;
|
use crate::config::KmsConfig;
|
||||||
use crate::error::KmsError;
|
use crate::error::KmsError;
|
||||||
|
|||||||
@@ -66,19 +66,16 @@ impl KmsManager {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// Encrypt data with a master key
|
/// Encrypt data with a master key
|
||||||
#[hotpath::measure]
|
|
||||||
pub async fn encrypt(&self, request: EncryptRequest) -> Result<EncryptResponse> {
|
pub async fn encrypt(&self, request: EncryptRequest) -> Result<EncryptResponse> {
|
||||||
self.backend.encrypt(request).await
|
self.backend.encrypt(request).await
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Decrypt data with a master key
|
/// Decrypt data with a master key
|
||||||
#[hotpath::measure]
|
|
||||||
pub async fn decrypt(&self, request: DecryptRequest) -> Result<DecryptResponse> {
|
pub async fn decrypt(&self, request: DecryptRequest) -> Result<DecryptResponse> {
|
||||||
self.backend.decrypt(request).await
|
self.backend.decrypt(request).await
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Generate a data encryption key
|
/// Generate a data encryption key
|
||||||
#[hotpath::measure]
|
|
||||||
pub async fn generate_data_key(&self, request: GenerateDataKeyRequest) -> Result<GenerateDataKeyResponse> {
|
pub async fn generate_data_key(&self, request: GenerateDataKeyRequest) -> Result<GenerateDataKeyResponse> {
|
||||||
self.backend.generate_data_key(request).await
|
self.backend.generate_data_key(request).await
|
||||||
}
|
}
|
||||||
|
|||||||
+22
-508
@@ -27,12 +27,6 @@
|
|||||||
//! retried automatically: a response lost after the server applied the write
|
//! retried automatically: a response lost after the server applied the write
|
||||||
//! would otherwise be replayed into duplicate side effects (extra key versions,
|
//! would otherwise be replayed into duplicate side effects (extra key versions,
|
||||||
//! repeated deletes).
|
//! repeated deletes).
|
||||||
//!
|
|
||||||
//! Every execution also records operation metrics (attempt failures by retry
|
|
||||||
//! class, terminal outcome, attempts used, wall-clock duration) through the
|
|
||||||
//! process-global `metrics` recorder. Metric labels carry only static enum
|
|
||||||
//! values — operation names, classes, outcomes — never key identifiers, key
|
|
||||||
//! material, ciphertext, or tokens.
|
|
||||||
|
|
||||||
use std::future::Future;
|
use std::future::Future;
|
||||||
use std::time::Duration;
|
use std::time::Duration;
|
||||||
@@ -206,130 +200,6 @@ fn equal_jitter(rng: &mut impl RngExt, cap: Duration) -> Duration {
|
|||||||
half + Duration::from_nanos(rng.random_range(0..=spread))
|
half + Duration::from_nanos(rng.random_range(0..=spread))
|
||||||
}
|
}
|
||||||
|
|
||||||
// ---------------------------------------------------------------------------
|
|
||||||
// Metrics
|
|
||||||
//
|
|
||||||
// Every execution is recorded here, at the single choke point all backend
|
|
||||||
// calls flow through, so instrumenting a new call site costs nothing beyond
|
|
||||||
// naming its operation. Label values are exclusively static enum strings
|
|
||||||
// (operation names, classes, outcomes) — key identifiers, key material,
|
|
||||||
// ciphertext, and tokens must never reach a metric label.
|
|
||||||
// ---------------------------------------------------------------------------
|
|
||||||
|
|
||||||
/// Counter: operations executed, by `operation`, `op_class`, and `outcome`.
|
|
||||||
const METRIC_OPERATIONS_TOTAL: &str = "rustfs_kms_backend_operations_total";
|
|
||||||
/// Counter: failed attempts, by `operation` and `error_class` (including
|
|
||||||
/// `attempt_timeout` for attempts cut off by the per-attempt timeout).
|
|
||||||
const METRIC_ATTEMPT_FAILURES_TOTAL: &str = "rustfs_kms_backend_attempt_failures_total";
|
|
||||||
/// Histogram: wall-clock duration of a whole operation (attempts plus
|
|
||||||
/// backoff), in seconds, by `operation` and `outcome`.
|
|
||||||
const METRIC_OPERATION_DURATION_SECONDS: &str = "rustfs_kms_backend_operation_duration_seconds";
|
|
||||||
/// Histogram: attempts one operation used before completing, by `operation`
|
|
||||||
/// and `outcome`.
|
|
||||||
const METRIC_OPERATION_ATTEMPTS: &str = "rustfs_kms_backend_operation_attempts";
|
|
||||||
|
|
||||||
impl OpClass {
|
|
||||||
fn as_label(self) -> &'static str {
|
|
||||||
match self {
|
|
||||||
OpClass::ReadIdempotent => "read_idempotent",
|
|
||||||
OpClass::MutatingNonIdempotent => "mutating_non_idempotent",
|
|
||||||
OpClass::Auth => "auth",
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl ErrorClass {
|
|
||||||
fn as_label(self) -> &'static str {
|
|
||||||
match self {
|
|
||||||
ErrorClass::RetryableConn => "retryable_conn",
|
|
||||||
ErrorClass::RetryableStatus => "retryable_status",
|
|
||||||
ErrorClass::Fatal => "fatal",
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// How one policy execution terminated, for the `outcome` metric label.
|
|
||||||
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
|
||||||
enum Outcome {
|
|
||||||
Success,
|
|
||||||
/// A fatal-classified failure ended the operation on its first observation.
|
|
||||||
Fatal,
|
|
||||||
/// The attempt budget ran out; the last failure was retryable (including a
|
|
||||||
/// timed-out final attempt).
|
|
||||||
BudgetExhausted,
|
|
||||||
/// The operation deadline ran out before another attempt could complete.
|
|
||||||
DeadlineExceeded,
|
|
||||||
Cancelled,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl Outcome {
|
|
||||||
fn as_label(self) -> &'static str {
|
|
||||||
match self {
|
|
||||||
Outcome::Success => "success",
|
|
||||||
Outcome::Fatal => "fatal",
|
|
||||||
Outcome::BudgetExhausted => "budget_exhausted",
|
|
||||||
Outcome::DeadlineExceeded => "deadline_exceeded",
|
|
||||||
Outcome::Cancelled => "cancelled",
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Register metric descriptions once per process.
|
|
||||||
fn describe_metrics() {
|
|
||||||
static DESCRIBE: std::sync::Once = std::sync::Once::new();
|
|
||||||
DESCRIBE.call_once(|| {
|
|
||||||
metrics::describe_counter!(
|
|
||||||
METRIC_OPERATIONS_TOTAL,
|
|
||||||
"Total KMS backend operations executed under the operation policy, by operation, operation class, and outcome"
|
|
||||||
);
|
|
||||||
metrics::describe_counter!(
|
|
||||||
METRIC_ATTEMPT_FAILURES_TOTAL,
|
|
||||||
"Total failed KMS backend attempts, by operation and retry classification"
|
|
||||||
);
|
|
||||||
metrics::describe_histogram!(
|
|
||||||
METRIC_OPERATION_DURATION_SECONDS,
|
|
||||||
"Wall-clock duration of KMS backend operations including retries and backoff, in seconds"
|
|
||||||
);
|
|
||||||
metrics::describe_histogram!(
|
|
||||||
METRIC_OPERATION_ATTEMPTS,
|
|
||||||
"Number of attempts a KMS backend operation used before completing"
|
|
||||||
);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Record one failed attempt with its retry classification.
|
|
||||||
fn record_attempt_failure(operation: &'static str, error_class: &'static str) {
|
|
||||||
metrics::counter!(
|
|
||||||
METRIC_ATTEMPT_FAILURES_TOTAL,
|
|
||||||
"operation" => operation,
|
|
||||||
"error_class" => error_class
|
|
||||||
)
|
|
||||||
.increment(1);
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Record the terminal outcome of one policy execution.
|
|
||||||
fn record_operation(operation: &'static str, class: OpClass, outcome: Outcome, attempts: u32, elapsed: Duration) {
|
|
||||||
metrics::counter!(
|
|
||||||
METRIC_OPERATIONS_TOTAL,
|
|
||||||
"operation" => operation,
|
|
||||||
"op_class" => class.as_label(),
|
|
||||||
"outcome" => outcome.as_label()
|
|
||||||
)
|
|
||||||
.increment(1);
|
|
||||||
metrics::histogram!(
|
|
||||||
METRIC_OPERATION_DURATION_SECONDS,
|
|
||||||
"operation" => operation,
|
|
||||||
"outcome" => outcome.as_label()
|
|
||||||
)
|
|
||||||
.record(elapsed.as_secs_f64());
|
|
||||||
metrics::histogram!(
|
|
||||||
METRIC_OPERATION_ATTEMPTS,
|
|
||||||
"operation" => operation,
|
|
||||||
"outcome" => outcome.as_label()
|
|
||||||
)
|
|
||||||
.record(f64::from(attempts));
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Run `attempt` under the policy.
|
/// Run `attempt` under the policy.
|
||||||
///
|
///
|
||||||
/// Each attempt is bounded by `attempt_timeout` (further capped by whatever is
|
/// Each attempt is bounded by `attempt_timeout` (further capped by whatever is
|
||||||
@@ -360,37 +230,13 @@ where
|
|||||||
/// [`execute`] with an injectable jitter source so tests can pin deterministic
|
/// [`execute`] with an injectable jitter source so tests can pin deterministic
|
||||||
/// backoff durations instead of asserting around random sleeps.
|
/// backoff durations instead of asserting around random sleeps.
|
||||||
pub(crate) async fn execute_with_jitter<T, F, Fut, J>(
|
pub(crate) async fn execute_with_jitter<T, F, Fut, J>(
|
||||||
operation: &'static str,
|
|
||||||
class: OpClass,
|
|
||||||
policy: &RetryPolicy,
|
|
||||||
cancel: &CancellationToken,
|
|
||||||
jitter: J,
|
|
||||||
attempt: F,
|
|
||||||
) -> Result<T>
|
|
||||||
where
|
|
||||||
F: FnMut() -> Fut,
|
|
||||||
Fut: Future<Output = std::result::Result<T, AttemptError>>,
|
|
||||||
J: FnMut(Duration) -> Duration,
|
|
||||||
{
|
|
||||||
describe_metrics();
|
|
||||||
let started = Instant::now();
|
|
||||||
let mut attempts_made = 0u32;
|
|
||||||
let (outcome, result) = drive_attempts(operation, class, policy, cancel, jitter, attempt, &mut attempts_made).await;
|
|
||||||
record_operation(operation, class, outcome, attempts_made, started.elapsed());
|
|
||||||
result
|
|
||||||
}
|
|
||||||
|
|
||||||
/// The attempt loop behind [`execute_with_jitter`], returning the terminal
|
|
||||||
/// outcome alongside the result so the caller can record it exactly once.
|
|
||||||
async fn drive_attempts<T, F, Fut, J>(
|
|
||||||
operation: &'static str,
|
operation: &'static str,
|
||||||
class: OpClass,
|
class: OpClass,
|
||||||
policy: &RetryPolicy,
|
policy: &RetryPolicy,
|
||||||
cancel: &CancellationToken,
|
cancel: &CancellationToken,
|
||||||
mut jitter: J,
|
mut jitter: J,
|
||||||
mut attempt: F,
|
mut attempt: F,
|
||||||
attempts_made: &mut u32,
|
) -> Result<T>
|
||||||
) -> (Outcome, Result<T>)
|
|
||||||
where
|
where
|
||||||
F: FnMut() -> Fut,
|
F: FnMut() -> Fut,
|
||||||
Fut: Future<Output = std::result::Result<T, AttemptError>>,
|
Fut: Future<Output = std::result::Result<T, AttemptError>>,
|
||||||
@@ -401,68 +247,48 @@ where
|
|||||||
|
|
||||||
let mut attempt_no = 0u32;
|
let mut attempt_no = 0u32;
|
||||||
loop {
|
loop {
|
||||||
|
attempt_no += 1;
|
||||||
if cancel.is_cancelled() {
|
if cancel.is_cancelled() {
|
||||||
return (
|
return Err(KmsError::operation_cancelled(format!(
|
||||||
Outcome::Cancelled,
|
"{operation} cancelled before attempt {attempt_no}"
|
||||||
Err(KmsError::operation_cancelled(format!(
|
)));
|
||||||
"{operation} cancelled before attempt {}",
|
|
||||||
attempt_no + 1
|
|
||||||
))),
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
let remaining = deadline.saturating_duration_since(Instant::now());
|
let remaining = deadline.saturating_duration_since(Instant::now());
|
||||||
if remaining.is_zero() {
|
if remaining.is_zero() {
|
||||||
return (
|
return Err(KmsError::operation_timed_out(format!(
|
||||||
Outcome::DeadlineExceeded,
|
"{operation} exceeded operation deadline of {:?}",
|
||||||
Err(KmsError::operation_timed_out(format!(
|
policy.op_deadline
|
||||||
"{operation} exceeded operation deadline of {:?}",
|
)));
|
||||||
policy.op_deadline
|
|
||||||
))),
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
attempt_no += 1;
|
|
||||||
*attempts_made = attempt_no;
|
|
||||||
let attempt_budget = policy.attempt_timeout.min(remaining);
|
let attempt_budget = policy.attempt_timeout.min(remaining);
|
||||||
let outcome = tokio::select! {
|
let outcome = tokio::select! {
|
||||||
biased;
|
biased;
|
||||||
_ = cancel.cancelled() => {
|
_ = cancel.cancelled() => {
|
||||||
return (
|
return Err(KmsError::operation_cancelled(format!("{operation} cancelled during attempt {attempt_no}")));
|
||||||
Outcome::Cancelled,
|
|
||||||
Err(KmsError::operation_cancelled(format!("{operation} cancelled during attempt {attempt_no}"))),
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
outcome = tokio::time::timeout(attempt_budget, attempt()) => outcome,
|
outcome = tokio::time::timeout(attempt_budget, attempt()) => outcome,
|
||||||
};
|
};
|
||||||
|
|
||||||
let failure = match outcome {
|
let failure = match outcome {
|
||||||
Ok(Ok(value)) => return (Outcome::Success, Ok(value)),
|
Ok(Ok(value)) => return Ok(value),
|
||||||
Ok(Err(failure)) => {
|
Ok(Err(failure)) => failure,
|
||||||
record_attempt_failure(operation, failure.class.as_label());
|
Err(_) => AttemptError {
|
||||||
failure
|
class: ErrorClass::RetryableConn,
|
||||||
}
|
error: KmsError::operation_timed_out(format!(
|
||||||
Err(_) => {
|
"{operation} attempt {attempt_no} timed out after {attempt_budget:?}"
|
||||||
record_attempt_failure(operation, "attempt_timeout");
|
)),
|
||||||
AttemptError {
|
},
|
||||||
class: ErrorClass::RetryableConn,
|
|
||||||
error: KmsError::operation_timed_out(format!(
|
|
||||||
"{operation} attempt {attempt_no} timed out after {attempt_budget:?}"
|
|
||||||
)),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
};
|
};
|
||||||
|
|
||||||
if failure.class == ErrorClass::Fatal {
|
if failure.class == ErrorClass::Fatal || attempt_no >= max_attempts {
|
||||||
return (Outcome::Fatal, Err(failure.error));
|
return Err(failure.error);
|
||||||
}
|
|
||||||
if attempt_no >= max_attempts {
|
|
||||||
return (Outcome::BudgetExhausted, Err(failure.error));
|
|
||||||
}
|
}
|
||||||
|
|
||||||
let backoff = jitter(backoff_cap(policy, attempt_no));
|
let backoff = jitter(backoff_cap(policy, attempt_no));
|
||||||
if backoff >= deadline.saturating_duration_since(Instant::now()) {
|
if backoff >= deadline.saturating_duration_since(Instant::now()) {
|
||||||
// Not enough deadline budget left for another attempt.
|
// Not enough deadline budget left for another attempt.
|
||||||
return (Outcome::DeadlineExceeded, Err(failure.error));
|
return Err(failure.error);
|
||||||
}
|
}
|
||||||
tracing::warn!(
|
tracing::warn!(
|
||||||
operation,
|
operation,
|
||||||
@@ -474,10 +300,7 @@ where
|
|||||||
tokio::select! {
|
tokio::select! {
|
||||||
biased;
|
biased;
|
||||||
_ = cancel.cancelled() => {
|
_ = cancel.cancelled() => {
|
||||||
return (
|
return Err(KmsError::operation_cancelled(format!("{operation} cancelled during retry backoff")));
|
||||||
Outcome::Cancelled,
|
|
||||||
Err(KmsError::operation_cancelled(format!("{operation} cancelled during retry backoff"))),
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
_ = tokio::time::sleep(backoff) => {}
|
_ = tokio::time::sleep(backoff) => {}
|
||||||
}
|
}
|
||||||
@@ -805,313 +628,4 @@ mod tests {
|
|||||||
assert_eq!(classify_vaultrs(&ClientError::ResponseEmptyError), ErrorClass::Fatal);
|
assert_eq!(classify_vaultrs(&ClientError::ResponseEmptyError), ErrorClass::Fatal);
|
||||||
assert_eq!(classify_vaultrs(&ClientError::InvalidLoginMethodError), ErrorClass::Fatal);
|
assert_eq!(classify_vaultrs(&ClientError::InvalidLoginMethodError), ErrorClass::Fatal);
|
||||||
}
|
}
|
||||||
|
|
||||||
// -- Metric emission ----------------------------------------------------
|
|
||||||
//
|
|
||||||
// Each test installs a thread-local debugging recorder and drives a
|
|
||||||
// paused-clock current-thread runtime inside it, so the emitted metrics
|
|
||||||
// (including virtual-clock durations) are fully deterministic.
|
|
||||||
|
|
||||||
use metrics_util::MetricKind;
|
|
||||||
use metrics_util::debugging::{DebugValue, DebuggingRecorder};
|
|
||||||
|
|
||||||
type MetricEntry = (
|
|
||||||
metrics_util::CompositeKey,
|
|
||||||
Option<metrics::Unit>,
|
|
||||||
Option<metrics::SharedString>,
|
|
||||||
DebugValue,
|
|
||||||
);
|
|
||||||
|
|
||||||
/// Run `test` on a paused current-thread runtime under a debugging
|
|
||||||
/// recorder and return one snapshot of everything it emitted.
|
|
||||||
///
|
|
||||||
/// A single snapshot per test on purpose: `Snapshotter::snapshot` drains
|
|
||||||
/// the recorded state, so taking it per assertion would only show the
|
|
||||||
/// first assertion any data.
|
|
||||||
fn record_metrics<Out>(test: impl FnOnce() -> std::pin::Pin<Box<dyn Future<Output = Out>>>) -> (Vec<MetricEntry>, Out) {
|
|
||||||
let recorder = DebuggingRecorder::new();
|
|
||||||
let snapshotter = recorder.snapshotter();
|
|
||||||
let out = metrics::with_local_recorder(&recorder, || {
|
|
||||||
let runtime = tokio::runtime::Builder::new_current_thread()
|
|
||||||
.enable_time()
|
|
||||||
.start_paused(true)
|
|
||||||
.build()
|
|
||||||
.expect("current-thread runtime must build");
|
|
||||||
runtime.block_on(test())
|
|
||||||
});
|
|
||||||
(snapshotter.snapshot().into_vec(), out)
|
|
||||||
}
|
|
||||||
|
|
||||||
fn labels_match(key: &metrics::Key, labels: &[(&str, &str)]) -> bool {
|
|
||||||
labels
|
|
||||||
.iter()
|
|
||||||
.all(|(label, expected)| key.labels().any(|l| l.key() == *label && l.value() == *expected))
|
|
||||||
}
|
|
||||||
|
|
||||||
fn counter_value(snapshot: &[MetricEntry], name: &str, labels: &[(&str, &str)]) -> u64 {
|
|
||||||
snapshot
|
|
||||||
.iter()
|
|
||||||
.filter_map(|(composite, _unit, _description, value)| {
|
|
||||||
let matches = composite.kind() == MetricKind::Counter
|
|
||||||
&& composite.key().name() == name
|
|
||||||
&& labels_match(composite.key(), labels);
|
|
||||||
match (matches, value) {
|
|
||||||
(true, DebugValue::Counter(count)) => Some(*count),
|
|
||||||
_ => None,
|
|
||||||
}
|
|
||||||
})
|
|
||||||
.sum()
|
|
||||||
}
|
|
||||||
|
|
||||||
fn histogram_values(snapshot: &[MetricEntry], name: &str, labels: &[(&str, &str)]) -> Vec<f64> {
|
|
||||||
snapshot
|
|
||||||
.iter()
|
|
||||||
.filter_map(|(composite, _unit, _description, value)| {
|
|
||||||
let matches = composite.kind() == MetricKind::Histogram
|
|
||||||
&& composite.key().name() == name
|
|
||||||
&& labels_match(composite.key(), labels);
|
|
||||||
match (matches, value) {
|
|
||||||
(true, DebugValue::Histogram(values)) => Some(values),
|
|
||||||
_ => None,
|
|
||||||
}
|
|
||||||
})
|
|
||||||
.flatten()
|
|
||||||
.map(|value| value.into_inner())
|
|
||||||
.collect()
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn metrics_record_retried_success_with_attempts_and_duration() {
|
|
||||||
let calls_in_test = Arc::new(AtomicU32::new(0));
|
|
||||||
let (snapshot, ()) = record_metrics(move || {
|
|
||||||
Box::pin(async move {
|
|
||||||
let policy = policy_of(1_000, 60_000, 3, 100, 2_000);
|
|
||||||
let cancel = CancellationToken::new();
|
|
||||||
let calls_in_attempt = calls_in_test.clone();
|
|
||||||
execute_with_jitter("metrics_read", OpClass::ReadIdempotent, &policy, &cancel, full_jitter, move || {
|
|
||||||
let calls = calls_in_attempt.clone();
|
|
||||||
async move {
|
|
||||||
if calls.fetch_add(1, Ordering::SeqCst) < 2 {
|
|
||||||
Err(AttemptError {
|
|
||||||
class: ErrorClass::RetryableStatus,
|
|
||||||
error: KmsError::backend_error("throttled (429)"),
|
|
||||||
})
|
|
||||||
} else {
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
}
|
|
||||||
})
|
|
||||||
.await
|
|
||||||
.expect("retries within budget must succeed");
|
|
||||||
})
|
|
||||||
});
|
|
||||||
|
|
||||||
assert_eq!(
|
|
||||||
counter_value(
|
|
||||||
&snapshot,
|
|
||||||
METRIC_OPERATIONS_TOTAL,
|
|
||||||
&[
|
|
||||||
("operation", "metrics_read"),
|
|
||||||
("op_class", "read_idempotent"),
|
|
||||||
("outcome", "success")
|
|
||||||
]
|
|
||||||
),
|
|
||||||
1
|
|
||||||
);
|
|
||||||
assert_eq!(
|
|
||||||
counter_value(
|
|
||||||
&snapshot,
|
|
||||||
METRIC_ATTEMPT_FAILURES_TOTAL,
|
|
||||||
&[("operation", "metrics_read"), ("error_class", "retryable_status")]
|
|
||||||
),
|
|
||||||
2
|
|
||||||
);
|
|
||||||
assert_eq!(
|
|
||||||
histogram_values(
|
|
||||||
&snapshot,
|
|
||||||
METRIC_OPERATION_ATTEMPTS,
|
|
||||||
&[("operation", "metrics_read"), ("outcome", "success")]
|
|
||||||
),
|
|
||||||
vec![3.0]
|
|
||||||
);
|
|
||||||
// Full-cap backoffs of 100ms and 200ms on the paused clock.
|
|
||||||
let durations = histogram_values(
|
|
||||||
&snapshot,
|
|
||||||
METRIC_OPERATION_DURATION_SECONDS,
|
|
||||||
&[("operation", "metrics_read"), ("outcome", "success")],
|
|
||||||
);
|
|
||||||
assert_eq!(durations.len(), 1);
|
|
||||||
assert!((durations[0] - 0.3).abs() < 1e-9, "expected 0.3s of virtual backoff, got {durations:?}");
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn metrics_record_fatal_outcome_with_single_attempt() {
|
|
||||||
let (snapshot, ()) = record_metrics(|| {
|
|
||||||
Box::pin(async {
|
|
||||||
let policy = policy_of(1_000, 60_000, 5, 100, 2_000);
|
|
||||||
let cancel = CancellationToken::new();
|
|
||||||
let result: Result<()> =
|
|
||||||
execute_with_jitter("metrics_fatal", OpClass::ReadIdempotent, &policy, &cancel, full_jitter, || async {
|
|
||||||
Err(AttemptError {
|
|
||||||
class: ErrorClass::Fatal,
|
|
||||||
error: KmsError::access_denied("permission denied (403)"),
|
|
||||||
})
|
|
||||||
})
|
|
||||||
.await;
|
|
||||||
result.expect_err("a fatal failure must end the operation");
|
|
||||||
})
|
|
||||||
});
|
|
||||||
|
|
||||||
assert_eq!(
|
|
||||||
counter_value(
|
|
||||||
&snapshot,
|
|
||||||
METRIC_OPERATIONS_TOTAL,
|
|
||||||
&[("operation", "metrics_fatal"), ("outcome", "fatal")]
|
|
||||||
),
|
|
||||||
1
|
|
||||||
);
|
|
||||||
assert_eq!(
|
|
||||||
counter_value(
|
|
||||||
&snapshot,
|
|
||||||
METRIC_ATTEMPT_FAILURES_TOTAL,
|
|
||||||
&[("operation", "metrics_fatal"), ("error_class", "fatal")]
|
|
||||||
),
|
|
||||||
1
|
|
||||||
);
|
|
||||||
assert_eq!(
|
|
||||||
histogram_values(
|
|
||||||
&snapshot,
|
|
||||||
METRIC_OPERATION_ATTEMPTS,
|
|
||||||
&[("operation", "metrics_fatal"), ("outcome", "fatal")]
|
|
||||||
),
|
|
||||||
vec![1.0]
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn metrics_record_mutating_budget_exhausted_after_one_attempt() {
|
|
||||||
let (snapshot, ()) = record_metrics(|| {
|
|
||||||
Box::pin(async {
|
|
||||||
let policy = policy_of(1_000, 60_000, 5, 100, 2_000);
|
|
||||||
let cancel = CancellationToken::new();
|
|
||||||
let result: Result<()> = execute_with_jitter(
|
|
||||||
"metrics_rotate",
|
|
||||||
OpClass::MutatingNonIdempotent,
|
|
||||||
&policy,
|
|
||||||
&cancel,
|
|
||||||
full_jitter,
|
|
||||||
|| async { Err(retryable_conn_error()) },
|
|
||||||
)
|
|
||||||
.await;
|
|
||||||
result.expect_err("a mutating operation must not retry a retryable failure");
|
|
||||||
})
|
|
||||||
});
|
|
||||||
|
|
||||||
assert_eq!(
|
|
||||||
counter_value(
|
|
||||||
&snapshot,
|
|
||||||
METRIC_OPERATIONS_TOTAL,
|
|
||||||
&[
|
|
||||||
("operation", "metrics_rotate"),
|
|
||||||
("op_class", "mutating_non_idempotent"),
|
|
||||||
("outcome", "budget_exhausted")
|
|
||||||
]
|
|
||||||
),
|
|
||||||
1
|
|
||||||
);
|
|
||||||
assert_eq!(
|
|
||||||
histogram_values(
|
|
||||||
&snapshot,
|
|
||||||
METRIC_OPERATION_ATTEMPTS,
|
|
||||||
&[("operation", "metrics_rotate"), ("outcome", "budget_exhausted")]
|
|
||||||
),
|
|
||||||
vec![1.0]
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn metrics_record_timeouts_and_deadline_outcome() {
|
|
||||||
let (snapshot, ()) = record_metrics(|| {
|
|
||||||
Box::pin(async {
|
|
||||||
// Hung attempts: 10s each against a 25s deadline (see
|
|
||||||
// total_duration_never_exceeds_deadline for the timeline).
|
|
||||||
let policy = policy_of(10_000, 25_000, 5, 100, 2_000);
|
|
||||||
let cancel = CancellationToken::new();
|
|
||||||
let result: Result<()> =
|
|
||||||
execute_with_jitter("metrics_hung", OpClass::ReadIdempotent, &policy, &cancel, full_jitter, || {
|
|
||||||
std::future::pending::<AttemptResult<()>>()
|
|
||||||
})
|
|
||||||
.await;
|
|
||||||
result.expect_err("hung attempts must exhaust the deadline");
|
|
||||||
})
|
|
||||||
});
|
|
||||||
|
|
||||||
assert_eq!(
|
|
||||||
counter_value(
|
|
||||||
&snapshot,
|
|
||||||
METRIC_OPERATIONS_TOTAL,
|
|
||||||
&[("operation", "metrics_hung"), ("outcome", "deadline_exceeded")]
|
|
||||||
),
|
|
||||||
1
|
|
||||||
);
|
|
||||||
assert_eq!(
|
|
||||||
counter_value(
|
|
||||||
&snapshot,
|
|
||||||
METRIC_ATTEMPT_FAILURES_TOTAL,
|
|
||||||
&[("operation", "metrics_hung"), ("error_class", "attempt_timeout")]
|
|
||||||
),
|
|
||||||
3
|
|
||||||
);
|
|
||||||
let durations = histogram_values(
|
|
||||||
&snapshot,
|
|
||||||
METRIC_OPERATION_DURATION_SECONDS,
|
|
||||||
&[("operation", "metrics_hung"), ("outcome", "deadline_exceeded")],
|
|
||||||
);
|
|
||||||
assert_eq!(durations.len(), 1);
|
|
||||||
assert!((durations[0] - 25.0).abs() < 1e-9, "expected the full 25s deadline, got {durations:?}");
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn metrics_record_cancelled_outcome() {
|
|
||||||
let calls_in_test = Arc::new(AtomicU32::new(0));
|
|
||||||
let (snapshot, ()) = record_metrics(move || {
|
|
||||||
Box::pin(async move {
|
|
||||||
let policy = policy_of(1_000, 600_000, 5, 10_000, 10_000);
|
|
||||||
let cancel = CancellationToken::new();
|
|
||||||
let canceller = cancel.clone();
|
|
||||||
tokio::spawn(async move {
|
|
||||||
tokio::time::sleep(Duration::from_millis(500)).await;
|
|
||||||
canceller.cancel();
|
|
||||||
});
|
|
||||||
let calls_in_attempt = calls_in_test.clone();
|
|
||||||
let result: Result<()> =
|
|
||||||
execute_with_jitter("metrics_cancel", OpClass::ReadIdempotent, &policy, &cancel, full_jitter, move || {
|
|
||||||
let calls = calls_in_attempt.clone();
|
|
||||||
async move {
|
|
||||||
calls.fetch_add(1, Ordering::SeqCst);
|
|
||||||
Err(retryable_conn_error())
|
|
||||||
}
|
|
||||||
})
|
|
||||||
.await;
|
|
||||||
result.expect_err("cancellation must abort the backoff");
|
|
||||||
})
|
|
||||||
});
|
|
||||||
|
|
||||||
assert_eq!(
|
|
||||||
counter_value(
|
|
||||||
&snapshot,
|
|
||||||
METRIC_OPERATIONS_TOTAL,
|
|
||||||
&[("operation", "metrics_cancel"), ("outcome", "cancelled")]
|
|
||||||
),
|
|
||||||
1
|
|
||||||
);
|
|
||||||
assert_eq!(
|
|
||||||
histogram_values(
|
|
||||||
&snapshot,
|
|
||||||
METRIC_OPERATION_ATTEMPTS,
|
|
||||||
&[("operation", "metrics_cancel"), ("outcome", "cancelled")]
|
|
||||||
),
|
|
||||||
vec![1.0]
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,255 +0,0 @@
|
|||||||
// Copyright 2024 RustFS Team
|
|
||||||
//
|
|
||||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
|
||||||
// you may not use this file except in compliance with the License.
|
|
||||||
// You may obtain a copy of the License at
|
|
||||||
//
|
|
||||||
// http://www.apache.org/licenses/LICENSE-2.0
|
|
||||||
//
|
|
||||||
// Unless required by applicable law or agreed to in writing, software
|
|
||||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
|
||||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
||||||
// See the License for the specific language governing permissions and
|
|
||||||
// limitations under the License.
|
|
||||||
|
|
||||||
//! Fault-injection matrix for the Vault backend operation policy.
|
|
||||||
//!
|
|
||||||
//! Offline cases run against locally injected transport faults (a closed
|
|
||||||
//! port, a listener that never responds) — deterministic, no external
|
|
||||||
//! dependencies. Real-Vault cases are `#[ignore]`d and need a dev Vault
|
|
||||||
//! (default `http://127.0.0.1:8200`, override with `RUSTFS_KMS_VAULT_ADDR`).
|
|
||||||
//!
|
|
||||||
//! Throttling (429) and recoverable 5xx responses cannot be forced on a stock
|
|
||||||
//! dev Vault, so their retry and metric behavior is pinned deterministically
|
|
||||||
//! by the scripted-Vault wiring tests in `backends::vault` and the engine
|
|
||||||
//! tests in `policy.rs`. Pointing `RUSTFS_KMS_VAULT_ADDR` at a
|
|
||||||
//! fault-injecting proxy reuses the ignored cases here unchanged.
|
|
||||||
//!
|
|
||||||
//! Every case installs a thread-local debugging metrics recorder and drives a
|
|
||||||
//! current-thread runtime inside it, so the policy metrics double as the
|
|
||||||
//! request-count assertion even against a real server.
|
|
||||||
|
|
||||||
use std::time::Duration;
|
|
||||||
|
|
||||||
use metrics_util::MetricKind;
|
|
||||||
use metrics_util::debugging::{DebugValue, DebuggingRecorder};
|
|
||||||
use rustfs_kms::backends::KmsClient;
|
|
||||||
use rustfs_kms::backends::vault::VaultKmsClient;
|
|
||||||
use rustfs_kms::{KmsConfig, KmsError, VaultAuthMethod, VaultConfig};
|
|
||||||
|
|
||||||
const OPERATIONS_TOTAL: &str = "rustfs_kms_backend_operations_total";
|
|
||||||
const ATTEMPT_FAILURES_TOTAL: &str = "rustfs_kms_backend_attempt_failures_total";
|
|
||||||
|
|
||||||
fn vault_config(address: &str, token: &str) -> VaultConfig {
|
|
||||||
VaultConfig {
|
|
||||||
address: address.to_string(),
|
|
||||||
auth_method: VaultAuthMethod::Token {
|
|
||||||
token: token.to_string(),
|
|
||||||
},
|
|
||||||
namespace: None,
|
|
||||||
mount_path: "transit".to_string(),
|
|
||||||
kv_mount: "secret".to_string(),
|
|
||||||
key_path_prefix: "rustfs/kms/fault-injection".to_string(),
|
|
||||||
tls: None,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fn kms_config(attempt_timeout: Duration, retry_attempts: u32) -> KmsConfig {
|
|
||||||
KmsConfig {
|
|
||||||
timeout: attempt_timeout,
|
|
||||||
retry_attempts,
|
|
||||||
..KmsConfig::default()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
type MetricEntry = (
|
|
||||||
metrics_util::CompositeKey,
|
|
||||||
Option<metrics::Unit>,
|
|
||||||
Option<metrics::SharedString>,
|
|
||||||
DebugValue,
|
|
||||||
);
|
|
||||||
|
|
||||||
/// Run `test` on a current-thread runtime under a debugging metrics recorder
|
|
||||||
/// and return one snapshot of everything it emitted.
|
|
||||||
///
|
|
||||||
/// A single snapshot per test on purpose: `Snapshotter::snapshot` drains the
|
|
||||||
/// recorded state, so taking it per assertion would only show the first
|
|
||||||
/// assertion any data.
|
|
||||||
fn record_metrics(test: impl FnOnce() -> std::pin::Pin<Box<dyn std::future::Future<Output = ()>>>) -> Vec<MetricEntry> {
|
|
||||||
let recorder = DebuggingRecorder::new();
|
|
||||||
let snapshotter = recorder.snapshotter();
|
|
||||||
metrics::with_local_recorder(&recorder, || {
|
|
||||||
let runtime = tokio::runtime::Builder::new_current_thread()
|
|
||||||
.enable_all()
|
|
||||||
.build()
|
|
||||||
.expect("current-thread runtime must build");
|
|
||||||
runtime.block_on(test());
|
|
||||||
});
|
|
||||||
snapshotter.snapshot().into_vec()
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Sum of counters with `name` whose labels include every `(key, value)` pair.
|
|
||||||
fn counter_value(snapshot: &[MetricEntry], name: &str, labels: &[(&str, &str)]) -> u64 {
|
|
||||||
snapshot
|
|
||||||
.iter()
|
|
||||||
.filter_map(|(composite, _unit, _description, value)| {
|
|
||||||
let key = composite.key();
|
|
||||||
let matches = composite.kind() == MetricKind::Counter
|
|
||||||
&& key.name() == name
|
|
||||||
&& labels
|
|
||||||
.iter()
|
|
||||||
.all(|(label, expected)| key.labels().any(|l| l.key() == *label && l.value() == *expected));
|
|
||||||
match (matches, value) {
|
|
||||||
(true, DebugValue::Counter(count)) => Some(*count),
|
|
||||||
_ => None,
|
|
||||||
}
|
|
||||||
})
|
|
||||||
.sum()
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Connection refused: connection-class failures are retried up to the
|
|
||||||
/// configured budget, then surface as a backend error.
|
|
||||||
#[test]
|
|
||||||
fn connection_refused_is_retried_within_budget() {
|
|
||||||
// Reserve a loopback port and release it so nothing is listening there.
|
|
||||||
let listener = std::net::TcpListener::bind("127.0.0.1:0").expect("reserve a loopback port");
|
|
||||||
let address = format!("http://{}", listener.local_addr().expect("reserved port addr"));
|
|
||||||
drop(listener);
|
|
||||||
|
|
||||||
let snapshot = record_metrics(|| {
|
|
||||||
Box::pin(async move {
|
|
||||||
let client = VaultKmsClient::new(vault_config(&address, "unused"), &kms_config(Duration::from_secs(2), 2))
|
|
||||||
.await
|
|
||||||
.expect("client construction performs no network calls");
|
|
||||||
let error = client
|
|
||||||
.describe_key("fault-injection-refused", None)
|
|
||||||
.await
|
|
||||||
.expect_err("a refused connection must fail the operation");
|
|
||||||
assert!(matches!(error, KmsError::BackendError { .. }), "got {error:?}");
|
|
||||||
})
|
|
||||||
});
|
|
||||||
|
|
||||||
assert_eq!(
|
|
||||||
counter_value(&snapshot, ATTEMPT_FAILURES_TOTAL, &[("error_class", "retryable_conn")]),
|
|
||||||
2,
|
|
||||||
"both budgeted attempts must observe the refused connection"
|
|
||||||
);
|
|
||||||
assert_eq!(counter_value(&snapshot, OPERATIONS_TOTAL, &[("outcome", "budget_exhausted")]), 1);
|
|
||||||
// The static-token login records its own success; the Vault read must not.
|
|
||||||
assert_eq!(
|
|
||||||
counter_value(
|
|
||||||
&snapshot,
|
|
||||||
OPERATIONS_TOTAL,
|
|
||||||
&[("operation", "vault_kv2_read_key"), ("outcome", "success")]
|
|
||||||
),
|
|
||||||
0
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Stalled connection: a server that accepts but never responds is cut off by
|
|
||||||
/// the per-attempt timeout (either the policy timer or the equally sized HTTP
|
|
||||||
/// client timeout, whichever fires first) instead of hanging forever.
|
|
||||||
#[test]
|
|
||||||
fn stalled_connection_is_cut_off_by_the_attempt_timeout() {
|
|
||||||
let snapshot = record_metrics(|| {
|
|
||||||
Box::pin(async move {
|
|
||||||
let listener = tokio::net::TcpListener::bind("127.0.0.1:0")
|
|
||||||
.await
|
|
||||||
.expect("bind stall listener");
|
|
||||||
let address = format!("http://{}", listener.local_addr().expect("stall listener addr"));
|
|
||||||
// Accept and park every connection without ever responding.
|
|
||||||
tokio::spawn(async move {
|
|
||||||
let mut parked = Vec::new();
|
|
||||||
loop {
|
|
||||||
let Ok((socket, _)) = listener.accept().await else { return };
|
|
||||||
parked.push(socket);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
let client = VaultKmsClient::new(vault_config(&address, "unused"), &kms_config(Duration::from_millis(250), 1))
|
|
||||||
.await
|
|
||||||
.expect("client construction performs no network calls");
|
|
||||||
let error = client
|
|
||||||
.describe_key("fault-injection-stalled", None)
|
|
||||||
.await
|
|
||||||
.expect_err("a stalled request must be cut off by the attempt timeout");
|
|
||||||
assert!(
|
|
||||||
matches!(error, KmsError::OperationTimedOut { .. } | KmsError::BackendError { .. }),
|
|
||||||
"got {error:?}"
|
|
||||||
);
|
|
||||||
})
|
|
||||||
});
|
|
||||||
|
|
||||||
// The policy timer reports attempt_timeout; the client-level HTTP timeout
|
|
||||||
// surfaces as a connection-class failure. Either way it is exactly one
|
|
||||||
// attempt that was cut off.
|
|
||||||
let cut_off = counter_value(&snapshot, ATTEMPT_FAILURES_TOTAL, &[("error_class", "attempt_timeout")])
|
|
||||||
+ counter_value(&snapshot, ATTEMPT_FAILURES_TOTAL, &[("error_class", "retryable_conn")]);
|
|
||||||
assert_eq!(cut_off, 1, "the single budgeted attempt must be cut off by a timeout");
|
|
||||||
assert_eq!(counter_value(&snapshot, OPERATIONS_TOTAL, &[("outcome", "budget_exhausted")]), 1);
|
|
||||||
}
|
|
||||||
|
|
||||||
fn real_vault_address() -> String {
|
|
||||||
std::env::var("RUSTFS_KMS_VAULT_ADDR").unwrap_or_else(|_| "http://127.0.0.1:8200".to_string())
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Invalid token against a real Vault: the 403 is fatal — exactly one
|
|
||||||
/// attempt, no retry, and the operation fails closed.
|
|
||||||
#[test]
|
|
||||||
#[ignore] // Requires a running Vault dev server
|
|
||||||
fn real_vault_invalid_token_is_fatal_and_never_retried() {
|
|
||||||
let snapshot = record_metrics(|| {
|
|
||||||
Box::pin(async {
|
|
||||||
let config = vault_config(&real_vault_address(), "fault-injection-invalid-token");
|
|
||||||
let client = VaultKmsClient::new(config, &kms_config(Duration::from_secs(5), 3))
|
|
||||||
.await
|
|
||||||
.expect("client construction performs no network calls");
|
|
||||||
let error = client
|
|
||||||
.describe_key("fault-injection-forbidden", None)
|
|
||||||
.await
|
|
||||||
.expect_err("an invalid token must be rejected");
|
|
||||||
assert!(matches!(error, KmsError::BackendError { .. }), "got {error:?}");
|
|
||||||
})
|
|
||||||
});
|
|
||||||
|
|
||||||
assert_eq!(
|
|
||||||
counter_value(&snapshot, ATTEMPT_FAILURES_TOTAL, &[("error_class", "fatal")]),
|
|
||||||
1,
|
|
||||||
"a 403 must be observed by exactly one attempt"
|
|
||||||
);
|
|
||||||
assert_eq!(counter_value(&snapshot, OPERATIONS_TOTAL, &[("outcome", "fatal")]), 1);
|
|
||||||
assert_eq!(
|
|
||||||
counter_value(&snapshot, ATTEMPT_FAILURES_TOTAL, &[("error_class", "retryable_status")]),
|
|
||||||
0,
|
|
||||||
"an auth failure must never be classified as retryable"
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Healthy read against a real Vault: a missing key resolves in one attempt
|
|
||||||
/// (404 is fatal for retry purposes) and records a fatal outcome rather than
|
|
||||||
/// burning the retry budget.
|
|
||||||
#[test]
|
|
||||||
#[ignore] // Requires a running Vault dev server
|
|
||||||
fn real_vault_missing_key_is_resolved_in_one_attempt() {
|
|
||||||
let token = std::env::var("RUSTFS_KMS_VAULT_TOKEN").unwrap_or_else(|_| "dev-only-token".to_string());
|
|
||||||
let snapshot = record_metrics(|| {
|
|
||||||
Box::pin(async move {
|
|
||||||
let config = vault_config(&real_vault_address(), &token);
|
|
||||||
let client = VaultKmsClient::new(config, &kms_config(Duration::from_secs(5), 3))
|
|
||||||
.await
|
|
||||||
.expect("client construction performs no network calls");
|
|
||||||
let error = client
|
|
||||||
.describe_key("fault-injection-definitely-missing", None)
|
|
||||||
.await
|
|
||||||
.expect_err("a missing key must resolve to key-not-found");
|
|
||||||
assert!(matches!(error, KmsError::KeyNotFound { .. }), "got {error:?}");
|
|
||||||
})
|
|
||||||
});
|
|
||||||
|
|
||||||
assert_eq!(
|
|
||||||
counter_value(&snapshot, ATTEMPT_FAILURES_TOTAL, &[("error_class", "fatal")]),
|
|
||||||
1,
|
|
||||||
"a 404 must be observed by exactly one attempt"
|
|
||||||
);
|
|
||||||
assert_eq!(counter_value(&snapshot, OPERATIONS_TOTAL, &[("outcome", "fatal")]), 1);
|
|
||||||
}
|
|
||||||
@@ -25,35 +25,7 @@ keywords = ["lifecycle", "storage", "rustfs", "Minio"]
|
|||||||
categories = ["web-programming", "development-tools", "filesystem"]
|
categories = ["web-programming", "development-tools", "filesystem"]
|
||||||
documentation = "https://docs.rs/rustfs-lifecycle/latest/rustfs_lifecycle/"
|
documentation = "https://docs.rs/rustfs-lifecycle/latest/rustfs_lifecycle/"
|
||||||
|
|
||||||
[features]
|
|
||||||
default = []
|
|
||||||
hotpath = [
|
|
||||||
"hotpath/hotpath",
|
|
||||||
"hotpath/tokio",
|
|
||||||
"rustfs-common/hotpath",
|
|
||||||
"rustfs-config/hotpath",
|
|
||||||
"rustfs-replication/hotpath",
|
|
||||||
"rustfs-storage-api/hotpath",
|
|
||||||
]
|
|
||||||
hotpath-alloc = [
|
|
||||||
"hotpath",
|
|
||||||
"hotpath/hotpath-alloc",
|
|
||||||
"rustfs-common/hotpath-alloc",
|
|
||||||
"rustfs-config/hotpath-alloc",
|
|
||||||
"rustfs-replication/hotpath-alloc",
|
|
||||||
"rustfs-storage-api/hotpath-alloc",
|
|
||||||
]
|
|
||||||
hotpath-cpu = [
|
|
||||||
"hotpath",
|
|
||||||
"hotpath/hotpath-cpu",
|
|
||||||
"rustfs-common/hotpath-cpu",
|
|
||||||
"rustfs-config/hotpath-cpu",
|
|
||||||
"rustfs-replication/hotpath-cpu",
|
|
||||||
"rustfs-storage-api/hotpath-cpu",
|
|
||||||
]
|
|
||||||
|
|
||||||
[dependencies]
|
[dependencies]
|
||||||
hotpath.workspace = true
|
|
||||||
async-trait.workspace = true
|
async-trait.workspace = true
|
||||||
metrics.workspace = true
|
metrics.workspace = true
|
||||||
rustfs-common.workspace = true
|
rustfs-common.workspace = true
|
||||||
|
|||||||
@@ -28,21 +28,7 @@ documentation = "https://docs.rs/rustfs-lock/latest/rustfs_lock/"
|
|||||||
[lints]
|
[lints]
|
||||||
workspace = true
|
workspace = true
|
||||||
|
|
||||||
[features]
|
|
||||||
default = []
|
|
||||||
hotpath = [
|
|
||||||
"hotpath/hotpath",
|
|
||||||
"hotpath/tokio",
|
|
||||||
"hotpath/futures",
|
|
||||||
"hotpath/parking_lot",
|
|
||||||
"rustfs-io-metrics/hotpath",
|
|
||||||
"rustfs-utils/hotpath",
|
|
||||||
]
|
|
||||||
hotpath-alloc = ["hotpath", "hotpath/hotpath-alloc", "rustfs-io-metrics/hotpath-alloc", "rustfs-utils/hotpath-alloc"]
|
|
||||||
hotpath-cpu = ["hotpath", "hotpath/hotpath-cpu", "rustfs-io-metrics/hotpath-cpu", "rustfs-utils/hotpath-cpu"]
|
|
||||||
|
|
||||||
[dependencies]
|
[dependencies]
|
||||||
hotpath.workspace = true
|
|
||||||
rustfs-io-metrics = { workspace = true }
|
rustfs-io-metrics = { workspace = true }
|
||||||
rustfs-utils = { workspace = true }
|
rustfs-utils = { workspace = true }
|
||||||
async-trait.workspace = true
|
async-trait.workspace = true
|
||||||
|
|||||||
@@ -540,7 +540,6 @@ impl DistributedLock {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// Acquire a lock and return a RAII guard
|
/// Acquire a lock and return a RAII guard
|
||||||
#[hotpath::measure]
|
|
||||||
pub(crate) async fn acquire_guard(&self, request: &LockRequest) -> Result<Option<DistributedLockGuard>> {
|
pub(crate) async fn acquire_guard(&self, request: &LockRequest) -> Result<Option<DistributedLockGuard>> {
|
||||||
if self.clients.is_empty() {
|
if self.clients.is_empty() {
|
||||||
return Err(LockError::internal("No lock clients available"));
|
return Err(LockError::internal("No lock clients available"));
|
||||||
@@ -627,7 +626,6 @@ impl DistributedLock {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// Convenience: acquire exclusive lock as a guard
|
/// Convenience: acquire exclusive lock as a guard
|
||||||
#[hotpath::measure]
|
|
||||||
pub async fn lock_guard(
|
pub async fn lock_guard(
|
||||||
&self,
|
&self,
|
||||||
resource: ObjectKey,
|
resource: ObjectKey,
|
||||||
@@ -642,7 +640,6 @@ impl DistributedLock {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// Convenience: acquire exclusive lock with expected contention logs suppressed
|
/// Convenience: acquire exclusive lock with expected contention logs suppressed
|
||||||
#[hotpath::measure]
|
|
||||||
pub async fn lock_guard_quiet(
|
pub async fn lock_guard_quiet(
|
||||||
&self,
|
&self,
|
||||||
resource: ObjectKey,
|
resource: ObjectKey,
|
||||||
@@ -658,7 +655,6 @@ impl DistributedLock {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// Convenience: acquire shared lock as a guard
|
/// Convenience: acquire shared lock as a guard
|
||||||
#[hotpath::measure]
|
|
||||||
pub async fn rlock_guard(
|
pub async fn rlock_guard(
|
||||||
&self,
|
&self,
|
||||||
resource: ObjectKey,
|
resource: ObjectKey,
|
||||||
|
|||||||
@@ -32,14 +32,7 @@ doctest = false
|
|||||||
name = "la-dump-anchors"
|
name = "la-dump-anchors"
|
||||||
path = "src/bin/la_dump_anchors.rs"
|
path = "src/bin/la_dump_anchors.rs"
|
||||||
|
|
||||||
[features]
|
|
||||||
default = []
|
|
||||||
hotpath = ["hotpath/hotpath"]
|
|
||||||
hotpath-alloc = ["hotpath", "hotpath/hotpath-alloc"]
|
|
||||||
hotpath-cpu = ["hotpath", "hotpath/hotpath-cpu"]
|
|
||||||
|
|
||||||
[dependencies]
|
[dependencies]
|
||||||
hotpath.workspace = true
|
|
||||||
chrono = { workspace = true, features = ["serde"] }
|
chrono = { workspace = true, features = ["serde"] }
|
||||||
flate2 = { workspace = true }
|
flate2 = { workspace = true }
|
||||||
regex = { workspace = true }
|
regex = { workspace = true }
|
||||||
|
|||||||
@@ -28,14 +28,7 @@ documentation = "https://docs.rs/rustfs-madmin/latest/rustfs_madmin/"
|
|||||||
[lints]
|
[lints]
|
||||||
workspace = true
|
workspace = true
|
||||||
|
|
||||||
[features]
|
|
||||||
default = []
|
|
||||||
hotpath = ["hotpath/hotpath"]
|
|
||||||
hotpath-alloc = ["hotpath", "hotpath/hotpath-alloc"]
|
|
||||||
hotpath-cpu = ["hotpath", "hotpath/hotpath-cpu"]
|
|
||||||
|
|
||||||
[dependencies]
|
[dependencies]
|
||||||
hotpath.workspace = true
|
|
||||||
chrono = { workspace = true, features = ["serde"] }
|
chrono = { workspace = true, features = ["serde"] }
|
||||||
humantime.workspace = true
|
humantime.workspace = true
|
||||||
hyper = { workspace = true, features = ["http2", "http1", "server"] }
|
hyper = { workspace = true, features = ["http2", "http1", "server"] }
|
||||||
|
|||||||
@@ -26,40 +26,9 @@ categories = ["web-programming", "development-tools", "filesystem"]
|
|||||||
documentation = "https://docs.rs/rustfs-notify/latest/rustfs_notify/"
|
documentation = "https://docs.rs/rustfs-notify/latest/rustfs_notify/"
|
||||||
|
|
||||||
[features]
|
[features]
|
||||||
hotpath = [
|
|
||||||
"hotpath/hotpath",
|
|
||||||
"hotpath/tokio",
|
|
||||||
"rustfs-config/hotpath",
|
|
||||||
"rustfs-ecstore/hotpath",
|
|
||||||
"rustfs-s3-ops/hotpath",
|
|
||||||
"rustfs-s3-types/hotpath",
|
|
||||||
"rustfs-targets/hotpath",
|
|
||||||
"rustfs-utils/hotpath",
|
|
||||||
]
|
|
||||||
hotpath-alloc = [
|
|
||||||
"hotpath",
|
|
||||||
"hotpath/hotpath-alloc",
|
|
||||||
"rustfs-config/hotpath-alloc",
|
|
||||||
"rustfs-ecstore/hotpath-alloc",
|
|
||||||
"rustfs-s3-ops/hotpath-alloc",
|
|
||||||
"rustfs-s3-types/hotpath-alloc",
|
|
||||||
"rustfs-targets/hotpath-alloc",
|
|
||||||
"rustfs-utils/hotpath-alloc",
|
|
||||||
]
|
|
||||||
hotpath-cpu = [
|
|
||||||
"hotpath",
|
|
||||||
"hotpath/hotpath-cpu",
|
|
||||||
"rustfs-config/hotpath-cpu",
|
|
||||||
"rustfs-ecstore/hotpath-cpu",
|
|
||||||
"rustfs-s3-ops/hotpath-cpu",
|
|
||||||
"rustfs-s3-types/hotpath-cpu",
|
|
||||||
"rustfs-targets/hotpath-cpu",
|
|
||||||
"rustfs-utils/hotpath-cpu",
|
|
||||||
]
|
|
||||||
demo-examples = []
|
demo-examples = []
|
||||||
|
|
||||||
[dependencies]
|
[dependencies]
|
||||||
hotpath.workspace = true
|
|
||||||
rustfs-config = { workspace = true, features = ["notify", "server-config-model"] }
|
rustfs-config = { workspace = true, features = ["notify", "server-config-model"] }
|
||||||
rustfs-ecstore = { workspace = true }
|
rustfs-ecstore = { workspace = true }
|
||||||
rustfs-s3-types = { workspace = true }
|
rustfs-s3-types = { workspace = true }
|
||||||
|
|||||||
@@ -34,33 +34,7 @@ harness = false
|
|||||||
[lints]
|
[lints]
|
||||||
workspace = true
|
workspace = true
|
||||||
|
|
||||||
[features]
|
|
||||||
default = []
|
|
||||||
hotpath = [
|
|
||||||
"hotpath/hotpath",
|
|
||||||
"hotpath/tokio",
|
|
||||||
"hotpath/futures",
|
|
||||||
"rustfs-config/hotpath",
|
|
||||||
"rustfs-io-metrics/hotpath",
|
|
||||||
"rustfs-utils/hotpath",
|
|
||||||
]
|
|
||||||
hotpath-alloc = [
|
|
||||||
"hotpath",
|
|
||||||
"hotpath/hotpath-alloc",
|
|
||||||
"rustfs-config/hotpath-alloc",
|
|
||||||
"rustfs-io-metrics/hotpath-alloc",
|
|
||||||
"rustfs-utils/hotpath-alloc",
|
|
||||||
]
|
|
||||||
hotpath-cpu = [
|
|
||||||
"hotpath",
|
|
||||||
"hotpath/hotpath-cpu",
|
|
||||||
"rustfs-config/hotpath-cpu",
|
|
||||||
"rustfs-io-metrics/hotpath-cpu",
|
|
||||||
"rustfs-utils/hotpath-cpu",
|
|
||||||
]
|
|
||||||
|
|
||||||
[dependencies]
|
[dependencies]
|
||||||
hotpath.workspace = true
|
|
||||||
rustfs-config = { workspace = true, features = ["constants"] }
|
rustfs-config = { workspace = true, features = ["constants"] }
|
||||||
rustfs-io-metrics = { workspace = true }
|
rustfs-io-metrics = { workspace = true }
|
||||||
rustfs-utils = { workspace = true, features = ["os"] }
|
rustfs-utils = { workspace = true, features = ["os"] }
|
||||||
|
|||||||
@@ -1035,7 +1035,6 @@ impl HybridCapacityManager {
|
|||||||
///
|
///
|
||||||
/// Joiners subscribe to the watch channel *before* releasing the mutex, which guarantees
|
/// Joiners subscribe to the watch channel *before* releasing the mutex, which guarantees
|
||||||
/// they cannot miss the completion notification even if the leader finishes very quickly.
|
/// they cannot miss the completion notification even if the leader finishes very quickly.
|
||||||
#[hotpath::measure]
|
|
||||||
pub async fn refresh_or_join<F, Fut>(&self, source: DataSource, refresh_fn: F) -> Result<CapacityUpdate, String>
|
pub async fn refresh_or_join<F, Fut>(&self, source: DataSource, refresh_fn: F) -> Result<CapacityUpdate, String>
|
||||||
where
|
where
|
||||||
F: FnOnce() -> Fut,
|
F: FnOnce() -> Fut,
|
||||||
@@ -1143,7 +1142,6 @@ impl HybridCapacityManager {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// Start a background refresh if one is not already in flight.
|
/// Start a background refresh if one is not already in flight.
|
||||||
#[hotpath::measure]
|
|
||||||
pub async fn spawn_refresh_if_needed<F, Fut>(self: Arc<Self>, source: DataSource, refresh_fn: F) -> bool
|
pub async fn spawn_refresh_if_needed<F, Fut>(self: Arc<Self>, source: DataSource, refresh_fn: F) -> bool
|
||||||
where
|
where
|
||||||
F: FnOnce() -> Fut + Send + 'static,
|
F: FnOnce() -> Fut + Send + 'static,
|
||||||
|
|||||||
@@ -338,7 +338,6 @@ pub async fn select_capacity_refresh_disks(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#[hotpath::measure]
|
|
||||||
pub async fn refresh_capacity_with_scope(disks: Vec<CapacityDiskRef>, dirty_subset: bool) -> Result<CapacityUpdate, String> {
|
pub async fn refresh_capacity_with_scope(disks: Vec<CapacityDiskRef>, dirty_subset: bool) -> Result<CapacityUpdate, String> {
|
||||||
let scan_started_at = Instant::now();
|
let scan_started_at = Instant::now();
|
||||||
let report = calculate_data_dir_used_capacity_report(&disks)
|
let report = calculate_data_dir_used_capacity_report(&disks)
|
||||||
|
|||||||
@@ -27,14 +27,7 @@ categories = ["web-programming", "development-tools"]
|
|||||||
[lib]
|
[lib]
|
||||||
doctest = false
|
doctest = false
|
||||||
|
|
||||||
[features]
|
|
||||||
default = []
|
|
||||||
hotpath = ["hotpath/hotpath", "hotpath/tokio"]
|
|
||||||
hotpath-alloc = ["hotpath", "hotpath/hotpath-alloc"]
|
|
||||||
hotpath-cpu = ["hotpath", "hotpath/hotpath-cpu"]
|
|
||||||
|
|
||||||
[dependencies]
|
[dependencies]
|
||||||
hotpath.workspace = true
|
|
||||||
bytes = { workspace = true, features = ["serde"] }
|
bytes = { workspace = true, features = ["serde"] }
|
||||||
metrics = { workspace = true }
|
metrics = { workspace = true }
|
||||||
moka = { workspace = true, features = ["future"] }
|
moka = { workspace = true, features = ["future"] }
|
||||||
|
|||||||
@@ -27,50 +27,6 @@ documentation = "https://docs.rs/rustfs-obs/latest/rustfs_obs/"
|
|||||||
|
|
||||||
[features]
|
[features]
|
||||||
default = []
|
default = []
|
||||||
hotpath = [
|
|
||||||
"hotpath/hotpath",
|
|
||||||
"hotpath/tokio",
|
|
||||||
"hotpath/futures",
|
|
||||||
"hotpath/crossbeam",
|
|
||||||
"rustfs-audit/hotpath",
|
|
||||||
"rustfs-common/hotpath",
|
|
||||||
"rustfs-config/hotpath",
|
|
||||||
"rustfs-ecstore/hotpath",
|
|
||||||
"rustfs-iam/hotpath",
|
|
||||||
"rustfs-io-metrics/hotpath",
|
|
||||||
"rustfs-notify/hotpath",
|
|
||||||
"rustfs-security-governance/hotpath",
|
|
||||||
"rustfs-storage-api/hotpath",
|
|
||||||
"rustfs-utils/hotpath",
|
|
||||||
]
|
|
||||||
hotpath-alloc = [
|
|
||||||
"hotpath",
|
|
||||||
"hotpath/hotpath-alloc",
|
|
||||||
"rustfs-audit/hotpath-alloc",
|
|
||||||
"rustfs-common/hotpath-alloc",
|
|
||||||
"rustfs-config/hotpath-alloc",
|
|
||||||
"rustfs-ecstore/hotpath-alloc",
|
|
||||||
"rustfs-iam/hotpath-alloc",
|
|
||||||
"rustfs-io-metrics/hotpath-alloc",
|
|
||||||
"rustfs-notify/hotpath-alloc",
|
|
||||||
"rustfs-security-governance/hotpath-alloc",
|
|
||||||
"rustfs-storage-api/hotpath-alloc",
|
|
||||||
"rustfs-utils/hotpath-alloc",
|
|
||||||
]
|
|
||||||
hotpath-cpu = [
|
|
||||||
"hotpath",
|
|
||||||
"hotpath/hotpath-cpu",
|
|
||||||
"rustfs-audit/hotpath-cpu",
|
|
||||||
"rustfs-common/hotpath-cpu",
|
|
||||||
"rustfs-config/hotpath-cpu",
|
|
||||||
"rustfs-ecstore/hotpath-cpu",
|
|
||||||
"rustfs-iam/hotpath-cpu",
|
|
||||||
"rustfs-io-metrics/hotpath-cpu",
|
|
||||||
"rustfs-notify/hotpath-cpu",
|
|
||||||
"rustfs-security-governance/hotpath-cpu",
|
|
||||||
"rustfs-storage-api/hotpath-cpu",
|
|
||||||
"rustfs-utils/hotpath-cpu",
|
|
||||||
]
|
|
||||||
# Tokio runtime-level telemetry. Requires a `--cfg tokio_unstable` build; the
|
# Tokio runtime-level telemetry. Requires a `--cfg tokio_unstable` build; the
|
||||||
# build script fails the compile when that flag is missing. Off by default so
|
# build script fails the compile when that flag is missing. Off by default so
|
||||||
# ordinary builds neither pay for nor depend on Tokio's unstable API.
|
# ordinary builds neither pay for nor depend on Tokio's unstable API.
|
||||||
@@ -102,7 +58,6 @@ required-features = ["dial9"]
|
|||||||
workspace = true
|
workspace = true
|
||||||
|
|
||||||
[dependencies]
|
[dependencies]
|
||||||
hotpath.workspace = true
|
|
||||||
rustfs-audit = { workspace = true }
|
rustfs-audit = { workspace = true }
|
||||||
rustfs-common = { workspace = true }
|
rustfs-common = { workspace = true }
|
||||||
rustfs-config = { workspace = true, features = ["observability"] }
|
rustfs-config = { workspace = true, features = ["observability"] }
|
||||||
|
|||||||
@@ -28,30 +28,6 @@ documentation = "https://docs.rs/rustfs-policy/latest/rustfs_policy/"
|
|||||||
[lints]
|
[lints]
|
||||||
workspace = true
|
workspace = true
|
||||||
|
|
||||||
[features]
|
|
||||||
default = []
|
|
||||||
hotpath = [
|
|
||||||
"hotpath/hotpath",
|
|
||||||
"hotpath/reqwest-0-13",
|
|
||||||
"rustfs-config/hotpath",
|
|
||||||
"rustfs-credentials/hotpath",
|
|
||||||
"rustfs-crypto/hotpath",
|
|
||||||
]
|
|
||||||
hotpath-alloc = [
|
|
||||||
"hotpath",
|
|
||||||
"hotpath/hotpath-alloc",
|
|
||||||
"rustfs-config/hotpath-alloc",
|
|
||||||
"rustfs-credentials/hotpath-alloc",
|
|
||||||
"rustfs-crypto/hotpath-alloc",
|
|
||||||
]
|
|
||||||
hotpath-cpu = [
|
|
||||||
"hotpath",
|
|
||||||
"hotpath/hotpath-cpu",
|
|
||||||
"rustfs-config/hotpath-cpu",
|
|
||||||
"rustfs-credentials/hotpath-cpu",
|
|
||||||
"rustfs-crypto/hotpath-cpu",
|
|
||||||
]
|
|
||||||
|
|
||||||
[dependencies]
|
[dependencies]
|
||||||
rustfs-credentials = { workspace = true }
|
rustfs-credentials = { workspace = true }
|
||||||
rustfs-config = { workspace = true, features = ["opa"] }
|
rustfs-config = { workspace = true, features = ["opa"] }
|
||||||
@@ -73,7 +49,6 @@ moka = { workspace = true, features = ["future"] }
|
|||||||
async-trait.workspace = true
|
async-trait.workspace = true
|
||||||
futures.workspace = true
|
futures.workspace = true
|
||||||
pollster.workspace = true
|
pollster.workspace = true
|
||||||
hotpath.workspace = true
|
|
||||||
|
|
||||||
[dev-dependencies]
|
[dev-dependencies]
|
||||||
pollster.workspace = true
|
pollster.workspace = true
|
||||||
|
|||||||
@@ -32,15 +32,10 @@ impl Args {
|
|||||||
|
|
||||||
#[derive(Debug, Clone)]
|
#[derive(Debug, Clone)]
|
||||||
pub struct AuthZPlugin {
|
pub struct AuthZPlugin {
|
||||||
client: OpaHttpClient,
|
client: reqwest::Client,
|
||||||
args: Args,
|
args: Args,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[cfg(feature = "hotpath")]
|
|
||||||
type OpaHttpClient = hotpath::wrap::reqwest::Client;
|
|
||||||
#[cfg(not(feature = "hotpath"))]
|
|
||||||
type OpaHttpClient = reqwest::Client;
|
|
||||||
|
|
||||||
#[derive(Debug, thiserror::Error)]
|
#[derive(Debug, thiserror::Error)]
|
||||||
pub enum OpaConfigError {
|
pub enum OpaConfigError {
|
||||||
#[error("Missing required env var: {0}")]
|
#[error("Missing required env var: {0}")]
|
||||||
@@ -146,9 +141,6 @@ impl AuthZPlugin {
|
|||||||
reqwest::Client::new()
|
reqwest::Client::new()
|
||||||
});
|
});
|
||||||
|
|
||||||
#[cfg(feature = "hotpath")]
|
|
||||||
let client = hotpath::http!(client, label = "Policy::OPA");
|
|
||||||
|
|
||||||
Self { client, args: config }
|
Self { client, args: config }
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -30,55 +30,6 @@ workspace = true
|
|||||||
|
|
||||||
[features]
|
[features]
|
||||||
default = []
|
default = []
|
||||||
hotpath = [
|
|
||||||
"hotpath/hotpath",
|
|
||||||
"hotpath/tokio",
|
|
||||||
"hotpath/futures",
|
|
||||||
"rustfs-config/hotpath",
|
|
||||||
"rustfs-credentials/hotpath",
|
|
||||||
"rustfs-ecstore?/hotpath",
|
|
||||||
"rustfs-iam/hotpath",
|
|
||||||
"rustfs-keystone?/hotpath",
|
|
||||||
"rustfs-policy/hotpath",
|
|
||||||
"rustfs-rio?/hotpath",
|
|
||||||
"rustfs-storage-api/hotpath",
|
|
||||||
"rustfs-tls-runtime?/hotpath",
|
|
||||||
"rustfs-trusted-proxies?/hotpath",
|
|
||||||
"rustfs-utils/hotpath",
|
|
||||||
"rustfs-test-utils/hotpath",
|
|
||||||
]
|
|
||||||
hotpath-alloc = [
|
|
||||||
"hotpath",
|
|
||||||
"hotpath/hotpath-alloc",
|
|
||||||
"rustfs-config/hotpath-alloc",
|
|
||||||
"rustfs-credentials/hotpath-alloc",
|
|
||||||
"rustfs-ecstore?/hotpath-alloc",
|
|
||||||
"rustfs-iam/hotpath-alloc",
|
|
||||||
"rustfs-keystone?/hotpath-alloc",
|
|
||||||
"rustfs-policy/hotpath-alloc",
|
|
||||||
"rustfs-rio?/hotpath-alloc",
|
|
||||||
"rustfs-storage-api/hotpath-alloc",
|
|
||||||
"rustfs-tls-runtime?/hotpath-alloc",
|
|
||||||
"rustfs-trusted-proxies?/hotpath-alloc",
|
|
||||||
"rustfs-utils/hotpath-alloc",
|
|
||||||
"rustfs-test-utils/hotpath-alloc",
|
|
||||||
]
|
|
||||||
hotpath-cpu = [
|
|
||||||
"hotpath",
|
|
||||||
"hotpath/hotpath-cpu",
|
|
||||||
"rustfs-config/hotpath-cpu",
|
|
||||||
"rustfs-credentials/hotpath-cpu",
|
|
||||||
"rustfs-ecstore?/hotpath-cpu",
|
|
||||||
"rustfs-iam/hotpath-cpu",
|
|
||||||
"rustfs-keystone?/hotpath-cpu",
|
|
||||||
"rustfs-policy/hotpath-cpu",
|
|
||||||
"rustfs-rio?/hotpath-cpu",
|
|
||||||
"rustfs-storage-api/hotpath-cpu",
|
|
||||||
"rustfs-tls-runtime?/hotpath-cpu",
|
|
||||||
"rustfs-trusted-proxies?/hotpath-cpu",
|
|
||||||
"rustfs-utils/hotpath-cpu",
|
|
||||||
"rustfs-test-utils/hotpath-cpu",
|
|
||||||
]
|
|
||||||
ftps = ["dep:libunftp", "dep:unftp-core", "dep:rustls", "dep:rustfs-tls-runtime", "dep:subtle"]
|
ftps = ["dep:libunftp", "dep:unftp-core", "dep:rustls", "dep:rustfs-tls-runtime", "dep:subtle"]
|
||||||
swift = [
|
swift = [
|
||||||
"dep:rustfs-keystone",
|
"dep:rustfs-keystone",
|
||||||
@@ -111,7 +62,6 @@ webdav = ["dep:dav-server", "dep:hyper", "dep:hyper-util", "dep:http-body-util",
|
|||||||
sftp = ["dep:russh", "dep:russh-sftp", "dep:uuid", "dep:subtle", "dep:tokio-util", "dep:socket2"]
|
sftp = ["dep:russh", "dep:russh-sftp", "dep:uuid", "dep:subtle", "dep:tokio-util", "dep:socket2"]
|
||||||
|
|
||||||
[dependencies]
|
[dependencies]
|
||||||
hotpath.workspace = true
|
|
||||||
# Core RustFS dependencies
|
# Core RustFS dependencies
|
||||||
rustfs-iam = { workspace = true }
|
rustfs-iam = { workspace = true }
|
||||||
rustfs-credentials = { workspace = true }
|
rustfs-credentials = { workspace = true }
|
||||||
|
|||||||
@@ -32,38 +32,7 @@ workspace = true
|
|||||||
name = "gproto"
|
name = "gproto"
|
||||||
path = "src/main.rs"
|
path = "src/main.rs"
|
||||||
|
|
||||||
[features]
|
|
||||||
default = []
|
|
||||||
hotpath = [
|
|
||||||
"hotpath/hotpath",
|
|
||||||
"hotpath/tokio",
|
|
||||||
"rustfs-common/hotpath",
|
|
||||||
"rustfs-config/hotpath",
|
|
||||||
"rustfs-io-metrics/hotpath",
|
|
||||||
"rustfs-tls-runtime/hotpath",
|
|
||||||
"rustfs-utils/hotpath",
|
|
||||||
]
|
|
||||||
hotpath-alloc = [
|
|
||||||
"hotpath",
|
|
||||||
"hotpath/hotpath-alloc",
|
|
||||||
"rustfs-common/hotpath-alloc",
|
|
||||||
"rustfs-config/hotpath-alloc",
|
|
||||||
"rustfs-io-metrics/hotpath-alloc",
|
|
||||||
"rustfs-tls-runtime/hotpath-alloc",
|
|
||||||
"rustfs-utils/hotpath-alloc",
|
|
||||||
]
|
|
||||||
hotpath-cpu = [
|
|
||||||
"hotpath",
|
|
||||||
"hotpath/hotpath-cpu",
|
|
||||||
"rustfs-common/hotpath-cpu",
|
|
||||||
"rustfs-config/hotpath-cpu",
|
|
||||||
"rustfs-io-metrics/hotpath-cpu",
|
|
||||||
"rustfs-tls-runtime/hotpath-cpu",
|
|
||||||
"rustfs-utils/hotpath-cpu",
|
|
||||||
]
|
|
||||||
|
|
||||||
[dependencies]
|
[dependencies]
|
||||||
hotpath.workspace = true
|
|
||||||
rustfs-common.workspace = true
|
rustfs-common.workspace = true
|
||||||
rustfs-io-metrics.workspace = true
|
rustfs-io-metrics.workspace = true
|
||||||
rustfs-config.workspace = true
|
rustfs-config.workspace = true
|
||||||
|
|||||||
@@ -25,14 +25,7 @@ keywords = ["replication", "storage", "rustfs", "Minio"]
|
|||||||
categories = ["web-programming", "development-tools", "filesystem"]
|
categories = ["web-programming", "development-tools", "filesystem"]
|
||||||
documentation = "https://docs.rs/rustfs-replication/latest/rustfs_replication/"
|
documentation = "https://docs.rs/rustfs-replication/latest/rustfs_replication/"
|
||||||
|
|
||||||
[features]
|
|
||||||
default = []
|
|
||||||
hotpath = ["hotpath/hotpath", "hotpath/tokio"]
|
|
||||||
hotpath-alloc = ["hotpath", "hotpath/hotpath-alloc"]
|
|
||||||
hotpath-cpu = ["hotpath", "hotpath/hotpath-cpu"]
|
|
||||||
|
|
||||||
[dependencies]
|
[dependencies]
|
||||||
hotpath.workspace = true
|
|
||||||
bytes = { workspace = true, features = ["serde"] }
|
bytes = { workspace = true, features = ["serde"] }
|
||||||
byteorder.workspace = true
|
byteorder.workspace = true
|
||||||
regex.workspace = true
|
regex.workspace = true
|
||||||
|
|||||||
@@ -28,32 +28,7 @@ documentation = "https://docs.rs/rustfs-rio-v2/latest/rustfs_rio_v2/"
|
|||||||
[lints]
|
[lints]
|
||||||
workspace = true
|
workspace = true
|
||||||
|
|
||||||
[features]
|
|
||||||
default = []
|
|
||||||
hotpath = [
|
|
||||||
"hotpath/hotpath",
|
|
||||||
"hotpath/tokio",
|
|
||||||
"rustfs-rio/hotpath",
|
|
||||||
"rustfs-utils/hotpath",
|
|
||||||
"rustfs-filemeta/hotpath",
|
|
||||||
]
|
|
||||||
hotpath-alloc = [
|
|
||||||
"hotpath",
|
|
||||||
"hotpath/hotpath-alloc",
|
|
||||||
"rustfs-rio/hotpath-alloc",
|
|
||||||
"rustfs-utils/hotpath-alloc",
|
|
||||||
"rustfs-filemeta/hotpath-alloc",
|
|
||||||
]
|
|
||||||
hotpath-cpu = [
|
|
||||||
"hotpath",
|
|
||||||
"hotpath/hotpath-cpu",
|
|
||||||
"rustfs-rio/hotpath-cpu",
|
|
||||||
"rustfs-utils/hotpath-cpu",
|
|
||||||
"rustfs-filemeta/hotpath-cpu",
|
|
||||||
]
|
|
||||||
|
|
||||||
[dependencies]
|
[dependencies]
|
||||||
hotpath.workspace = true
|
|
||||||
aes-gcm = { workspace = true, features = ["rand_core"] }
|
aes-gcm = { workspace = true, features = ["rand_core"] }
|
||||||
bytes = { workspace = true, features = ["serde"] }
|
bytes = { workspace = true, features = ["serde"] }
|
||||||
chacha20poly1305.workspace = true
|
chacha20poly1305.workspace = true
|
||||||
|
|||||||
+2
-26
@@ -30,32 +30,8 @@ workspace = true
|
|||||||
|
|
||||||
[features]
|
[features]
|
||||||
default = []
|
default = []
|
||||||
hotpath = [
|
hotpath = ["hotpath/hotpath", "hotpath/tokio", "hotpath/futures", "hotpath/reqwest-0-13"]
|
||||||
"hotpath/hotpath",
|
hotpath-alloc = ["hotpath/hotpath-alloc"]
|
||||||
"hotpath/tokio",
|
|
||||||
"hotpath/futures",
|
|
||||||
"hotpath/reqwest-0-13",
|
|
||||||
"rustfs-config/hotpath",
|
|
||||||
"rustfs-io-metrics/hotpath",
|
|
||||||
"rustfs-tls-runtime/hotpath",
|
|
||||||
"rustfs-utils/hotpath",
|
|
||||||
]
|
|
||||||
hotpath-alloc = [
|
|
||||||
"hotpath",
|
|
||||||
"hotpath/hotpath-alloc",
|
|
||||||
"rustfs-config/hotpath-alloc",
|
|
||||||
"rustfs-io-metrics/hotpath-alloc",
|
|
||||||
"rustfs-tls-runtime/hotpath-alloc",
|
|
||||||
"rustfs-utils/hotpath-alloc",
|
|
||||||
]
|
|
||||||
hotpath-cpu = [
|
|
||||||
"hotpath",
|
|
||||||
"hotpath/hotpath-cpu",
|
|
||||||
"rustfs-config/hotpath-cpu",
|
|
||||||
"rustfs-io-metrics/hotpath-cpu",
|
|
||||||
"rustfs-tls-runtime/hotpath-cpu",
|
|
||||||
"rustfs-utils/hotpath-cpu",
|
|
||||||
]
|
|
||||||
|
|
||||||
[dependencies]
|
[dependencies]
|
||||||
hotpath.workspace = true
|
hotpath.workspace = true
|
||||||
|
|||||||
@@ -27,14 +27,7 @@ categories = ["data-structures"]
|
|||||||
[lints]
|
[lints]
|
||||||
workspace = true
|
workspace = true
|
||||||
|
|
||||||
[features]
|
|
||||||
default = []
|
|
||||||
hotpath = ["hotpath/hotpath", "rustfs-s3-types/hotpath"]
|
|
||||||
hotpath-alloc = ["hotpath", "hotpath/hotpath-alloc", "rustfs-s3-types/hotpath-alloc"]
|
|
||||||
hotpath-cpu = ["hotpath", "hotpath/hotpath-cpu", "rustfs-s3-types/hotpath-cpu"]
|
|
||||||
|
|
||||||
[dependencies]
|
[dependencies]
|
||||||
hotpath.workspace = true
|
|
||||||
rustfs-s3-types = { workspace = true }
|
rustfs-s3-types = { workspace = true }
|
||||||
|
|
||||||
[lib]
|
[lib]
|
||||||
|
|||||||
@@ -27,14 +27,7 @@ categories = ["data-structures"]
|
|||||||
[lints]
|
[lints]
|
||||||
workspace = true
|
workspace = true
|
||||||
|
|
||||||
[features]
|
|
||||||
default = []
|
|
||||||
hotpath = ["hotpath/hotpath"]
|
|
||||||
hotpath-alloc = ["hotpath", "hotpath/hotpath-alloc"]
|
|
||||||
hotpath-cpu = ["hotpath", "hotpath/hotpath-cpu"]
|
|
||||||
|
|
||||||
[dependencies]
|
[dependencies]
|
||||||
hotpath.workspace = true
|
|
||||||
serde = { workspace = true, features = ["derive"] }
|
serde = { workspace = true, features = ["derive"] }
|
||||||
serde_json = { workspace = true, features = ["raw_value"] }
|
serde_json = { workspace = true, features = ["raw_value"] }
|
||||||
|
|
||||||
|
|||||||
@@ -28,37 +28,7 @@ documentation = "https://docs.rs/rustfs-s3select-api/latest/rustfs_s3select_api/
|
|||||||
[lints]
|
[lints]
|
||||||
workspace = true
|
workspace = true
|
||||||
|
|
||||||
[features]
|
|
||||||
default = []
|
|
||||||
hotpath = [
|
|
||||||
"hotpath/hotpath",
|
|
||||||
"hotpath/tokio",
|
|
||||||
"hotpath/futures",
|
|
||||||
"hotpath/parking_lot",
|
|
||||||
"rustfs-common/hotpath",
|
|
||||||
"rustfs-ecstore/hotpath",
|
|
||||||
"rustfs-storage-api/hotpath",
|
|
||||||
"rustfs-test-utils/hotpath",
|
|
||||||
]
|
|
||||||
hotpath-alloc = [
|
|
||||||
"hotpath",
|
|
||||||
"hotpath/hotpath-alloc",
|
|
||||||
"rustfs-common/hotpath-alloc",
|
|
||||||
"rustfs-ecstore/hotpath-alloc",
|
|
||||||
"rustfs-storage-api/hotpath-alloc",
|
|
||||||
"rustfs-test-utils/hotpath-alloc",
|
|
||||||
]
|
|
||||||
hotpath-cpu = [
|
|
||||||
"hotpath",
|
|
||||||
"hotpath/hotpath-cpu",
|
|
||||||
"rustfs-common/hotpath-cpu",
|
|
||||||
"rustfs-ecstore/hotpath-cpu",
|
|
||||||
"rustfs-storage-api/hotpath-cpu",
|
|
||||||
"rustfs-test-utils/hotpath-cpu",
|
|
||||||
]
|
|
||||||
|
|
||||||
[dependencies]
|
[dependencies]
|
||||||
hotpath.workspace = true
|
|
||||||
metrics = { workspace = true }
|
metrics = { workspace = true }
|
||||||
async-trait.workspace = true
|
async-trait.workspace = true
|
||||||
bytes = { workspace = true, features = ["serde"] }
|
bytes = { workspace = true, features = ["serde"] }
|
||||||
|
|||||||
@@ -28,20 +28,7 @@ documentation = "https://docs.rs/rustfs-s3select-query/latest/rustfs_s3select_qu
|
|||||||
[lints]
|
[lints]
|
||||||
workspace = true
|
workspace = true
|
||||||
|
|
||||||
[features]
|
|
||||||
default = []
|
|
||||||
hotpath = [
|
|
||||||
"hotpath/hotpath",
|
|
||||||
"hotpath/tokio",
|
|
||||||
"hotpath/futures",
|
|
||||||
"hotpath/parking_lot",
|
|
||||||
"rustfs-s3select-api/hotpath",
|
|
||||||
]
|
|
||||||
hotpath-alloc = ["hotpath", "hotpath/hotpath-alloc", "rustfs-s3select-api/hotpath-alloc"]
|
|
||||||
hotpath-cpu = ["hotpath", "hotpath/hotpath-cpu", "rustfs-s3select-api/hotpath-cpu"]
|
|
||||||
|
|
||||||
[dependencies]
|
[dependencies]
|
||||||
hotpath.workspace = true
|
|
||||||
rustfs-s3select-api = { workspace = true }
|
rustfs-s3select-api = { workspace = true }
|
||||||
async-recursion = { workspace = true }
|
async-recursion = { workspace = true }
|
||||||
async-trait.workspace = true
|
async-trait.workspace = true
|
||||||
|
|||||||
@@ -29,48 +29,7 @@ documentation = "https://docs.rs/rustfs-scanner/latest/rustfs_scanner/"
|
|||||||
[lints]
|
[lints]
|
||||||
workspace = true
|
workspace = true
|
||||||
|
|
||||||
[features]
|
|
||||||
default = []
|
|
||||||
hotpath = [
|
|
||||||
"hotpath/hotpath",
|
|
||||||
"hotpath/tokio",
|
|
||||||
"hotpath/futures",
|
|
||||||
"rustfs-common/hotpath",
|
|
||||||
"rustfs-config/hotpath",
|
|
||||||
"rustfs-credentials/hotpath",
|
|
||||||
"rustfs-data-usage/hotpath",
|
|
||||||
"rustfs-ecstore/hotpath",
|
|
||||||
"rustfs-filemeta/hotpath",
|
|
||||||
"rustfs-storage-api/hotpath",
|
|
||||||
"rustfs-utils/hotpath",
|
|
||||||
]
|
|
||||||
hotpath-alloc = [
|
|
||||||
"hotpath",
|
|
||||||
"hotpath/hotpath-alloc",
|
|
||||||
"rustfs-common/hotpath-alloc",
|
|
||||||
"rustfs-config/hotpath-alloc",
|
|
||||||
"rustfs-credentials/hotpath-alloc",
|
|
||||||
"rustfs-data-usage/hotpath-alloc",
|
|
||||||
"rustfs-ecstore/hotpath-alloc",
|
|
||||||
"rustfs-filemeta/hotpath-alloc",
|
|
||||||
"rustfs-storage-api/hotpath-alloc",
|
|
||||||
"rustfs-utils/hotpath-alloc",
|
|
||||||
]
|
|
||||||
hotpath-cpu = [
|
|
||||||
"hotpath",
|
|
||||||
"hotpath/hotpath-cpu",
|
|
||||||
"rustfs-common/hotpath-cpu",
|
|
||||||
"rustfs-config/hotpath-cpu",
|
|
||||||
"rustfs-credentials/hotpath-cpu",
|
|
||||||
"rustfs-data-usage/hotpath-cpu",
|
|
||||||
"rustfs-ecstore/hotpath-cpu",
|
|
||||||
"rustfs-filemeta/hotpath-cpu",
|
|
||||||
"rustfs-storage-api/hotpath-cpu",
|
|
||||||
"rustfs-utils/hotpath-cpu",
|
|
||||||
]
|
|
||||||
|
|
||||||
[dependencies]
|
[dependencies]
|
||||||
hotpath.workspace = true
|
|
||||||
rustfs-config = { workspace = true, features = ["server-config-model"] }
|
rustfs-config = { workspace = true, features = ["server-config-model"] }
|
||||||
rustfs-common = { workspace = true }
|
rustfs-common = { workspace = true }
|
||||||
rustfs-credentials = { workspace = true }
|
rustfs-credentials = { workspace = true }
|
||||||
|
|||||||
@@ -2531,7 +2531,6 @@ where
|
|||||||
}
|
}
|
||||||
|
|
||||||
#[instrument(skip_all)]
|
#[instrument(skip_all)]
|
||||||
#[hotpath::measure]
|
|
||||||
async fn run_data_scanner_cycle(
|
async fn run_data_scanner_cycle(
|
||||||
ctx: &CancellationToken,
|
ctx: &CancellationToken,
|
||||||
storeapi: &Arc<ECStore>,
|
storeapi: &Arc<ECStore>,
|
||||||
|
|||||||
@@ -30,12 +30,5 @@ doctest = false
|
|||||||
[lints]
|
[lints]
|
||||||
workspace = true
|
workspace = true
|
||||||
|
|
||||||
[features]
|
|
||||||
default = []
|
|
||||||
hotpath = ["hotpath/hotpath"]
|
|
||||||
hotpath-alloc = ["hotpath", "hotpath/hotpath-alloc"]
|
|
||||||
hotpath-cpu = ["hotpath", "hotpath/hotpath-cpu"]
|
|
||||||
|
|
||||||
[dependencies]
|
[dependencies]
|
||||||
hotpath.workspace = true
|
|
||||||
thiserror = { workspace = true }
|
thiserror = { workspace = true }
|
||||||
|
|||||||
@@ -25,14 +25,7 @@ keywords = ["digital-signature", "verification", "integrity", "rustfs", "Minio"]
|
|||||||
categories = ["web-programming", "development-tools", "cryptography"]
|
categories = ["web-programming", "development-tools", "cryptography"]
|
||||||
documentation = "https://docs.rs/rustfs-signer/latest/rustfs_signer/"
|
documentation = "https://docs.rs/rustfs-signer/latest/rustfs_signer/"
|
||||||
|
|
||||||
[features]
|
|
||||||
default = []
|
|
||||||
hotpath = ["hotpath/hotpath", "rustfs-utils/hotpath"]
|
|
||||||
hotpath-alloc = ["hotpath", "hotpath/hotpath-alloc", "rustfs-utils/hotpath-alloc"]
|
|
||||||
hotpath-cpu = ["hotpath", "hotpath/hotpath-cpu", "rustfs-utils/hotpath-cpu"]
|
|
||||||
|
|
||||||
[dependencies]
|
[dependencies]
|
||||||
hotpath.workspace = true
|
|
||||||
tracing.workspace = true
|
tracing.workspace = true
|
||||||
bytes = { workspace = true, features = ["serde"] }
|
bytes = { workspace = true, features = ["serde"] }
|
||||||
http.workspace = true
|
http.workspace = true
|
||||||
|
|||||||
@@ -27,14 +27,7 @@ categories = ["web-programming", "development-tools", "filesystem"]
|
|||||||
[lib]
|
[lib]
|
||||||
doctest = false
|
doctest = false
|
||||||
|
|
||||||
[features]
|
|
||||||
default = []
|
|
||||||
hotpath = ["hotpath/hotpath", "hotpath/tokio", "rustfs-filemeta/hotpath"]
|
|
||||||
hotpath-alloc = ["hotpath", "hotpath/hotpath-alloc", "rustfs-filemeta/hotpath-alloc"]
|
|
||||||
hotpath-cpu = ["hotpath", "hotpath/hotpath-cpu", "rustfs-filemeta/hotpath-cpu"]
|
|
||||||
|
|
||||||
[dependencies]
|
[dependencies]
|
||||||
hotpath.workspace = true
|
|
||||||
async-trait.workspace = true
|
async-trait.workspace = true
|
||||||
# Storage-facing replication contracts are isolated in src/replication.rs until
|
# Storage-facing replication contracts are isolated in src/replication.rs until
|
||||||
# the underlying wire types can move without creating a replication/storage-api cycle.
|
# the underlying wire types can move without creating a replication/storage-api cycle.
|
||||||
|
|||||||
@@ -11,41 +11,7 @@ keywords = ["file-system", "notification", "target", "rustfs", "Minio"]
|
|||||||
categories = ["web-programming", "development-tools", "filesystem"]
|
categories = ["web-programming", "development-tools", "filesystem"]
|
||||||
documentation = "https://docs.rs/rustfs-targets/latest/rustfs_targets/"
|
documentation = "https://docs.rs/rustfs-targets/latest/rustfs_targets/"
|
||||||
|
|
||||||
[features]
|
|
||||||
default = []
|
|
||||||
hotpath = [
|
|
||||||
"hotpath/hotpath",
|
|
||||||
"hotpath/tokio",
|
|
||||||
"hotpath/futures",
|
|
||||||
"hotpath/parking_lot",
|
|
||||||
"hotpath/reqwest-0-13",
|
|
||||||
"rustfs-config/hotpath",
|
|
||||||
"rustfs-extension-schema/hotpath",
|
|
||||||
"rustfs-s3-types/hotpath",
|
|
||||||
"rustfs-tls-runtime/hotpath",
|
|
||||||
"rustfs-utils/hotpath",
|
|
||||||
]
|
|
||||||
hotpath-alloc = [
|
|
||||||
"hotpath",
|
|
||||||
"hotpath/hotpath-alloc",
|
|
||||||
"rustfs-config/hotpath-alloc",
|
|
||||||
"rustfs-extension-schema/hotpath-alloc",
|
|
||||||
"rustfs-s3-types/hotpath-alloc",
|
|
||||||
"rustfs-tls-runtime/hotpath-alloc",
|
|
||||||
"rustfs-utils/hotpath-alloc",
|
|
||||||
]
|
|
||||||
hotpath-cpu = [
|
|
||||||
"hotpath",
|
|
||||||
"hotpath/hotpath-cpu",
|
|
||||||
"rustfs-config/hotpath-cpu",
|
|
||||||
"rustfs-extension-schema/hotpath-cpu",
|
|
||||||
"rustfs-s3-types/hotpath-cpu",
|
|
||||||
"rustfs-tls-runtime/hotpath-cpu",
|
|
||||||
"rustfs-utils/hotpath-cpu",
|
|
||||||
]
|
|
||||||
|
|
||||||
[dependencies]
|
[dependencies]
|
||||||
hotpath.workspace = true
|
|
||||||
rustfs-config = { workspace = true, features = ["notify", "audit", "server-config-model"] }
|
rustfs-config = { workspace = true, features = ["notify", "audit", "server-config-model"] }
|
||||||
rustfs-extension-schema = { workspace = true }
|
rustfs-extension-schema = { workspace = true }
|
||||||
rustfs-tls-runtime = { workspace = true }
|
rustfs-tls-runtime = { workspace = true }
|
||||||
|
|||||||
@@ -522,7 +522,6 @@ fn snapshot_from_delivery(target_id: TargetID, delivery: TargetDeliverySnapshot)
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#[hotpath::measure]
|
|
||||||
pub async fn init_target_and_optionally_start_replay<E, F, G>(
|
pub async fn init_target_and_optionally_start_replay<E, F, G>(
|
||||||
target: Box<dyn Target<E> + Send + Sync>,
|
target: Box<dyn Target<E> + Send + Sync>,
|
||||||
on_replay_start: F,
|
on_replay_start: F,
|
||||||
@@ -558,7 +557,6 @@ where
|
|||||||
Some((shared, cancel))
|
Some((shared, cancel))
|
||||||
}
|
}
|
||||||
|
|
||||||
#[hotpath::measure]
|
|
||||||
pub(crate) async fn prepare_target<E>(
|
pub(crate) async fn prepare_target<E>(
|
||||||
target: Box<dyn Target<E> + Send + Sync>,
|
target: Box<dyn Target<E> + Send + Sync>,
|
||||||
cancellation: Option<&CancellationToken>,
|
cancellation: Option<&CancellationToken>,
|
||||||
@@ -600,7 +598,6 @@ where
|
|||||||
|
|
||||||
type ActivatedTarget<E> = (SharedTarget<E>, Option<(mpsc::Sender<()>, JoinHandle<()>)>);
|
type ActivatedTarget<E> = (SharedTarget<E>, Option<(mpsc::Sender<()>, JoinHandle<()>)>);
|
||||||
|
|
||||||
#[hotpath::measure]
|
|
||||||
pub async fn activate_targets_with_replay<E, F, Fut>(
|
pub async fn activate_targets_with_replay<E, F, Fut>(
|
||||||
targets: Vec<Box<dyn Target<E> + Send + Sync>>,
|
targets: Vec<Box<dyn Target<E> + Send + Sync>>,
|
||||||
mut activate_one: F,
|
mut activate_one: F,
|
||||||
@@ -673,7 +670,6 @@ fn seed_interval_start(now: tokio::time::Instant, interval: Duration) -> tokio::
|
|||||||
now.checked_sub(interval).unwrap_or(now)
|
now.checked_sub(interval).unwrap_or(now)
|
||||||
}
|
}
|
||||||
|
|
||||||
#[hotpath::measure]
|
|
||||||
async fn stream_replay_worker<E>(
|
async fn stream_replay_worker<E>(
|
||||||
store: &mut (dyn Store<QueuedPayload, Error = StoreError, Key = Key> + Send),
|
store: &mut (dyn Store<QueuedPayload, Error = StoreError, Key = Key> + Send),
|
||||||
target: SharedTarget<E>,
|
target: SharedTarget<E>,
|
||||||
@@ -809,7 +805,6 @@ async fn stream_replay_worker<E>(
|
|||||||
/// Returns `true` if a cancel signal was observed while processing (e.g. during
|
/// Returns `true` if a cancel signal was observed while processing (e.g. during
|
||||||
/// retry backoff), so the caller can stop promptly instead of continuing to
|
/// retry backoff), so the caller can stop promptly instead of continuing to
|
||||||
/// drain a store that a replacement worker may already own.
|
/// drain a store that a replacement worker may already own.
|
||||||
#[hotpath::measure]
|
|
||||||
async fn process_replay_batch<E>(
|
async fn process_replay_batch<E>(
|
||||||
store: &(dyn Store<QueuedPayload, Error = StoreError, Key = Key> + Send),
|
store: &(dyn Store<QueuedPayload, Error = StoreError, Key = Key> + Send),
|
||||||
batch_keys: &mut Vec<Key>,
|
batch_keys: &mut Vec<Key>,
|
||||||
|
|||||||
@@ -85,7 +85,6 @@ fn classify_probe_error(err: &reqwest::Error) -> TargetHealthReason {
|
|||||||
TargetHealthReason::Unreachable
|
TargetHealthReason::Unreachable
|
||||||
}
|
}
|
||||||
|
|
||||||
#[hotpath::measure]
|
|
||||||
async fn probe_health_url(client: &Client, health_check_url: &Url) -> TargetHealth {
|
async fn probe_health_url(client: &Client, health_check_url: &Url) -> TargetHealth {
|
||||||
match tokio::time::timeout(WEBHOOK_HEALTH_TIMEOUT, client.head(health_check_url.as_str()).send()).await {
|
match tokio::time::timeout(WEBHOOK_HEALTH_TIMEOUT, client.head(health_check_url.as_str()).send()).await {
|
||||||
Ok(Ok(_)) => TargetHealth::online(TargetHealthReason::Reachable),
|
Ok(Ok(_)) => TargetHealth::online(TargetHealthReason::Reachable),
|
||||||
@@ -481,7 +480,6 @@ where
|
|||||||
build_queued_payload(event)
|
build_queued_payload(event)
|
||||||
}
|
}
|
||||||
|
|
||||||
#[hotpath::measure]
|
|
||||||
async fn send_body(&self, body: Vec<u8>, meta: &QueuedPayloadMeta) -> Result<(), TargetError> {
|
async fn send_body(&self, body: Vec<u8>, meta: &QueuedPayloadMeta) -> Result<(), TargetError> {
|
||||||
debug!(
|
debug!(
|
||||||
event = EVENT_WEBHOOK_DELIVERY_STATE,
|
event = EVENT_WEBHOOK_DELIVERY_STATE,
|
||||||
|
|||||||
@@ -25,32 +25,7 @@ keywords = ["testing", "storage", "rustfs", "Minio"]
|
|||||||
categories = ["development-tools", "filesystem"]
|
categories = ["development-tools", "filesystem"]
|
||||||
documentation = "https://docs.rs/rustfs-test-utils/latest/rustfs_test_utils/"
|
documentation = "https://docs.rs/rustfs-test-utils/latest/rustfs_test_utils/"
|
||||||
|
|
||||||
[features]
|
|
||||||
default = []
|
|
||||||
hotpath = [
|
|
||||||
"hotpath/hotpath",
|
|
||||||
"hotpath/tokio",
|
|
||||||
"rustfs-ecstore/hotpath",
|
|
||||||
"rustfs-storage-api/hotpath",
|
|
||||||
"rustfs-data-usage/hotpath",
|
|
||||||
]
|
|
||||||
hotpath-alloc = [
|
|
||||||
"hotpath",
|
|
||||||
"hotpath/hotpath-alloc",
|
|
||||||
"rustfs-ecstore/hotpath-alloc",
|
|
||||||
"rustfs-storage-api/hotpath-alloc",
|
|
||||||
"rustfs-data-usage/hotpath-alloc",
|
|
||||||
]
|
|
||||||
hotpath-cpu = [
|
|
||||||
"hotpath",
|
|
||||||
"hotpath/hotpath-cpu",
|
|
||||||
"rustfs-ecstore/hotpath-cpu",
|
|
||||||
"rustfs-storage-api/hotpath-cpu",
|
|
||||||
"rustfs-data-usage/hotpath-cpu",
|
|
||||||
]
|
|
||||||
|
|
||||||
[dependencies]
|
[dependencies]
|
||||||
hotpath.workspace = true
|
|
||||||
rustfs-ecstore = { workspace = true }
|
rustfs-ecstore = { workspace = true }
|
||||||
rustfs-storage-api = { workspace = true }
|
rustfs-storage-api = { workspace = true }
|
||||||
tokio = { workspace = true, features = ["fs", "rt-multi-thread"] }
|
tokio = { workspace = true, features = ["fs", "rt-multi-thread"] }
|
||||||
|
|||||||
@@ -27,14 +27,7 @@ categories = ["network-programming", "web-programming", "development-tools"]
|
|||||||
[lints]
|
[lints]
|
||||||
workspace = true
|
workspace = true
|
||||||
|
|
||||||
[features]
|
|
||||||
default = []
|
|
||||||
hotpath = ["hotpath/hotpath", "hotpath/tokio", "rustfs-common/hotpath", "rustfs-config/hotpath"]
|
|
||||||
hotpath-alloc = ["hotpath", "hotpath/hotpath-alloc", "rustfs-common/hotpath-alloc", "rustfs-config/hotpath-alloc"]
|
|
||||||
hotpath-cpu = ["hotpath", "hotpath/hotpath-cpu", "rustfs-common/hotpath-cpu", "rustfs-config/hotpath-cpu"]
|
|
||||||
|
|
||||||
[dependencies]
|
[dependencies]
|
||||||
hotpath.workspace = true
|
|
||||||
rustfs-common.workspace = true
|
rustfs-common.workspace = true
|
||||||
rustfs-config.workspace = true
|
rustfs-config.workspace = true
|
||||||
arc-swap.workspace = true
|
arc-swap.workspace = true
|
||||||
|
|||||||
@@ -24,20 +24,7 @@ description = " RustFS Trusted Proxies module provides secure and efficient mana
|
|||||||
keywords = ["trusted-proxies", "network-security", "rustfs", "proxy-management"]
|
keywords = ["trusted-proxies", "network-security", "rustfs", "proxy-management"]
|
||||||
categories = ["network-programming", "security", "web-programming"]
|
categories = ["network-programming", "security", "web-programming"]
|
||||||
|
|
||||||
[features]
|
|
||||||
default = []
|
|
||||||
hotpath = [
|
|
||||||
"hotpath/hotpath",
|
|
||||||
"hotpath/tokio",
|
|
||||||
"hotpath/reqwest-0-13",
|
|
||||||
"rustfs-config/hotpath",
|
|
||||||
"rustfs-utils/hotpath",
|
|
||||||
]
|
|
||||||
hotpath-alloc = ["hotpath", "hotpath/hotpath-alloc", "rustfs-config/hotpath-alloc", "rustfs-utils/hotpath-alloc"]
|
|
||||||
hotpath-cpu = ["hotpath", "hotpath/hotpath-cpu", "rustfs-config/hotpath-cpu", "rustfs-utils/hotpath-cpu"]
|
|
||||||
|
|
||||||
[dependencies]
|
[dependencies]
|
||||||
hotpath.workspace = true
|
|
||||||
async-trait = { workspace = true }
|
async-trait = { workspace = true }
|
||||||
axum = { workspace = true }
|
axum = { workspace = true }
|
||||||
http = { workspace = true }
|
http = { workspace = true }
|
||||||
|
|||||||
@@ -227,7 +227,6 @@ pub struct GoogleCloudIpRanges;
|
|||||||
|
|
||||||
impl GoogleCloudIpRanges {
|
impl GoogleCloudIpRanges {
|
||||||
/// Fetches the latest Google Cloud IP ranges from their official source.
|
/// Fetches the latest Google Cloud IP ranges from their official source.
|
||||||
#[hotpath::measure]
|
|
||||||
pub async fn fetch() -> Result<Vec<IpNetwork>, AppError> {
|
pub async fn fetch() -> Result<Vec<IpNetwork>, AppError> {
|
||||||
let client = Client::builder()
|
let client = Client::builder()
|
||||||
.timeout(Duration::from_secs(10))
|
.timeout(Duration::from_secs(10))
|
||||||
|
|||||||
@@ -25,7 +25,6 @@ keywords = ["utilities", "hashing", "compression", "network", "rustfs"]
|
|||||||
categories = ["web-programming", "development-tools", "cryptography"]
|
categories = ["web-programming", "development-tools", "cryptography"]
|
||||||
|
|
||||||
[dependencies]
|
[dependencies]
|
||||||
hotpath.workspace = true
|
|
||||||
base64-simd = { workspace = true, optional = true }
|
base64-simd = { workspace = true, optional = true }
|
||||||
blake2 = { workspace = true, optional = true }
|
blake2 = { workspace = true, optional = true }
|
||||||
brotli = { workspace = true, optional = true }
|
brotli = { workspace = true, optional = true }
|
||||||
@@ -77,9 +76,6 @@ workspace = true
|
|||||||
|
|
||||||
[features]
|
[features]
|
||||||
default = ["ip"] # features that are enabled by default
|
default = ["ip"] # features that are enabled by default
|
||||||
hotpath = ["hotpath/hotpath", "hotpath/tokio", "hotpath/futures", "hotpath/reqwest-0-13"]
|
|
||||||
hotpath-alloc = ["hotpath", "hotpath/hotpath-alloc"]
|
|
||||||
hotpath-cpu = ["hotpath", "hotpath/hotpath-cpu"]
|
|
||||||
ip = ["dep:local-ip-address"] # ip characteristics and their dependencies
|
ip = ["dep:local-ip-address"] # ip characteristics and their dependencies
|
||||||
net = ["ip", "dep:url", "dep:netif", "dep:futures", "dep:transform-stream", "dep:bytes", "dep:hyper", "dep:tokio"] # network features with DNS resolver
|
net = ["ip", "dep:url", "dep:netif", "dep:futures", "dep:transform-stream", "dep:bytes", "dep:hyper", "dep:tokio"] # network features with DNS resolver
|
||||||
egress = ["ip", "dep:reqwest", "dep:tokio", "dep:url"]
|
egress = ["ip", "dep:reqwest", "dep:tokio", "dep:url"]
|
||||||
|
|||||||
@@ -32,14 +32,7 @@ doctest = false
|
|||||||
name = "zip_benchmark"
|
name = "zip_benchmark"
|
||||||
harness = false
|
harness = false
|
||||||
|
|
||||||
[features]
|
|
||||||
default = []
|
|
||||||
hotpath = ["hotpath/hotpath", "hotpath/tokio"]
|
|
||||||
hotpath-alloc = ["hotpath", "hotpath/hotpath-alloc"]
|
|
||||||
hotpath-cpu = ["hotpath", "hotpath/hotpath-cpu"]
|
|
||||||
|
|
||||||
[dependencies]
|
[dependencies]
|
||||||
hotpath.workspace = true
|
|
||||||
async-compression = { workspace = true, features = [
|
async-compression = { workspace = true, features = [
|
||||||
"tokio",
|
"tokio",
|
||||||
"bzip2",
|
"bzip2",
|
||||||
|
|||||||
@@ -60,137 +60,15 @@ hotpath = [
|
|||||||
"hotpath/crossbeam",
|
"hotpath/crossbeam",
|
||||||
"hotpath/parking_lot",
|
"hotpath/parking_lot",
|
||||||
"hotpath/reqwest-0-13",
|
"hotpath/reqwest-0-13",
|
||||||
"rustfs-audit/hotpath",
|
|
||||||
"rustfs-common/hotpath",
|
|
||||||
"rustfs-concurrency/hotpath",
|
|
||||||
"rustfs-config/hotpath",
|
|
||||||
"rustfs-credentials/hotpath",
|
|
||||||
"rustfs-crypto/hotpath",
|
|
||||||
"rustfs-data-usage/hotpath",
|
|
||||||
"rustfs-ecstore/hotpath",
|
"rustfs-ecstore/hotpath",
|
||||||
"rustfs-extension-schema/hotpath",
|
|
||||||
"rustfs-filemeta/hotpath",
|
"rustfs-filemeta/hotpath",
|
||||||
"rustfs-heal/hotpath",
|
|
||||||
"rustfs-iam/hotpath",
|
|
||||||
"rustfs-io-core/hotpath",
|
|
||||||
"rustfs-io-metrics/hotpath",
|
|
||||||
"rustfs-keystone/hotpath",
|
|
||||||
"rustfs-kms/hotpath",
|
|
||||||
"rustfs-lock/hotpath",
|
|
||||||
"rustfs-log-analyzer/hotpath",
|
|
||||||
"rustfs-madmin/hotpath",
|
|
||||||
"rustfs-notify/hotpath",
|
|
||||||
"rustfs-object-capacity/hotpath",
|
|
||||||
"rustfs-object-data-cache/hotpath",
|
|
||||||
"rustfs-obs/hotpath",
|
|
||||||
"rustfs-policy/hotpath",
|
|
||||||
"rustfs-protocols/hotpath",
|
|
||||||
"rustfs-protos/hotpath",
|
|
||||||
"rustfs-rio/hotpath",
|
"rustfs-rio/hotpath",
|
||||||
"rustfs-s3-ops/hotpath",
|
|
||||||
"rustfs-s3-types/hotpath",
|
|
||||||
"rustfs-s3select-api/hotpath",
|
|
||||||
"rustfs-s3select-query/hotpath",
|
|
||||||
"rustfs-scanner/hotpath",
|
|
||||||
"rustfs-security-governance/hotpath",
|
|
||||||
"rustfs-signer/hotpath",
|
|
||||||
"rustfs-storage-api/hotpath",
|
|
||||||
"rustfs-targets/hotpath",
|
|
||||||
"rustfs-tls-runtime/hotpath",
|
|
||||||
"rustfs-trusted-proxies/hotpath",
|
|
||||||
"rustfs-utils/hotpath",
|
|
||||||
"rustfs-zip/hotpath",
|
|
||||||
"rustfs-test-utils/hotpath",
|
|
||||||
]
|
]
|
||||||
hotpath-alloc = [
|
hotpath-alloc = [
|
||||||
"hotpath",
|
|
||||||
"hotpath/hotpath-alloc",
|
"hotpath/hotpath-alloc",
|
||||||
"rustfs-audit/hotpath-alloc",
|
|
||||||
"rustfs-common/hotpath-alloc",
|
|
||||||
"rustfs-concurrency/hotpath-alloc",
|
|
||||||
"rustfs-config/hotpath-alloc",
|
|
||||||
"rustfs-credentials/hotpath-alloc",
|
|
||||||
"rustfs-crypto/hotpath-alloc",
|
|
||||||
"rustfs-data-usage/hotpath-alloc",
|
|
||||||
"rustfs-ecstore/hotpath-alloc",
|
"rustfs-ecstore/hotpath-alloc",
|
||||||
"rustfs-extension-schema/hotpath-alloc",
|
|
||||||
"rustfs-filemeta/hotpath-alloc",
|
"rustfs-filemeta/hotpath-alloc",
|
||||||
"rustfs-heal/hotpath-alloc",
|
|
||||||
"rustfs-iam/hotpath-alloc",
|
|
||||||
"rustfs-io-core/hotpath-alloc",
|
|
||||||
"rustfs-io-metrics/hotpath-alloc",
|
|
||||||
"rustfs-keystone/hotpath-alloc",
|
|
||||||
"rustfs-kms/hotpath-alloc",
|
|
||||||
"rustfs-lock/hotpath-alloc",
|
|
||||||
"rustfs-log-analyzer/hotpath-alloc",
|
|
||||||
"rustfs-madmin/hotpath-alloc",
|
|
||||||
"rustfs-notify/hotpath-alloc",
|
|
||||||
"rustfs-object-capacity/hotpath-alloc",
|
|
||||||
"rustfs-object-data-cache/hotpath-alloc",
|
|
||||||
"rustfs-obs/hotpath-alloc",
|
|
||||||
"rustfs-policy/hotpath-alloc",
|
|
||||||
"rustfs-protocols/hotpath-alloc",
|
|
||||||
"rustfs-protos/hotpath-alloc",
|
|
||||||
"rustfs-rio/hotpath-alloc",
|
"rustfs-rio/hotpath-alloc",
|
||||||
"rustfs-s3-ops/hotpath-alloc",
|
|
||||||
"rustfs-s3-types/hotpath-alloc",
|
|
||||||
"rustfs-s3select-api/hotpath-alloc",
|
|
||||||
"rustfs-s3select-query/hotpath-alloc",
|
|
||||||
"rustfs-scanner/hotpath-alloc",
|
|
||||||
"rustfs-security-governance/hotpath-alloc",
|
|
||||||
"rustfs-signer/hotpath-alloc",
|
|
||||||
"rustfs-storage-api/hotpath-alloc",
|
|
||||||
"rustfs-targets/hotpath-alloc",
|
|
||||||
"rustfs-tls-runtime/hotpath-alloc",
|
|
||||||
"rustfs-trusted-proxies/hotpath-alloc",
|
|
||||||
"rustfs-utils/hotpath-alloc",
|
|
||||||
"rustfs-zip/hotpath-alloc",
|
|
||||||
"rustfs-test-utils/hotpath-alloc",
|
|
||||||
]
|
|
||||||
hotpath-cpu = [
|
|
||||||
"hotpath",
|
|
||||||
"hotpath/hotpath-cpu",
|
|
||||||
"rustfs-audit/hotpath-cpu",
|
|
||||||
"rustfs-common/hotpath-cpu",
|
|
||||||
"rustfs-concurrency/hotpath-cpu",
|
|
||||||
"rustfs-config/hotpath-cpu",
|
|
||||||
"rustfs-credentials/hotpath-cpu",
|
|
||||||
"rustfs-crypto/hotpath-cpu",
|
|
||||||
"rustfs-data-usage/hotpath-cpu",
|
|
||||||
"rustfs-ecstore/hotpath-cpu",
|
|
||||||
"rustfs-extension-schema/hotpath-cpu",
|
|
||||||
"rustfs-filemeta/hotpath-cpu",
|
|
||||||
"rustfs-heal/hotpath-cpu",
|
|
||||||
"rustfs-iam/hotpath-cpu",
|
|
||||||
"rustfs-io-core/hotpath-cpu",
|
|
||||||
"rustfs-io-metrics/hotpath-cpu",
|
|
||||||
"rustfs-keystone/hotpath-cpu",
|
|
||||||
"rustfs-kms/hotpath-cpu",
|
|
||||||
"rustfs-lock/hotpath-cpu",
|
|
||||||
"rustfs-log-analyzer/hotpath-cpu",
|
|
||||||
"rustfs-madmin/hotpath-cpu",
|
|
||||||
"rustfs-notify/hotpath-cpu",
|
|
||||||
"rustfs-object-capacity/hotpath-cpu",
|
|
||||||
"rustfs-object-data-cache/hotpath-cpu",
|
|
||||||
"rustfs-obs/hotpath-cpu",
|
|
||||||
"rustfs-policy/hotpath-cpu",
|
|
||||||
"rustfs-protocols/hotpath-cpu",
|
|
||||||
"rustfs-protos/hotpath-cpu",
|
|
||||||
"rustfs-rio/hotpath-cpu",
|
|
||||||
"rustfs-s3-ops/hotpath-cpu",
|
|
||||||
"rustfs-s3-types/hotpath-cpu",
|
|
||||||
"rustfs-s3select-api/hotpath-cpu",
|
|
||||||
"rustfs-s3select-query/hotpath-cpu",
|
|
||||||
"rustfs-scanner/hotpath-cpu",
|
|
||||||
"rustfs-security-governance/hotpath-cpu",
|
|
||||||
"rustfs-signer/hotpath-cpu",
|
|
||||||
"rustfs-storage-api/hotpath-cpu",
|
|
||||||
"rustfs-targets/hotpath-cpu",
|
|
||||||
"rustfs-tls-runtime/hotpath-cpu",
|
|
||||||
"rustfs-trusted-proxies/hotpath-cpu",
|
|
||||||
"rustfs-utils/hotpath-cpu",
|
|
||||||
"rustfs-zip/hotpath-cpu",
|
|
||||||
"rustfs-test-utils/hotpath-cpu",
|
|
||||||
]
|
]
|
||||||
|
|
||||||
[lints]
|
[lints]
|
||||||
|
|||||||
Reference in New Issue
Block a user