mirror of
https://github.com/rustfs/rustfs.git
synced 2026-09-04 19:25:40 +00:00
feat(targets): add NATS and Pulsar target support (#2618)
This commit is contained in:
Generated
+253
-19
@@ -578,6 +578,42 @@ dependencies = [
|
|||||||
"pin-project-lite",
|
"pin-project-lite",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "async-nats"
|
||||||
|
version = "0.47.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "07d6f157065c3461096d51aacde0c326fa49f3f6e0199e204c566842cdaa5299"
|
||||||
|
dependencies = [
|
||||||
|
"base64 0.22.1",
|
||||||
|
"bytes",
|
||||||
|
"futures-util",
|
||||||
|
"memchr",
|
||||||
|
"nkeys",
|
||||||
|
"nuid",
|
||||||
|
"pin-project",
|
||||||
|
"portable-atomic",
|
||||||
|
"rand 0.8.6",
|
||||||
|
"regex",
|
||||||
|
"ring",
|
||||||
|
"rustls-native-certs",
|
||||||
|
"rustls-pki-types",
|
||||||
|
"rustls-webpki",
|
||||||
|
"serde",
|
||||||
|
"serde_json",
|
||||||
|
"serde_nanos",
|
||||||
|
"serde_repr",
|
||||||
|
"thiserror 1.0.69",
|
||||||
|
"time",
|
||||||
|
"tokio",
|
||||||
|
"tokio-rustls",
|
||||||
|
"tokio-stream",
|
||||||
|
"tokio-util",
|
||||||
|
"tokio-websockets",
|
||||||
|
"tracing",
|
||||||
|
"tryhard",
|
||||||
|
"url",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "async-recursion"
|
name = "async-recursion"
|
||||||
version = "1.1.1"
|
version = "1.1.1"
|
||||||
@@ -2760,7 +2796,7 @@ dependencies = [
|
|||||||
"itertools 0.14.0",
|
"itertools 0.14.0",
|
||||||
"parking_lot 0.12.5",
|
"parking_lot 0.12.5",
|
||||||
"paste",
|
"paste",
|
||||||
"petgraph",
|
"petgraph 0.8.3",
|
||||||
"recursive",
|
"recursive",
|
||||||
"tokio",
|
"tokio",
|
||||||
]
|
]
|
||||||
@@ -3302,6 +3338,7 @@ dependencies = [
|
|||||||
"ed25519",
|
"ed25519",
|
||||||
"serde",
|
"serde",
|
||||||
"sha2 0.10.9",
|
"sha2 0.10.9",
|
||||||
|
"signature 2.2.0",
|
||||||
"subtle",
|
"subtle",
|
||||||
"zeroize",
|
"zeroize",
|
||||||
]
|
]
|
||||||
@@ -3906,8 +3943,8 @@ dependencies = [
|
|||||||
"opentelemetry_sdk",
|
"opentelemetry_sdk",
|
||||||
"percent-encoding",
|
"percent-encoding",
|
||||||
"pin-project",
|
"pin-project",
|
||||||
"prost",
|
"prost 0.14.3",
|
||||||
"prost-types",
|
"prost-types 0.14.3",
|
||||||
"reqwest 0.13.2",
|
"reqwest 0.13.2",
|
||||||
"rustc_version",
|
"rustc_version",
|
||||||
"serde",
|
"serde",
|
||||||
@@ -4013,8 +4050,8 @@ dependencies = [
|
|||||||
"md5",
|
"md5",
|
||||||
"percent-encoding",
|
"percent-encoding",
|
||||||
"pin-project",
|
"pin-project",
|
||||||
"prost",
|
"prost 0.14.3",
|
||||||
"prost-types",
|
"prost-types 0.14.3",
|
||||||
"serde",
|
"serde",
|
||||||
"serde_json",
|
"serde_json",
|
||||||
"serde_with",
|
"serde_with",
|
||||||
@@ -4410,7 +4447,7 @@ dependencies = [
|
|||||||
"tokio",
|
"tokio",
|
||||||
"tokio-rustls",
|
"tokio-rustls",
|
||||||
"tower-service",
|
"tower-service",
|
||||||
"webpki-roots",
|
"webpki-roots 1.0.7",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
@@ -5605,6 +5642,12 @@ version = "0.10.1"
|
|||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "1d87ecb2933e8aeadb3e3a02b828fed80a7528047e68b4f424523a0981a3a084"
|
checksum = "1d87ecb2933e8aeadb3e3a02b828fed80a7528047e68b4f424523a0981a3a084"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "murmur3"
|
||||||
|
version = "0.5.2"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "9252111cf132ba0929b6f8e030cac2a24b507f3a4d6db6fb2896f27b354c714b"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "names"
|
name = "names"
|
||||||
version = "0.14.0"
|
version = "0.14.0"
|
||||||
@@ -5698,6 +5741,21 @@ dependencies = [
|
|||||||
"libc",
|
"libc",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "nkeys"
|
||||||
|
version = "0.4.5"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "879011babc47a1c7fdf5a935ae3cfe94f34645ca0cac1c7f6424b36fc743d1bf"
|
||||||
|
dependencies = [
|
||||||
|
"data-encoding",
|
||||||
|
"ed25519",
|
||||||
|
"ed25519-dalek",
|
||||||
|
"getrandom 0.2.17",
|
||||||
|
"log",
|
||||||
|
"rand 0.8.6",
|
||||||
|
"signatory",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "no_std_io2"
|
name = "no_std_io2"
|
||||||
version = "0.9.3"
|
version = "0.9.3"
|
||||||
@@ -5744,6 +5802,15 @@ dependencies = [
|
|||||||
"windows-sys 0.61.2",
|
"windows-sys 0.61.2",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "nuid"
|
||||||
|
version = "0.5.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "fc895af95856f929163a0aa20c26a78d26bfdc839f51b9d5aa7a5b79e52b7e83"
|
||||||
|
dependencies = [
|
||||||
|
"rand 0.8.6",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "num"
|
name = "num"
|
||||||
version = "0.4.3"
|
version = "0.4.3"
|
||||||
@@ -6098,7 +6165,7 @@ dependencies = [
|
|||||||
"opentelemetry-http",
|
"opentelemetry-http",
|
||||||
"opentelemetry-proto",
|
"opentelemetry-proto",
|
||||||
"opentelemetry_sdk",
|
"opentelemetry_sdk",
|
||||||
"prost",
|
"prost 0.14.3",
|
||||||
"reqwest 0.12.28",
|
"reqwest 0.12.28",
|
||||||
"thiserror 2.0.18",
|
"thiserror 2.0.18",
|
||||||
"tracing",
|
"tracing",
|
||||||
@@ -6112,7 +6179,7 @@ checksum = "a7175df06de5eaee9909d4805a3d07e28bb752c34cab57fa9cff549da596b30f"
|
|||||||
dependencies = [
|
dependencies = [
|
||||||
"opentelemetry",
|
"opentelemetry",
|
||||||
"opentelemetry_sdk",
|
"opentelemetry_sdk",
|
||||||
"prost",
|
"prost 0.14.3",
|
||||||
"tonic",
|
"tonic",
|
||||||
"tonic-prost",
|
"tonic-prost",
|
||||||
]
|
]
|
||||||
@@ -6423,6 +6490,16 @@ version = "2.3.2"
|
|||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "9b4f627cb1b25917193a259e49bdad08f671f8d9708acfd5fe0a8c1455d87220"
|
checksum = "9b4f627cb1b25917193a259e49bdad08f671f8d9708acfd5fe0a8c1455d87220"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "petgraph"
|
||||||
|
version = "0.7.1"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "3672b37090dbd86368a4145bc067582552b29c27377cad4e0a306c97f9bd7772"
|
||||||
|
dependencies = [
|
||||||
|
"fixedbitset",
|
||||||
|
"indexmap 2.14.0",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "petgraph"
|
name = "petgraph"
|
||||||
version = "0.8.3"
|
version = "0.8.3"
|
||||||
@@ -6689,7 +6766,7 @@ dependencies = [
|
|||||||
"inferno 0.12.6",
|
"inferno 0.12.6",
|
||||||
"num",
|
"num",
|
||||||
"paste",
|
"paste",
|
||||||
"prost",
|
"prost 0.14.3",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
@@ -6775,6 +6852,16 @@ dependencies = [
|
|||||||
"thiserror 2.0.18",
|
"thiserror 2.0.18",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "prost"
|
||||||
|
version = "0.13.5"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "2796faa41db3ec313a31f7624d9286acf277b52de526150b7e69f3debf891ee5"
|
||||||
|
dependencies = [
|
||||||
|
"bytes",
|
||||||
|
"prost-derive 0.13.5",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "prost"
|
name = "prost"
|
||||||
version = "0.14.3"
|
version = "0.14.3"
|
||||||
@@ -6782,7 +6869,27 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
|
|||||||
checksum = "d2ea70524a2f82d518bce41317d0fae74151505651af45faf1ffbd6fd33f0568"
|
checksum = "d2ea70524a2f82d518bce41317d0fae74151505651af45faf1ffbd6fd33f0568"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"bytes",
|
"bytes",
|
||||||
"prost-derive",
|
"prost-derive 0.14.3",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "prost-build"
|
||||||
|
version = "0.13.5"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "be769465445e8c1474e9c5dac2018218498557af32d9ed057325ec9a41ae81bf"
|
||||||
|
dependencies = [
|
||||||
|
"heck",
|
||||||
|
"itertools 0.14.0",
|
||||||
|
"log",
|
||||||
|
"multimap",
|
||||||
|
"once_cell",
|
||||||
|
"petgraph 0.7.1",
|
||||||
|
"prettyplease",
|
||||||
|
"prost 0.13.5",
|
||||||
|
"prost-types 0.13.5",
|
||||||
|
"regex",
|
||||||
|
"syn 2.0.117",
|
||||||
|
"tempfile",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
@@ -6795,10 +6902,10 @@ dependencies = [
|
|||||||
"itertools 0.14.0",
|
"itertools 0.14.0",
|
||||||
"log",
|
"log",
|
||||||
"multimap",
|
"multimap",
|
||||||
"petgraph",
|
"petgraph 0.8.3",
|
||||||
"prettyplease",
|
"prettyplease",
|
||||||
"prost",
|
"prost 0.14.3",
|
||||||
"prost-types",
|
"prost-types 0.14.3",
|
||||||
"pulldown-cmark",
|
"pulldown-cmark",
|
||||||
"pulldown-cmark-to-cmark",
|
"pulldown-cmark-to-cmark",
|
||||||
"regex",
|
"regex",
|
||||||
@@ -6806,6 +6913,19 @@ dependencies = [
|
|||||||
"tempfile",
|
"tempfile",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "prost-derive"
|
||||||
|
version = "0.13.5"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "8a56d757972c98b346a9b766e3f02746cde6dd1cd1d1d563472929fdd74bec4d"
|
||||||
|
dependencies = [
|
||||||
|
"anyhow",
|
||||||
|
"itertools 0.14.0",
|
||||||
|
"proc-macro2",
|
||||||
|
"quote",
|
||||||
|
"syn 2.0.117",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "prost-derive"
|
name = "prost-derive"
|
||||||
version = "0.14.3"
|
version = "0.14.3"
|
||||||
@@ -6819,13 +6939,22 @@ dependencies = [
|
|||||||
"syn 2.0.117",
|
"syn 2.0.117",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "prost-types"
|
||||||
|
version = "0.13.5"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "52c2c1bf36ddb1a1c396b3601a3cec27c2462e45f07c386894ec3ccf5332bd16"
|
||||||
|
dependencies = [
|
||||||
|
"prost 0.13.5",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "prost-types"
|
name = "prost-types"
|
||||||
version = "0.14.3"
|
version = "0.14.3"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "8991c4cbdb8bc5b11f0b074ffe286c30e523de90fee5ba8132f1399f23cb3dd7"
|
checksum = "8991c4cbdb8bc5b11f0b074ffe286c30e523de90fee5ba8132f1399f23cb3dd7"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"prost",
|
"prost 0.14.3",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
@@ -6919,6 +7048,36 @@ dependencies = [
|
|||||||
"pulldown-cmark",
|
"pulldown-cmark",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "pulsar"
|
||||||
|
version = "6.7.2"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "e2367cb38f1b65857bc11dd13b2adf13b7a1d991ef1cd43572f1420958c56cc2"
|
||||||
|
dependencies = [
|
||||||
|
"async-channel",
|
||||||
|
"async-trait",
|
||||||
|
"bytes",
|
||||||
|
"chrono",
|
||||||
|
"crc",
|
||||||
|
"futures",
|
||||||
|
"log",
|
||||||
|
"murmur3",
|
||||||
|
"nom 7.1.3",
|
||||||
|
"pem",
|
||||||
|
"prost 0.13.5",
|
||||||
|
"prost-build 0.13.5",
|
||||||
|
"prost-derive 0.13.5",
|
||||||
|
"rand 0.8.6",
|
||||||
|
"regex",
|
||||||
|
"rustls",
|
||||||
|
"tokio",
|
||||||
|
"tokio-rustls",
|
||||||
|
"tokio-util",
|
||||||
|
"url",
|
||||||
|
"uuid",
|
||||||
|
"webpki-roots 1.0.7",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "pyroscope"
|
name = "pyroscope"
|
||||||
version = "2.0.0"
|
version = "2.0.0"
|
||||||
@@ -6931,7 +7090,7 @@ dependencies = [
|
|||||||
"log",
|
"log",
|
||||||
"names",
|
"names",
|
||||||
"pprof-pyroscope-fork",
|
"pprof-pyroscope-fork",
|
||||||
"prost",
|
"prost 0.14.3",
|
||||||
"reqwest 0.13.2",
|
"reqwest 0.13.2",
|
||||||
"serde_json",
|
"serde_json",
|
||||||
"thiserror 2.0.18",
|
"thiserror 2.0.18",
|
||||||
@@ -8366,7 +8525,7 @@ name = "rustfs-protos"
|
|||||||
version = "0.0.5"
|
version = "0.0.5"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"flatbuffers",
|
"flatbuffers",
|
||||||
"prost",
|
"prost 0.14.3",
|
||||||
"rustfs-common",
|
"rustfs-common",
|
||||||
"tonic",
|
"tonic",
|
||||||
"tonic-prost",
|
"tonic-prost",
|
||||||
@@ -8510,12 +8669,15 @@ dependencies = [
|
|||||||
name = "rustfs-targets"
|
name = "rustfs-targets"
|
||||||
version = "0.0.5"
|
version = "0.0.5"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
|
"async-nats",
|
||||||
"async-trait",
|
"async-trait",
|
||||||
"criterion",
|
"criterion",
|
||||||
"hyper-rustls",
|
"hyper-rustls",
|
||||||
|
"pulsar",
|
||||||
"reqwest 0.13.2",
|
"reqwest 0.13.2",
|
||||||
"rumqttc-next",
|
"rumqttc-next",
|
||||||
"rustfs-config",
|
"rustfs-config",
|
||||||
|
"rustfs-ecstore",
|
||||||
"rustfs-s3-common",
|
"rustfs-s3-common",
|
||||||
"rustfs-utils",
|
"rustfs-utils",
|
||||||
"rustls",
|
"rustls",
|
||||||
@@ -9009,6 +9171,15 @@ dependencies = [
|
|||||||
"zmij",
|
"zmij",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "serde_nanos"
|
||||||
|
version = "0.1.4"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "a93142f0367a4cc53ae0fead1bcda39e85beccfad3dcd717656cacab94b12985"
|
||||||
|
dependencies = [
|
||||||
|
"serde",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "serde_path_to_error"
|
name = "serde_path_to_error"
|
||||||
version = "0.1.20"
|
version = "0.1.20"
|
||||||
@@ -9029,6 +9200,17 @@ dependencies = [
|
|||||||
"serde",
|
"serde",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "serde_repr"
|
||||||
|
version = "0.1.20"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "175ee3e80ae9982737ca543e96133087cbd9a485eecc3bc4de9c1a37b47ea59c"
|
||||||
|
dependencies = [
|
||||||
|
"proc-macro2",
|
||||||
|
"quote",
|
||||||
|
"syn 2.0.117",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "serde_urlencoded"
|
name = "serde_urlencoded"
|
||||||
version = "0.7.1"
|
version = "0.7.1"
|
||||||
@@ -9200,6 +9382,18 @@ dependencies = [
|
|||||||
"libc",
|
"libc",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "signatory"
|
||||||
|
version = "0.27.1"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "c1e303f8205714074f6068773f0e29527e0453937fe837c9717d066635b65f31"
|
||||||
|
dependencies = [
|
||||||
|
"pkcs8 0.10.2",
|
||||||
|
"rand_core 0.6.4",
|
||||||
|
"signature 2.2.0",
|
||||||
|
"zeroize",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "signature"
|
name = "signature"
|
||||||
version = "1.6.4"
|
version = "1.6.4"
|
||||||
@@ -10000,6 +10194,27 @@ dependencies = [
|
|||||||
"tokio",
|
"tokio",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "tokio-websockets"
|
||||||
|
version = "0.10.1"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "f591660438b3038dd04d16c938271c79e7e06260ad2ea2885a4861bfb238605d"
|
||||||
|
dependencies = [
|
||||||
|
"base64 0.22.1",
|
||||||
|
"bytes",
|
||||||
|
"futures-core",
|
||||||
|
"futures-sink",
|
||||||
|
"http 1.4.0",
|
||||||
|
"httparse",
|
||||||
|
"rand 0.8.6",
|
||||||
|
"ring",
|
||||||
|
"rustls-pki-types",
|
||||||
|
"tokio",
|
||||||
|
"tokio-rustls",
|
||||||
|
"tokio-util",
|
||||||
|
"webpki-roots 0.26.11",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "tonic"
|
name = "tonic"
|
||||||
version = "0.14.5"
|
version = "0.14.5"
|
||||||
@@ -10051,7 +10266,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
|
|||||||
checksum = "a55376a0bbaa4975a3f10d009ad763d8f4108f067c7c2e74f3001fb49778d309"
|
checksum = "a55376a0bbaa4975a3f10d009ad763d8f4108f067c7c2e74f3001fb49778d309"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"bytes",
|
"bytes",
|
||||||
"prost",
|
"prost 0.14.3",
|
||||||
"tonic",
|
"tonic",
|
||||||
]
|
]
|
||||||
|
|
||||||
@@ -10063,8 +10278,8 @@ checksum = "f3144df636917574672e93d0f56d7edec49f90305749c668df5101751bb8f95a"
|
|||||||
dependencies = [
|
dependencies = [
|
||||||
"prettyplease",
|
"prettyplease",
|
||||||
"proc-macro2",
|
"proc-macro2",
|
||||||
"prost-build",
|
"prost-build 0.14.3",
|
||||||
"prost-types",
|
"prost-types 0.14.3",
|
||||||
"quote",
|
"quote",
|
||||||
"syn 2.0.117",
|
"syn 2.0.117",
|
||||||
"tempfile",
|
"tempfile",
|
||||||
@@ -10257,6 +10472,16 @@ version = "0.2.5"
|
|||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "e421abadd41a4225275504ea4d6566923418b7f05506fbc9c0fe86ba7396114b"
|
checksum = "e421abadd41a4225275504ea4d6566923418b7f05506fbc9c0fe86ba7396114b"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "tryhard"
|
||||||
|
version = "0.5.2"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "9fe58ebd5edd976e0fe0f8a14d2a04b7c81ef153ea9a54eebc42e67c2c23b4e5"
|
||||||
|
dependencies = [
|
||||||
|
"pin-project-lite",
|
||||||
|
"tokio",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "tungstenite"
|
name = "tungstenite"
|
||||||
version = "0.29.0"
|
version = "0.29.0"
|
||||||
@@ -10642,6 +10867,15 @@ dependencies = [
|
|||||||
"rustls-pki-types",
|
"rustls-pki-types",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "webpki-roots"
|
||||||
|
version = "0.26.11"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "521bc38abb08001b01866da9f51eb7c5d647a19260e00054a8c7fd5f9e57f7a9"
|
||||||
|
dependencies = [
|
||||||
|
"webpki-roots 1.0.7",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "webpki-roots"
|
name = "webpki-roots"
|
||||||
version = "1.0.7"
|
version = "1.0.7"
|
||||||
|
|||||||
+3
-1
@@ -38,7 +38,7 @@ members = [
|
|||||||
"crates/protocols", # Protocol implementations (FTPS, SFTP, etc.)
|
"crates/protocols", # Protocol implementations (FTPS, SFTP, etc.)
|
||||||
"crates/protos", # Protocol buffer definitions
|
"crates/protos", # Protocol buffer definitions
|
||||||
"crates/rio", # Rust I/O utilities and abstractions
|
"crates/rio", # Rust I/O utilities and abstractions
|
||||||
"crates/concurrency", # Rust I/O utilities and abstractions
|
"crates/concurrency", # Concurrency management for RustFS - timeout, locking, backpressure, and I/O scheduling
|
||||||
"crates/s3-common", # Common utilities and data structures for S3 compatibility
|
"crates/s3-common", # Common utilities and data structures for S3 compatibility
|
||||||
"crates/s3select-api", # S3 Select API interface
|
"crates/s3select-api", # S3 Select API interface
|
||||||
"crates/s3select-query", # S3 Select query engine
|
"crates/s3select-query", # S3 Select query engine
|
||||||
@@ -118,11 +118,13 @@ async-channel = "2.5.0"
|
|||||||
async-compression = { version = "0.4.41" }
|
async-compression = { version = "0.4.41" }
|
||||||
async-recursion = "1.1.1"
|
async-recursion = "1.1.1"
|
||||||
async-trait = "0.1.89"
|
async-trait = "0.1.89"
|
||||||
|
async-nats = "0.47.0"
|
||||||
axum = "0.8.9"
|
axum = "0.8.9"
|
||||||
futures = "0.3.32"
|
futures = "0.3.32"
|
||||||
futures-core = "0.3.32"
|
futures-core = "0.3.32"
|
||||||
futures-util = "0.3.32"
|
futures-util = "0.3.32"
|
||||||
pollster = "0.4.0"
|
pollster = "0.4.0"
|
||||||
|
pulsar = { version = "6.7.2", default-features = false, features = ["tokio-rustls-runtime"] }
|
||||||
hyper = { version = "1.9.0", features = ["http2", "http1", "server"] }
|
hyper = { version = "1.9.0", features = ["http2", "http1", "server"] }
|
||||||
hyper-rustls = { version = "0.27.9", default-features = false, features = ["native-tokio", "http1", "tls12", "logging", "http2", "aws-lc-rs", "webpki-roots"] }
|
hyper-rustls = { version = "0.27.9", default-features = false, features = ["native-tokio", "http1", "tls12", "logging", "http2", "aws-lc-rs", "webpki-roots"] }
|
||||||
hyper-util = { version = "0.1.20", features = ["tokio", "server-auto", "server-graceful", "tracing"] }
|
hyper-util = { version = "0.1.20", features = ["tokio", "server-auto", "server-graceful", "tracing"] }
|
||||||
|
|||||||
+48
-176
@@ -14,28 +14,19 @@
|
|||||||
|
|
||||||
use crate::AuditEntry;
|
use crate::AuditEntry;
|
||||||
use async_trait::async_trait;
|
use async_trait::async_trait;
|
||||||
use hashbrown::HashSet;
|
use rustfs_config::AUDIT_DEFAULT_DIR;
|
||||||
use rumqttc::QoS;
|
use rustfs_config::audit::{AUDIT_MQTT_KEYS, AUDIT_NATS_KEYS, AUDIT_PULSAR_KEYS, AUDIT_WEBHOOK_KEYS};
|
||||||
use rustfs_config::audit::{AUDIT_MQTT_KEYS, AUDIT_WEBHOOK_KEYS, ENV_AUDIT_MQTT_KEYS, ENV_AUDIT_WEBHOOK_KEYS};
|
|
||||||
use rustfs_config::{
|
|
||||||
AUDIT_DEFAULT_DIR, DEFAULT_LIMIT, MQTT_BROKER, MQTT_KEEP_ALIVE_INTERVAL, MQTT_PASSWORD, MQTT_QOS, MQTT_QUEUE_DIR,
|
|
||||||
MQTT_QUEUE_LIMIT, MQTT_RECONNECT_INTERVAL, MQTT_TLS_CA, MQTT_TLS_CLIENT_CERT, MQTT_TLS_CLIENT_KEY, MQTT_TLS_POLICY,
|
|
||||||
MQTT_TLS_TRUST_LEAF_AS_CA, MQTT_TOPIC, MQTT_USERNAME, MQTT_WS_PATH_ALLOWLIST, RUSTFS_WEBHOOK_SKIP_TLS_VERIFY_DEFAULT,
|
|
||||||
WEBHOOK_AUTH_TOKEN, WEBHOOK_CLIENT_CA, WEBHOOK_CLIENT_CERT, WEBHOOK_CLIENT_KEY, WEBHOOK_ENDPOINT, WEBHOOK_QUEUE_DIR,
|
|
||||||
WEBHOOK_QUEUE_LIMIT, WEBHOOK_SKIP_TLS_VERIFY,
|
|
||||||
};
|
|
||||||
use rustfs_ecstore::config::KVS;
|
use rustfs_ecstore::config::KVS;
|
||||||
use rustfs_targets::{
|
use rustfs_targets::{
|
||||||
Target,
|
Target,
|
||||||
error::TargetError,
|
config::{
|
||||||
target::{
|
build_mqtt_args, build_nats_args, build_pulsar_args, build_webhook_args, validate_mqtt_config, validate_nats_config,
|
||||||
mqtt::{MQTTArgs, MQTTTlsConfig, validate_mqtt_broker_url},
|
validate_pulsar_config, validate_webhook_config,
|
||||||
webhook::WebhookArgs,
|
|
||||||
},
|
},
|
||||||
|
error::TargetError,
|
||||||
|
target::TargetType,
|
||||||
};
|
};
|
||||||
use std::time::Duration;
|
use std::collections::HashSet;
|
||||||
use tracing::{debug, warn};
|
|
||||||
use url::Url;
|
|
||||||
|
|
||||||
/// Trait for creating targets from configuration
|
/// Trait for creating targets from configuration
|
||||||
#[async_trait]
|
#[async_trait]
|
||||||
@@ -49,10 +40,6 @@ pub trait TargetFactory: Send + Sync {
|
|||||||
/// Returns a set of valid configuration field names for this target type.
|
/// Returns a set of valid configuration field names for this target type.
|
||||||
/// This is used to filter environment variables.
|
/// This is used to filter environment variables.
|
||||||
fn get_valid_fields(&self) -> HashSet<String>;
|
fn get_valid_fields(&self) -> HashSet<String>;
|
||||||
|
|
||||||
/// Returns a set of valid configuration env field names for this target type.
|
|
||||||
/// This is used to filter environment variables.
|
|
||||||
fn get_valid_env_fields(&self) -> HashSet<String>;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Factory for creating Webhook targets
|
/// Factory for creating Webhook targets
|
||||||
@@ -61,75 +48,18 @@ pub struct WebhookTargetFactory;
|
|||||||
#[async_trait]
|
#[async_trait]
|
||||||
impl TargetFactory for WebhookTargetFactory {
|
impl TargetFactory for WebhookTargetFactory {
|
||||||
async fn create_target(&self, id: String, config: &KVS) -> Result<Box<dyn Target<AuditEntry> + Send + Sync>, TargetError> {
|
async fn create_target(&self, id: String, config: &KVS) -> Result<Box<dyn Target<AuditEntry> + Send + Sync>, TargetError> {
|
||||||
// All config values are now read directly from the merged `config` KVS.
|
let args = build_webhook_args(config, AUDIT_DEFAULT_DIR, TargetType::AuditLog)?;
|
||||||
let endpoint = config
|
|
||||||
.lookup(WEBHOOK_ENDPOINT)
|
|
||||||
.ok_or_else(|| TargetError::Configuration("Missing webhook endpoint".to_string()))?;
|
|
||||||
let parsed_endpoint = endpoint.trim();
|
|
||||||
let endpoint_url = Url::parse(parsed_endpoint)
|
|
||||||
.map_err(|e| TargetError::Configuration(format!("Invalid endpoint URL: {e} (value: '{parsed_endpoint}')")))?;
|
|
||||||
|
|
||||||
let args = WebhookArgs {
|
|
||||||
enable: true, // If we are here, it's already enabled.
|
|
||||||
endpoint: endpoint_url,
|
|
||||||
auth_token: config.lookup(WEBHOOK_AUTH_TOKEN).unwrap_or_default(),
|
|
||||||
queue_dir: config
|
|
||||||
.lookup(WEBHOOK_QUEUE_DIR)
|
|
||||||
.unwrap_or_else(|| AUDIT_DEFAULT_DIR.to_string()),
|
|
||||||
queue_limit: config
|
|
||||||
.lookup(WEBHOOK_QUEUE_LIMIT)
|
|
||||||
.and_then(|v| v.parse::<u64>().ok())
|
|
||||||
.unwrap_or(DEFAULT_LIMIT),
|
|
||||||
client_cert: config.lookup(WEBHOOK_CLIENT_CERT).unwrap_or_default(),
|
|
||||||
client_key: config.lookup(WEBHOOK_CLIENT_KEY).unwrap_or_default(),
|
|
||||||
client_ca: config.lookup(WEBHOOK_CLIENT_CA).unwrap_or_default(),
|
|
||||||
skip_tls_verify: config
|
|
||||||
.lookup(WEBHOOK_SKIP_TLS_VERIFY)
|
|
||||||
.and_then(|v| v.parse::<bool>().ok())
|
|
||||||
.unwrap_or(RUSTFS_WEBHOOK_SKIP_TLS_VERIFY_DEFAULT),
|
|
||||||
target_type: rustfs_targets::target::TargetType::AuditLog,
|
|
||||||
};
|
|
||||||
|
|
||||||
let target = rustfs_targets::target::webhook::WebhookTarget::new(id, args)?;
|
let target = rustfs_targets::target::webhook::WebhookTarget::new(id, args)?;
|
||||||
Ok(Box::new(target))
|
Ok(Box::new(target))
|
||||||
}
|
}
|
||||||
|
|
||||||
fn validate_config(&self, _id: &str, config: &KVS) -> Result<(), TargetError> {
|
fn validate_config(&self, _id: &str, config: &KVS) -> Result<(), TargetError> {
|
||||||
// Validation also uses the merged `config` KVS directly.
|
validate_webhook_config(config, AUDIT_DEFAULT_DIR)
|
||||||
let endpoint = config
|
|
||||||
.lookup(WEBHOOK_ENDPOINT)
|
|
||||||
.ok_or_else(|| TargetError::Configuration("Missing webhook endpoint".to_string()))?;
|
|
||||||
debug!("endpoint: {}", endpoint);
|
|
||||||
let parsed_endpoint = endpoint.trim();
|
|
||||||
Url::parse(parsed_endpoint)
|
|
||||||
.map_err(|e| TargetError::Configuration(format!("Invalid endpoint URL: {e} (value: '{parsed_endpoint}')")))?;
|
|
||||||
|
|
||||||
let client_cert = config.lookup(WEBHOOK_CLIENT_CERT).unwrap_or_default();
|
|
||||||
let client_key = config.lookup(WEBHOOK_CLIENT_KEY).unwrap_or_default();
|
|
||||||
|
|
||||||
if client_cert.is_empty() != client_key.is_empty() {
|
|
||||||
return Err(TargetError::Configuration(
|
|
||||||
"Both client_cert and client_key must be specified together".to_string(),
|
|
||||||
));
|
|
||||||
}
|
|
||||||
|
|
||||||
let queue_dir = config
|
|
||||||
.lookup(WEBHOOK_QUEUE_DIR)
|
|
||||||
.unwrap_or_else(|| AUDIT_DEFAULT_DIR.to_string());
|
|
||||||
if !queue_dir.is_empty() && !std::path::Path::new(&queue_dir).is_absolute() {
|
|
||||||
return Err(TargetError::Configuration("Webhook queue directory must be an absolute path".to_string()));
|
|
||||||
}
|
|
||||||
|
|
||||||
Ok(())
|
|
||||||
}
|
}
|
||||||
|
|
||||||
fn get_valid_fields(&self) -> HashSet<String> {
|
fn get_valid_fields(&self) -> HashSet<String> {
|
||||||
AUDIT_WEBHOOK_KEYS.iter().map(|s| s.to_string()).collect()
|
AUDIT_WEBHOOK_KEYS.iter().map(|s| s.to_string()).collect()
|
||||||
}
|
}
|
||||||
|
|
||||||
fn get_valid_env_fields(&self) -> HashSet<String> {
|
|
||||||
ENV_AUDIT_WEBHOOK_KEYS.iter().map(|s| s.to_string()).collect()
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Factory for creating MQTT targets
|
/// Factory for creating MQTT targets
|
||||||
@@ -138,112 +68,54 @@ pub struct MQTTTargetFactory;
|
|||||||
#[async_trait]
|
#[async_trait]
|
||||||
impl TargetFactory for MQTTTargetFactory {
|
impl TargetFactory for MQTTTargetFactory {
|
||||||
async fn create_target(&self, id: String, config: &KVS) -> Result<Box<dyn Target<AuditEntry> + Send + Sync>, TargetError> {
|
async fn create_target(&self, id: String, config: &KVS) -> Result<Box<dyn Target<AuditEntry> + Send + Sync>, TargetError> {
|
||||||
let broker = config
|
let args = build_mqtt_args(config, AUDIT_DEFAULT_DIR, TargetType::AuditLog)?;
|
||||||
.lookup(MQTT_BROKER)
|
|
||||||
.ok_or_else(|| TargetError::Configuration("Missing MQTT broker".to_string()))?;
|
|
||||||
let broker_url = Url::parse(&broker)
|
|
||||||
.map_err(|e| TargetError::Configuration(format!("Invalid broker URL: {e} (value: '{broker}')")))?;
|
|
||||||
|
|
||||||
let topic = config
|
|
||||||
.lookup(MQTT_TOPIC)
|
|
||||||
.ok_or_else(|| TargetError::Configuration("Missing MQTT topic".to_string()))?;
|
|
||||||
|
|
||||||
let args = MQTTArgs {
|
|
||||||
enable: true, // Assumed enabled.
|
|
||||||
broker: broker_url,
|
|
||||||
topic,
|
|
||||||
qos: config
|
|
||||||
.lookup(MQTT_QOS)
|
|
||||||
.and_then(|v| v.parse::<u8>().ok())
|
|
||||||
.map(|q| match q {
|
|
||||||
0 => QoS::AtMostOnce,
|
|
||||||
1 => QoS::AtLeastOnce,
|
|
||||||
2 => QoS::ExactlyOnce,
|
|
||||||
_ => QoS::AtLeastOnce,
|
|
||||||
})
|
|
||||||
.unwrap_or(QoS::AtLeastOnce),
|
|
||||||
username: config.lookup(MQTT_USERNAME).unwrap_or_default(),
|
|
||||||
password: config.lookup(MQTT_PASSWORD).unwrap_or_default(),
|
|
||||||
max_reconnect_interval: config
|
|
||||||
.lookup(MQTT_RECONNECT_INTERVAL)
|
|
||||||
.and_then(|v| v.parse::<u64>().ok())
|
|
||||||
.map(Duration::from_secs)
|
|
||||||
.unwrap_or_else(|| Duration::from_secs(5)),
|
|
||||||
keep_alive: config
|
|
||||||
.lookup(MQTT_KEEP_ALIVE_INTERVAL)
|
|
||||||
.and_then(|v| v.parse::<u64>().ok())
|
|
||||||
.map(Duration::from_secs)
|
|
||||||
.unwrap_or_else(|| Duration::from_secs(30)),
|
|
||||||
tls: MQTTTlsConfig::from_values(
|
|
||||||
config.lookup(MQTT_TLS_POLICY).as_deref(),
|
|
||||||
config.lookup(MQTT_TLS_CA).as_deref(),
|
|
||||||
config.lookup(MQTT_TLS_CLIENT_CERT).as_deref(),
|
|
||||||
config.lookup(MQTT_TLS_CLIENT_KEY).as_deref(),
|
|
||||||
config.lookup(MQTT_TLS_TRUST_LEAF_AS_CA).as_deref(),
|
|
||||||
config.lookup(MQTT_WS_PATH_ALLOWLIST).as_deref(),
|
|
||||||
)?,
|
|
||||||
queue_dir: config.lookup(MQTT_QUEUE_DIR).unwrap_or_else(|| AUDIT_DEFAULT_DIR.to_string()),
|
|
||||||
queue_limit: config
|
|
||||||
.lookup(MQTT_QUEUE_LIMIT)
|
|
||||||
.and_then(|v| v.parse::<u64>().ok())
|
|
||||||
.unwrap_or(DEFAULT_LIMIT),
|
|
||||||
target_type: rustfs_targets::target::TargetType::AuditLog,
|
|
||||||
};
|
|
||||||
|
|
||||||
let target = rustfs_targets::target::mqtt::MQTTTarget::new(id, args)?;
|
let target = rustfs_targets::target::mqtt::MQTTTarget::new(id, args)?;
|
||||||
Ok(Box::new(target))
|
Ok(Box::new(target))
|
||||||
}
|
}
|
||||||
|
|
||||||
fn validate_config(&self, _id: &str, config: &KVS) -> Result<(), TargetError> {
|
fn validate_config(&self, _id: &str, config: &KVS) -> Result<(), TargetError> {
|
||||||
let broker = config
|
validate_mqtt_config(config)
|
||||||
.lookup(MQTT_BROKER)
|
|
||||||
.ok_or_else(|| TargetError::Configuration("Missing MQTT broker".to_string()))?;
|
|
||||||
let url = Url::parse(&broker)
|
|
||||||
.map_err(|e| TargetError::Configuration(format!("Invalid broker URL: {e} (value: '{broker}')")))?;
|
|
||||||
|
|
||||||
let tls = MQTTTlsConfig::from_values(
|
|
||||||
config.lookup(MQTT_TLS_POLICY).as_deref(),
|
|
||||||
config.lookup(MQTT_TLS_CA).as_deref(),
|
|
||||||
config.lookup(MQTT_TLS_CLIENT_CERT).as_deref(),
|
|
||||||
config.lookup(MQTT_TLS_CLIENT_KEY).as_deref(),
|
|
||||||
config.lookup(MQTT_TLS_TRUST_LEAF_AS_CA).as_deref(),
|
|
||||||
config.lookup(MQTT_WS_PATH_ALLOWLIST).as_deref(),
|
|
||||||
)?;
|
|
||||||
validate_mqtt_broker_url(&url, &tls)?;
|
|
||||||
|
|
||||||
if config.lookup(MQTT_TOPIC).is_none() {
|
|
||||||
return Err(TargetError::Configuration("Missing MQTT topic".to_string()));
|
|
||||||
}
|
|
||||||
|
|
||||||
if let Some(qos_str) = config.lookup(MQTT_QOS) {
|
|
||||||
let qos = qos_str
|
|
||||||
.parse::<u8>()
|
|
||||||
.map_err(|_| TargetError::Configuration("Invalid QoS value".to_string()))?;
|
|
||||||
if qos > 2 {
|
|
||||||
return Err(TargetError::Configuration("QoS must be 0, 1, or 2".to_string()));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
let queue_dir = config.lookup(MQTT_QUEUE_DIR).unwrap_or_default();
|
|
||||||
if !queue_dir.is_empty() {
|
|
||||||
if !std::path::Path::new(&queue_dir).is_absolute() {
|
|
||||||
return Err(TargetError::Configuration("MQTT queue directory must be an absolute path".to_string()));
|
|
||||||
}
|
|
||||||
if let Some(qos_str) = config.lookup(MQTT_QOS)
|
|
||||||
&& qos_str == "0"
|
|
||||||
{
|
|
||||||
warn!("Using queue_dir with QoS 0 may result in event loss");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Ok(())
|
|
||||||
}
|
}
|
||||||
|
|
||||||
fn get_valid_fields(&self) -> HashSet<String> {
|
fn get_valid_fields(&self) -> HashSet<String> {
|
||||||
AUDIT_MQTT_KEYS.iter().map(|s| s.to_string()).collect()
|
AUDIT_MQTT_KEYS.iter().map(|s| s.to_string()).collect()
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
fn get_valid_env_fields(&self) -> HashSet<String> {
|
pub struct NATSTargetFactory;
|
||||||
ENV_AUDIT_MQTT_KEYS.iter().map(|s| s.to_string()).collect()
|
|
||||||
|
#[async_trait]
|
||||||
|
impl TargetFactory for NATSTargetFactory {
|
||||||
|
async fn create_target(&self, id: String, config: &KVS) -> Result<Box<dyn Target<AuditEntry> + Send + Sync>, TargetError> {
|
||||||
|
let args = build_nats_args(config, AUDIT_DEFAULT_DIR, TargetType::AuditLog)?;
|
||||||
|
let target = rustfs_targets::target::nats::NATSTarget::new(id, args)?;
|
||||||
|
Ok(Box::new(target))
|
||||||
|
}
|
||||||
|
|
||||||
|
fn validate_config(&self, _id: &str, config: &KVS) -> Result<(), TargetError> {
|
||||||
|
validate_nats_config(config, AUDIT_DEFAULT_DIR)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn get_valid_fields(&self) -> HashSet<String> {
|
||||||
|
AUDIT_NATS_KEYS.iter().map(|s| s.to_string()).collect()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub struct PulsarTargetFactory;
|
||||||
|
|
||||||
|
#[async_trait]
|
||||||
|
impl TargetFactory for PulsarTargetFactory {
|
||||||
|
async fn create_target(&self, id: String, config: &KVS) -> Result<Box<dyn Target<AuditEntry> + Send + Sync>, TargetError> {
|
||||||
|
let args = build_pulsar_args(config, AUDIT_DEFAULT_DIR, TargetType::AuditLog)?;
|
||||||
|
let target = rustfs_targets::target::pulsar::PulsarTarget::new(id, args)?;
|
||||||
|
Ok(Box::new(target))
|
||||||
|
}
|
||||||
|
|
||||||
|
fn validate_config(&self, _id: &str, config: &KVS) -> Result<(), TargetError> {
|
||||||
|
validate_pulsar_config(config, AUDIT_DEFAULT_DIR)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn get_valid_fields(&self) -> HashSet<String> {
|
||||||
|
AUDIT_PULSAR_KEYS.iter().map(|s| s.to_string()).collect()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
+15
-135
@@ -14,18 +14,17 @@
|
|||||||
|
|
||||||
use crate::{
|
use crate::{
|
||||||
AuditEntry, AuditError, AuditResult,
|
AuditEntry, AuditError, AuditResult,
|
||||||
factory::{MQTTTargetFactory, TargetFactory, WebhookTargetFactory},
|
factory::{MQTTTargetFactory, NATSTargetFactory, PulsarTargetFactory, TargetFactory, WebhookTargetFactory},
|
||||||
};
|
};
|
||||||
use futures::StreamExt;
|
use futures::StreamExt;
|
||||||
use futures::stream::FuturesUnordered;
|
use futures::stream::FuturesUnordered;
|
||||||
use hashbrown::{HashMap, HashSet};
|
use hashbrown::HashMap;
|
||||||
use rustfs_config::{DEFAULT_DELIMITER, ENABLE_KEY, ENV_PREFIX, EnableState, audit::AUDIT_ROUTE_PREFIX};
|
use rustfs_config::audit::AUDIT_ROUTE_PREFIX;
|
||||||
use rustfs_ecstore::config::{Config, KVS};
|
use rustfs_ecstore::config::{Config, KVS};
|
||||||
use rustfs_targets::arn::TargetID;
|
use rustfs_targets::arn::TargetID;
|
||||||
use rustfs_targets::{Target, TargetError, target::ChannelTargetType};
|
use rustfs_targets::{Target, TargetError, config::collect_target_configs, target::ChannelTargetType};
|
||||||
use std::str::FromStr;
|
|
||||||
use std::sync::Arc;
|
use std::sync::Arc;
|
||||||
use tracing::{debug, error, info, warn};
|
use tracing::{error, info};
|
||||||
|
|
||||||
/// Registry for managing audit targets
|
/// Registry for managing audit targets
|
||||||
pub struct AuditRegistry {
|
pub struct AuditRegistry {
|
||||||
@@ -52,6 +51,8 @@ impl AuditRegistry {
|
|||||||
// Register built-in factories
|
// Register built-in factories
|
||||||
registry.register(ChannelTargetType::Webhook.as_str(), Box::new(WebhookTargetFactory));
|
registry.register(ChannelTargetType::Webhook.as_str(), Box::new(WebhookTargetFactory));
|
||||||
registry.register(ChannelTargetType::Mqtt.as_str(), Box::new(MQTTTargetFactory));
|
registry.register(ChannelTargetType::Mqtt.as_str(), Box::new(MQTTTargetFactory));
|
||||||
|
registry.register(ChannelTargetType::Nats.as_str(), Box::new(NATSTargetFactory));
|
||||||
|
registry.register(ChannelTargetType::Pulsar.as_str(), Box::new(PulsarTargetFactory));
|
||||||
|
|
||||||
registry
|
registry
|
||||||
}
|
}
|
||||||
@@ -105,143 +106,22 @@ impl AuditRegistry {
|
|||||||
&self,
|
&self,
|
||||||
config: &Config,
|
config: &Config,
|
||||||
) -> AuditResult<Vec<Box<dyn Target<AuditEntry> + Send + Sync>>> {
|
) -> AuditResult<Vec<Box<dyn Target<AuditEntry> + Send + Sync>>> {
|
||||||
// Collect only environment variables with the relevant prefix to reduce memory usage
|
|
||||||
let all_env: Vec<(String, String)> = std::env::vars().filter(|(key, _)| key.starts_with(ENV_PREFIX)).collect();
|
|
||||||
// A collection of asynchronous tasks for concurrently executing target creation
|
|
||||||
let mut tasks = FuturesUnordered::new();
|
let mut tasks = FuturesUnordered::new();
|
||||||
// 1. Traverse all registered plants and process them by target type
|
|
||||||
for (target_type, factory) in &self.factories {
|
for (target_type, factory) in &self.factories {
|
||||||
tracing::Span::current().record("target_type", target_type.as_str());
|
tracing::Span::current().record("target_type", target_type.as_str());
|
||||||
info!("Start working on target types...");
|
info!("Start working on target types...");
|
||||||
|
|
||||||
// 2. Prepare the configuration source
|
|
||||||
// 2.1. Get the configuration segment in the file, e.g. 'audit_webhook'
|
|
||||||
let section_name = format!("{AUDIT_ROUTE_PREFIX}{target_type}").to_lowercase();
|
|
||||||
let file_configs = config.0.get(§ion_name).cloned().unwrap_or_default();
|
|
||||||
// 2.2. Get the default configuration for that type
|
|
||||||
let default_cfg = file_configs.get(DEFAULT_DELIMITER).cloned().unwrap_or_default();
|
|
||||||
debug!(?default_cfg, "Get the default configuration");
|
|
||||||
|
|
||||||
// *** Optimization point 1: Get all legitimate fields of the current target type ***
|
|
||||||
let valid_fields = factory.get_valid_fields();
|
let valid_fields = factory.get_valid_fields();
|
||||||
debug!(?valid_fields, "Get the legitimate configuration fields");
|
for (id, merged_config) in collect_target_configs(config, AUDIT_ROUTE_PREFIX, target_type, &valid_fields) {
|
||||||
|
info!(instance_id = %id, "Target is enabled, ready to create a task");
|
||||||
// 3. Resolve instance IDs and configuration overrides from environment variables
|
let tid = id.clone();
|
||||||
let mut instance_ids_from_env = HashSet::new();
|
let merged_config_arc = Arc::new(merged_config);
|
||||||
// 3.1. Instance discovery: Based on the '..._ENABLE_INSTANCEID' format
|
tasks.push(async move {
|
||||||
let enable_prefix =
|
let result = factory.create_target(tid.clone(), &merged_config_arc).await;
|
||||||
format!("{ENV_PREFIX}{AUDIT_ROUTE_PREFIX}{target_type}{DEFAULT_DELIMITER}{ENABLE_KEY}{DEFAULT_DELIMITER}")
|
(tid, result)
|
||||||
.to_uppercase();
|
});
|
||||||
for (key, value) in &all_env {
|
|
||||||
if EnableState::from_str(value).ok().map(|s| s.is_enabled()).unwrap_or(false)
|
|
||||||
&& let Some(id) = key.strip_prefix(&enable_prefix)
|
|
||||||
&& !id.is_empty()
|
|
||||||
{
|
|
||||||
instance_ids_from_env.insert(id.to_lowercase());
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// 3.2. Parse all relevant environment variable configurations
|
|
||||||
// 3.2.1. Build environment variable prefixes such as 'RUSTFS_AUDIT_WEBHOOK_'
|
|
||||||
let env_prefix = format!("{ENV_PREFIX}{AUDIT_ROUTE_PREFIX}{target_type}{DEFAULT_DELIMITER}").to_uppercase();
|
|
||||||
// 3.2.2. 'env_overrides' is used to store configurations parsed from environment variables in the format: {instance id -> {field -> value}}
|
|
||||||
let mut env_overrides: HashMap<String, HashMap<String, String>> = HashMap::new();
|
|
||||||
for (key, value) in &all_env {
|
|
||||||
if let Some(rest) = key.strip_prefix(&env_prefix) {
|
|
||||||
// Use rsplitn to split from the right side to properly extract the INSTANCE_ID at the end
|
|
||||||
// Format: <FIELD_NAME>_<INSTANCE_ID> or <FIELD_NAME>
|
|
||||||
let mut parts = rest.rsplitn(2, DEFAULT_DELIMITER);
|
|
||||||
|
|
||||||
// The first part from the right is INSTANCE_ID
|
|
||||||
let instance_id_part = parts.next().unwrap_or(DEFAULT_DELIMITER);
|
|
||||||
// The remaining part is FIELD_NAME
|
|
||||||
let field_name_part = parts.next();
|
|
||||||
|
|
||||||
let (field_name, instance_id) = match field_name_part {
|
|
||||||
// Case 1: The format is <FIELD_NAME>_<INSTANCE_ID>
|
|
||||||
// e.g., rest = "ENDPOINT_PRIMARY" -> field_name="ENDPOINT", instance_id="PRIMARY"
|
|
||||||
Some(field) => (field.to_lowercase(), instance_id_part.to_lowercase()),
|
|
||||||
// Case 2: The format is <FIELD_NAME> (without INSTANCE_ID)
|
|
||||||
// e.g., rest = "ENABLE" -> field_name="ENABLE", instance_id="" (Universal configuration `_ DEFAULT_DELIMITER`)
|
|
||||||
None => (instance_id_part.to_lowercase(), DEFAULT_DELIMITER.to_string()),
|
|
||||||
};
|
|
||||||
|
|
||||||
// *** Optimization point 2: Verify whether the parsed field_name is legal ***
|
|
||||||
if !field_name.is_empty() && valid_fields.contains(&field_name) {
|
|
||||||
debug!(
|
|
||||||
instance_id = %if instance_id.is_empty() { DEFAULT_DELIMITER } else { &instance_id },
|
|
||||||
%field_name,
|
|
||||||
%value,
|
|
||||||
"Parsing to environment variables"
|
|
||||||
);
|
|
||||||
env_overrides
|
|
||||||
.entry(instance_id)
|
|
||||||
.or_default()
|
|
||||||
.insert(field_name, value.clone());
|
|
||||||
} else {
|
|
||||||
// Ignore illegal field names
|
|
||||||
warn!(
|
|
||||||
field_name = %field_name,
|
|
||||||
"Ignore environment variable fields, not found in the list of valid fields for target type {}",
|
|
||||||
target_type
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
debug!(?env_overrides, "Complete the environment variable analysis");
|
|
||||||
|
|
||||||
// 4. Determine all instance IDs that need to be processed
|
|
||||||
let mut all_instance_ids: HashSet<String> =
|
|
||||||
file_configs.keys().filter(|k| *k != DEFAULT_DELIMITER).cloned().collect();
|
|
||||||
all_instance_ids.extend(instance_ids_from_env);
|
|
||||||
debug!(?all_instance_ids, "Determine all instance IDs");
|
|
||||||
|
|
||||||
// 5. Merge configurations and create tasks for each instance
|
|
||||||
for id in all_instance_ids {
|
|
||||||
// 5.1. Merge configuration, priority: Environment variables > File instance configuration > File default configuration
|
|
||||||
let mut merged_config = default_cfg.clone();
|
|
||||||
// Instance-specific configuration in application files
|
|
||||||
if let Some(file_instance_cfg) = file_configs.get(&id) {
|
|
||||||
merged_config.extend(file_instance_cfg.clone());
|
|
||||||
}
|
|
||||||
// Application instance-specific environment variable configuration
|
|
||||||
if let Some(env_instance_cfg) = env_overrides.get(&id) {
|
|
||||||
// Convert HashMap<String, String> to KVS
|
|
||||||
let mut kvs_from_env = KVS::new();
|
|
||||||
for (k, v) in env_instance_cfg {
|
|
||||||
kvs_from_env.insert(k.clone(), v.clone());
|
|
||||||
}
|
|
||||||
merged_config.extend(kvs_from_env);
|
|
||||||
}
|
|
||||||
debug!(instance_id = %id, ?merged_config, "Complete configuration merge");
|
|
||||||
|
|
||||||
// 5.2. Check if the instance is enabled
|
|
||||||
let enabled = merged_config
|
|
||||||
.lookup(ENABLE_KEY)
|
|
||||||
.map(|v| {
|
|
||||||
EnableState::from_str(v.as_str())
|
|
||||||
.ok()
|
|
||||||
.map(|s| s.is_enabled())
|
|
||||||
.unwrap_or(false)
|
|
||||||
})
|
|
||||||
.unwrap_or(false);
|
|
||||||
|
|
||||||
if enabled {
|
|
||||||
info!(instance_id = %id, "Target is enabled, ready to create a task");
|
|
||||||
// 5.3. Create asynchronous tasks for enabled instances
|
|
||||||
let tid = id.clone();
|
|
||||||
let merged_config_arc = Arc::new(merged_config);
|
|
||||||
tasks.push(async move {
|
|
||||||
let result = factory.create_target(tid.clone(), &merged_config_arc).await;
|
|
||||||
(tid, result)
|
|
||||||
});
|
|
||||||
} else {
|
|
||||||
info!(instance_id = %id, "Skip disabled target");
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// 6. Concurrently execute all creation tasks and collect results
|
|
||||||
let mut successful_targets = Vec::new();
|
let mut successful_targets = Vec::new();
|
||||||
while let Some((id, result)) = tasks.next().await {
|
while let Some((id, result)) = tasks.next().await {
|
||||||
match result {
|
match result {
|
||||||
|
|||||||
@@ -17,9 +17,13 @@
|
|||||||
//! webhook and MQTT audit-related settings.
|
//! webhook and MQTT audit-related settings.
|
||||||
|
|
||||||
mod mqtt;
|
mod mqtt;
|
||||||
|
mod nats;
|
||||||
|
mod pulsar;
|
||||||
mod webhook;
|
mod webhook;
|
||||||
|
|
||||||
pub use mqtt::*;
|
pub use mqtt::*;
|
||||||
|
pub use nats::*;
|
||||||
|
pub use pulsar::*;
|
||||||
pub use webhook::*;
|
pub use webhook::*;
|
||||||
|
|
||||||
use crate::DEFAULT_DELIMITER;
|
use crate::DEFAULT_DELIMITER;
|
||||||
@@ -30,7 +34,14 @@ pub const AUDIT_ROUTE_PREFIX: &str = const_str::concat!(AUDIT_PREFIX, DEFAULT_DE
|
|||||||
|
|
||||||
pub const AUDIT_WEBHOOK_SUB_SYS: &str = "audit_webhook";
|
pub const AUDIT_WEBHOOK_SUB_SYS: &str = "audit_webhook";
|
||||||
pub const AUDIT_MQTT_SUB_SYS: &str = "audit_mqtt";
|
pub const AUDIT_MQTT_SUB_SYS: &str = "audit_mqtt";
|
||||||
|
pub const AUDIT_NATS_SUB_SYS: &str = "audit_nats";
|
||||||
|
pub const AUDIT_PULSAR_SUB_SYS: &str = "audit_pulsar";
|
||||||
|
|
||||||
pub const AUDIT_STORE_EXTENSION: &str = ".audit";
|
pub const AUDIT_STORE_EXTENSION: &str = ".audit";
|
||||||
#[allow(dead_code)]
|
#[allow(dead_code)]
|
||||||
pub const AUDIT_SUB_SYSTEMS: &[&str] = &[AUDIT_MQTT_SUB_SYS, AUDIT_WEBHOOK_SUB_SYS];
|
pub const AUDIT_SUB_SYSTEMS: &[&str] = &[
|
||||||
|
AUDIT_MQTT_SUB_SYS,
|
||||||
|
AUDIT_NATS_SUB_SYS,
|
||||||
|
AUDIT_PULSAR_SUB_SYS,
|
||||||
|
AUDIT_WEBHOOK_SUB_SYS,
|
||||||
|
];
|
||||||
|
|||||||
@@ -0,0 +1,60 @@
|
|||||||
|
// 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.
|
||||||
|
|
||||||
|
pub const ENV_AUDIT_NATS_ENABLE: &str = "RUSTFS_AUDIT_NATS_ENABLE";
|
||||||
|
pub const ENV_AUDIT_NATS_ADDRESS: &str = "RUSTFS_AUDIT_NATS_ADDRESS";
|
||||||
|
pub const ENV_AUDIT_NATS_SUBJECT: &str = "RUSTFS_AUDIT_NATS_SUBJECT";
|
||||||
|
pub const ENV_AUDIT_NATS_USERNAME: &str = "RUSTFS_AUDIT_NATS_USERNAME";
|
||||||
|
pub const ENV_AUDIT_NATS_PASSWORD: &str = "RUSTFS_AUDIT_NATS_PASSWORD";
|
||||||
|
pub const ENV_AUDIT_NATS_TOKEN: &str = "RUSTFS_AUDIT_NATS_TOKEN";
|
||||||
|
pub const ENV_AUDIT_NATS_CREDENTIALS_FILE: &str = "RUSTFS_AUDIT_NATS_CREDENTIALS_FILE";
|
||||||
|
pub const ENV_AUDIT_NATS_TLS_CA: &str = "RUSTFS_AUDIT_NATS_TLS_CA";
|
||||||
|
pub const ENV_AUDIT_NATS_TLS_CLIENT_CERT: &str = "RUSTFS_AUDIT_NATS_TLS_CLIENT_CERT";
|
||||||
|
pub const ENV_AUDIT_NATS_TLS_CLIENT_KEY: &str = "RUSTFS_AUDIT_NATS_TLS_CLIENT_KEY";
|
||||||
|
pub const ENV_AUDIT_NATS_TLS_REQUIRED: &str = "RUSTFS_AUDIT_NATS_TLS_REQUIRED";
|
||||||
|
pub const ENV_AUDIT_NATS_QUEUE_DIR: &str = "RUSTFS_AUDIT_NATS_QUEUE_DIR";
|
||||||
|
pub const ENV_AUDIT_NATS_QUEUE_LIMIT: &str = "RUSTFS_AUDIT_NATS_QUEUE_LIMIT";
|
||||||
|
|
||||||
|
pub const ENV_AUDIT_NATS_KEYS: &[&str; 13] = &[
|
||||||
|
ENV_AUDIT_NATS_ENABLE,
|
||||||
|
ENV_AUDIT_NATS_ADDRESS,
|
||||||
|
ENV_AUDIT_NATS_SUBJECT,
|
||||||
|
ENV_AUDIT_NATS_USERNAME,
|
||||||
|
ENV_AUDIT_NATS_PASSWORD,
|
||||||
|
ENV_AUDIT_NATS_TOKEN,
|
||||||
|
ENV_AUDIT_NATS_CREDENTIALS_FILE,
|
||||||
|
ENV_AUDIT_NATS_TLS_CA,
|
||||||
|
ENV_AUDIT_NATS_TLS_CLIENT_CERT,
|
||||||
|
ENV_AUDIT_NATS_TLS_CLIENT_KEY,
|
||||||
|
ENV_AUDIT_NATS_TLS_REQUIRED,
|
||||||
|
ENV_AUDIT_NATS_QUEUE_DIR,
|
||||||
|
ENV_AUDIT_NATS_QUEUE_LIMIT,
|
||||||
|
];
|
||||||
|
|
||||||
|
pub const AUDIT_NATS_KEYS: &[&str] = &[
|
||||||
|
crate::ENABLE_KEY,
|
||||||
|
crate::NATS_ADDRESS,
|
||||||
|
crate::NATS_SUBJECT,
|
||||||
|
crate::NATS_USERNAME,
|
||||||
|
crate::NATS_PASSWORD,
|
||||||
|
crate::NATS_TOKEN,
|
||||||
|
crate::NATS_CREDENTIALS_FILE,
|
||||||
|
crate::NATS_TLS_CA,
|
||||||
|
crate::NATS_TLS_CLIENT_CERT,
|
||||||
|
crate::NATS_TLS_CLIENT_KEY,
|
||||||
|
crate::NATS_TLS_REQUIRED,
|
||||||
|
crate::NATS_QUEUE_DIR,
|
||||||
|
crate::NATS_QUEUE_LIMIT,
|
||||||
|
crate::COMMENT_KEY,
|
||||||
|
];
|
||||||
@@ -0,0 +1,54 @@
|
|||||||
|
// 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.
|
||||||
|
|
||||||
|
pub const ENV_AUDIT_PULSAR_ENABLE: &str = "RUSTFS_AUDIT_PULSAR_ENABLE";
|
||||||
|
pub const ENV_AUDIT_PULSAR_BROKER: &str = "RUSTFS_AUDIT_PULSAR_BROKER";
|
||||||
|
pub const ENV_AUDIT_PULSAR_TOPIC: &str = "RUSTFS_AUDIT_PULSAR_TOPIC";
|
||||||
|
pub const ENV_AUDIT_PULSAR_AUTH_TOKEN: &str = "RUSTFS_AUDIT_PULSAR_AUTH_TOKEN";
|
||||||
|
pub const ENV_AUDIT_PULSAR_USERNAME: &str = "RUSTFS_AUDIT_PULSAR_USERNAME";
|
||||||
|
pub const ENV_AUDIT_PULSAR_PASSWORD: &str = "RUSTFS_AUDIT_PULSAR_PASSWORD";
|
||||||
|
pub const ENV_AUDIT_PULSAR_TLS_CA: &str = "RUSTFS_AUDIT_PULSAR_TLS_CA";
|
||||||
|
pub const ENV_AUDIT_PULSAR_TLS_ALLOW_INSECURE: &str = "RUSTFS_AUDIT_PULSAR_TLS_ALLOW_INSECURE";
|
||||||
|
pub const ENV_AUDIT_PULSAR_TLS_HOSTNAME_VERIFICATION: &str = "RUSTFS_AUDIT_PULSAR_TLS_HOSTNAME_VERIFICATION";
|
||||||
|
pub const ENV_AUDIT_PULSAR_QUEUE_DIR: &str = "RUSTFS_AUDIT_PULSAR_QUEUE_DIR";
|
||||||
|
pub const ENV_AUDIT_PULSAR_QUEUE_LIMIT: &str = "RUSTFS_AUDIT_PULSAR_QUEUE_LIMIT";
|
||||||
|
|
||||||
|
pub const ENV_AUDIT_PULSAR_KEYS: &[&str; 11] = &[
|
||||||
|
ENV_AUDIT_PULSAR_ENABLE,
|
||||||
|
ENV_AUDIT_PULSAR_BROKER,
|
||||||
|
ENV_AUDIT_PULSAR_TOPIC,
|
||||||
|
ENV_AUDIT_PULSAR_AUTH_TOKEN,
|
||||||
|
ENV_AUDIT_PULSAR_USERNAME,
|
||||||
|
ENV_AUDIT_PULSAR_PASSWORD,
|
||||||
|
ENV_AUDIT_PULSAR_TLS_CA,
|
||||||
|
ENV_AUDIT_PULSAR_TLS_ALLOW_INSECURE,
|
||||||
|
ENV_AUDIT_PULSAR_TLS_HOSTNAME_VERIFICATION,
|
||||||
|
ENV_AUDIT_PULSAR_QUEUE_DIR,
|
||||||
|
ENV_AUDIT_PULSAR_QUEUE_LIMIT,
|
||||||
|
];
|
||||||
|
|
||||||
|
pub const AUDIT_PULSAR_KEYS: &[&str] = &[
|
||||||
|
crate::ENABLE_KEY,
|
||||||
|
crate::PULSAR_BROKER,
|
||||||
|
crate::PULSAR_TOPIC,
|
||||||
|
crate::PULSAR_AUTH_TOKEN,
|
||||||
|
crate::PULSAR_USERNAME,
|
||||||
|
crate::PULSAR_PASSWORD,
|
||||||
|
crate::PULSAR_TLS_CA,
|
||||||
|
crate::PULSAR_TLS_ALLOW_INSECURE,
|
||||||
|
crate::PULSAR_TLS_HOSTNAME_VERIFICATION,
|
||||||
|
crate::PULSAR_QUEUE_DIR,
|
||||||
|
crate::PULSAR_QUEUE_LIMIT,
|
||||||
|
crate::COMMENT_KEY,
|
||||||
|
];
|
||||||
@@ -41,6 +41,30 @@ pub const MQTT_TLS_CLIENT_KEY: &str = "tls_client_key";
|
|||||||
pub const MQTT_TLS_TRUST_LEAF_AS_CA: &str = "tls_trust_leaf_as_ca";
|
pub const MQTT_TLS_TRUST_LEAF_AS_CA: &str = "tls_trust_leaf_as_ca";
|
||||||
pub const MQTT_WS_PATH_ALLOWLIST: &str = "ws_path_allowlist";
|
pub const MQTT_WS_PATH_ALLOWLIST: &str = "ws_path_allowlist";
|
||||||
|
|
||||||
|
pub const NATS_ADDRESS: &str = "address";
|
||||||
|
pub const NATS_SUBJECT: &str = "subject";
|
||||||
|
pub const NATS_USERNAME: &str = "username";
|
||||||
|
pub const NATS_PASSWORD: &str = "password";
|
||||||
|
pub const NATS_TOKEN: &str = "token";
|
||||||
|
pub const NATS_CREDENTIALS_FILE: &str = "credentials_file";
|
||||||
|
pub const NATS_TLS_CA: &str = "tls_ca";
|
||||||
|
pub const NATS_TLS_CLIENT_CERT: &str = "tls_client_cert";
|
||||||
|
pub const NATS_TLS_CLIENT_KEY: &str = "tls_client_key";
|
||||||
|
pub const NATS_TLS_REQUIRED: &str = "tls_required";
|
||||||
|
pub const NATS_QUEUE_DIR: &str = "queue_dir";
|
||||||
|
pub const NATS_QUEUE_LIMIT: &str = "queue_limit";
|
||||||
|
|
||||||
|
pub const PULSAR_BROKER: &str = "broker";
|
||||||
|
pub const PULSAR_TOPIC: &str = "topic";
|
||||||
|
pub const PULSAR_AUTH_TOKEN: &str = "auth_token";
|
||||||
|
pub const PULSAR_USERNAME: &str = "username";
|
||||||
|
pub const PULSAR_PASSWORD: &str = "password";
|
||||||
|
pub const PULSAR_TLS_CA: &str = "tls_ca";
|
||||||
|
pub const PULSAR_TLS_ALLOW_INSECURE: &str = "tls_allow_insecure";
|
||||||
|
pub const PULSAR_TLS_HOSTNAME_VERIFICATION: &str = "tls_hostname_verification";
|
||||||
|
pub const PULSAR_QUEUE_DIR: &str = "queue_dir";
|
||||||
|
pub const PULSAR_QUEUE_LIMIT: &str = "queue_limit";
|
||||||
|
|
||||||
/// Environment variable controlling whether target queue files are Snappy-compressed.
|
/// Environment variable controlling whether target queue files are Snappy-compressed.
|
||||||
/// Applies to both notify and audit target queue stores.
|
/// Applies to both notify and audit target queue stores.
|
||||||
pub const ENV_TARGET_STORE_COMPRESS: &str = "RUSTFS_TARGET_STORE_COMPRESS";
|
pub const ENV_TARGET_STORE_COMPRESS: &str = "RUSTFS_TARGET_STORE_COMPRESS";
|
||||||
|
|||||||
@@ -14,11 +14,15 @@
|
|||||||
|
|
||||||
mod arn;
|
mod arn;
|
||||||
mod mqtt;
|
mod mqtt;
|
||||||
|
mod nats;
|
||||||
|
mod pulsar;
|
||||||
mod store;
|
mod store;
|
||||||
mod webhook;
|
mod webhook;
|
||||||
|
|
||||||
pub use arn::*;
|
pub use arn::*;
|
||||||
pub use mqtt::*;
|
pub use mqtt::*;
|
||||||
|
pub use nats::*;
|
||||||
|
pub use pulsar::*;
|
||||||
pub use store::*;
|
pub use store::*;
|
||||||
pub use webhook::*;
|
pub use webhook::*;
|
||||||
|
|
||||||
@@ -64,7 +68,12 @@ pub const ENV_NOTIFY_SEND_CONCURRENCY: &str = "RUSTFS_NOTIFY_SEND_CONCURRENCY";
|
|||||||
pub const DEFAULT_NOTIFY_SEND_CONCURRENCY: usize = 64;
|
pub const DEFAULT_NOTIFY_SEND_CONCURRENCY: usize = 64;
|
||||||
|
|
||||||
#[allow(dead_code)]
|
#[allow(dead_code)]
|
||||||
pub const NOTIFY_SUB_SYSTEMS: &[&str] = &[NOTIFY_MQTT_SUB_SYS, NOTIFY_WEBHOOK_SUB_SYS];
|
pub const NOTIFY_SUB_SYSTEMS: &[&str] = &[
|
||||||
|
NOTIFY_MQTT_SUB_SYS,
|
||||||
|
NOTIFY_NATS_SUB_SYS,
|
||||||
|
NOTIFY_PULSAR_SUB_SYS,
|
||||||
|
NOTIFY_WEBHOOK_SUB_SYS,
|
||||||
|
];
|
||||||
|
|
||||||
#[allow(dead_code)]
|
#[allow(dead_code)]
|
||||||
pub const NOTIFY_KAFKA_SUB_SYS: &str = "notify_kafka";
|
pub const NOTIFY_KAFKA_SUB_SYS: &str = "notify_kafka";
|
||||||
@@ -83,4 +92,5 @@ pub const NOTIFY_AMQP_SUB_SYS: &str = "notify_amqp";
|
|||||||
pub const NOTIFY_POSTGRES_SUB_SYS: &str = "notify_postgres";
|
pub const NOTIFY_POSTGRES_SUB_SYS: &str = "notify_postgres";
|
||||||
#[allow(dead_code)]
|
#[allow(dead_code)]
|
||||||
pub const NOTIFY_REDIS_SUB_SYS: &str = "notify_redis";
|
pub const NOTIFY_REDIS_SUB_SYS: &str = "notify_redis";
|
||||||
|
pub const NOTIFY_PULSAR_SUB_SYS: &str = "notify_pulsar";
|
||||||
pub const NOTIFY_WEBHOOK_SUB_SYS: &str = "notify_webhook";
|
pub const NOTIFY_WEBHOOK_SUB_SYS: &str = "notify_webhook";
|
||||||
|
|||||||
@@ -0,0 +1,60 @@
|
|||||||
|
// 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.
|
||||||
|
|
||||||
|
pub const NOTIFY_NATS_KEYS: &[&str] = &[
|
||||||
|
crate::ENABLE_KEY,
|
||||||
|
crate::NATS_ADDRESS,
|
||||||
|
crate::NATS_SUBJECT,
|
||||||
|
crate::NATS_USERNAME,
|
||||||
|
crate::NATS_PASSWORD,
|
||||||
|
crate::NATS_TOKEN,
|
||||||
|
crate::NATS_CREDENTIALS_FILE,
|
||||||
|
crate::NATS_TLS_CA,
|
||||||
|
crate::NATS_TLS_CLIENT_CERT,
|
||||||
|
crate::NATS_TLS_CLIENT_KEY,
|
||||||
|
crate::NATS_TLS_REQUIRED,
|
||||||
|
crate::NATS_QUEUE_DIR,
|
||||||
|
crate::NATS_QUEUE_LIMIT,
|
||||||
|
crate::COMMENT_KEY,
|
||||||
|
];
|
||||||
|
|
||||||
|
pub const ENV_NOTIFY_NATS_ENABLE: &str = "RUSTFS_NOTIFY_NATS_ENABLE";
|
||||||
|
pub const ENV_NOTIFY_NATS_ADDRESS: &str = "RUSTFS_NOTIFY_NATS_ADDRESS";
|
||||||
|
pub const ENV_NOTIFY_NATS_SUBJECT: &str = "RUSTFS_NOTIFY_NATS_SUBJECT";
|
||||||
|
pub const ENV_NOTIFY_NATS_USERNAME: &str = "RUSTFS_NOTIFY_NATS_USERNAME";
|
||||||
|
pub const ENV_NOTIFY_NATS_PASSWORD: &str = "RUSTFS_NOTIFY_NATS_PASSWORD";
|
||||||
|
pub const ENV_NOTIFY_NATS_TOKEN: &str = "RUSTFS_NOTIFY_NATS_TOKEN";
|
||||||
|
pub const ENV_NOTIFY_NATS_CREDENTIALS_FILE: &str = "RUSTFS_NOTIFY_NATS_CREDENTIALS_FILE";
|
||||||
|
pub const ENV_NOTIFY_NATS_TLS_CA: &str = "RUSTFS_NOTIFY_NATS_TLS_CA";
|
||||||
|
pub const ENV_NOTIFY_NATS_TLS_CLIENT_CERT: &str = "RUSTFS_NOTIFY_NATS_TLS_CLIENT_CERT";
|
||||||
|
pub const ENV_NOTIFY_NATS_TLS_CLIENT_KEY: &str = "RUSTFS_NOTIFY_NATS_TLS_CLIENT_KEY";
|
||||||
|
pub const ENV_NOTIFY_NATS_TLS_REQUIRED: &str = "RUSTFS_NOTIFY_NATS_TLS_REQUIRED";
|
||||||
|
pub const ENV_NOTIFY_NATS_QUEUE_DIR: &str = "RUSTFS_NOTIFY_NATS_QUEUE_DIR";
|
||||||
|
pub const ENV_NOTIFY_NATS_QUEUE_LIMIT: &str = "RUSTFS_NOTIFY_NATS_QUEUE_LIMIT";
|
||||||
|
|
||||||
|
pub const ENV_NOTIFY_NATS_KEYS: &[&str; 13] = &[
|
||||||
|
ENV_NOTIFY_NATS_ENABLE,
|
||||||
|
ENV_NOTIFY_NATS_ADDRESS,
|
||||||
|
ENV_NOTIFY_NATS_SUBJECT,
|
||||||
|
ENV_NOTIFY_NATS_USERNAME,
|
||||||
|
ENV_NOTIFY_NATS_PASSWORD,
|
||||||
|
ENV_NOTIFY_NATS_TOKEN,
|
||||||
|
ENV_NOTIFY_NATS_CREDENTIALS_FILE,
|
||||||
|
ENV_NOTIFY_NATS_TLS_CA,
|
||||||
|
ENV_NOTIFY_NATS_TLS_CLIENT_CERT,
|
||||||
|
ENV_NOTIFY_NATS_TLS_CLIENT_KEY,
|
||||||
|
ENV_NOTIFY_NATS_TLS_REQUIRED,
|
||||||
|
ENV_NOTIFY_NATS_QUEUE_DIR,
|
||||||
|
ENV_NOTIFY_NATS_QUEUE_LIMIT,
|
||||||
|
];
|
||||||
@@ -0,0 +1,54 @@
|
|||||||
|
// 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.
|
||||||
|
|
||||||
|
pub const NOTIFY_PULSAR_KEYS: &[&str] = &[
|
||||||
|
crate::ENABLE_KEY,
|
||||||
|
crate::PULSAR_BROKER,
|
||||||
|
crate::PULSAR_TOPIC,
|
||||||
|
crate::PULSAR_AUTH_TOKEN,
|
||||||
|
crate::PULSAR_USERNAME,
|
||||||
|
crate::PULSAR_PASSWORD,
|
||||||
|
crate::PULSAR_TLS_CA,
|
||||||
|
crate::PULSAR_TLS_ALLOW_INSECURE,
|
||||||
|
crate::PULSAR_TLS_HOSTNAME_VERIFICATION,
|
||||||
|
crate::PULSAR_QUEUE_DIR,
|
||||||
|
crate::PULSAR_QUEUE_LIMIT,
|
||||||
|
crate::COMMENT_KEY,
|
||||||
|
];
|
||||||
|
|
||||||
|
pub const ENV_NOTIFY_PULSAR_ENABLE: &str = "RUSTFS_NOTIFY_PULSAR_ENABLE";
|
||||||
|
pub const ENV_NOTIFY_PULSAR_BROKER: &str = "RUSTFS_NOTIFY_PULSAR_BROKER";
|
||||||
|
pub const ENV_NOTIFY_PULSAR_TOPIC: &str = "RUSTFS_NOTIFY_PULSAR_TOPIC";
|
||||||
|
pub const ENV_NOTIFY_PULSAR_AUTH_TOKEN: &str = "RUSTFS_NOTIFY_PULSAR_AUTH_TOKEN";
|
||||||
|
pub const ENV_NOTIFY_PULSAR_USERNAME: &str = "RUSTFS_NOTIFY_PULSAR_USERNAME";
|
||||||
|
pub const ENV_NOTIFY_PULSAR_PASSWORD: &str = "RUSTFS_NOTIFY_PULSAR_PASSWORD";
|
||||||
|
pub const ENV_NOTIFY_PULSAR_TLS_CA: &str = "RUSTFS_NOTIFY_PULSAR_TLS_CA";
|
||||||
|
pub const ENV_NOTIFY_PULSAR_TLS_ALLOW_INSECURE: &str = "RUSTFS_NOTIFY_PULSAR_TLS_ALLOW_INSECURE";
|
||||||
|
pub const ENV_NOTIFY_PULSAR_TLS_HOSTNAME_VERIFICATION: &str = "RUSTFS_NOTIFY_PULSAR_TLS_HOSTNAME_VERIFICATION";
|
||||||
|
pub const ENV_NOTIFY_PULSAR_QUEUE_DIR: &str = "RUSTFS_NOTIFY_PULSAR_QUEUE_DIR";
|
||||||
|
pub const ENV_NOTIFY_PULSAR_QUEUE_LIMIT: &str = "RUSTFS_NOTIFY_PULSAR_QUEUE_LIMIT";
|
||||||
|
|
||||||
|
pub const ENV_NOTIFY_PULSAR_KEYS: &[&str; 11] = &[
|
||||||
|
ENV_NOTIFY_PULSAR_ENABLE,
|
||||||
|
ENV_NOTIFY_PULSAR_BROKER,
|
||||||
|
ENV_NOTIFY_PULSAR_TOPIC,
|
||||||
|
ENV_NOTIFY_PULSAR_AUTH_TOKEN,
|
||||||
|
ENV_NOTIFY_PULSAR_USERNAME,
|
||||||
|
ENV_NOTIFY_PULSAR_PASSWORD,
|
||||||
|
ENV_NOTIFY_PULSAR_TLS_CA,
|
||||||
|
ENV_NOTIFY_PULSAR_TLS_ALLOW_INSECURE,
|
||||||
|
ENV_NOTIFY_PULSAR_TLS_HOSTNAME_VERIFICATION,
|
||||||
|
ENV_NOTIFY_PULSAR_QUEUE_DIR,
|
||||||
|
ENV_NOTIFY_PULSAR_QUEUE_LIMIT,
|
||||||
|
];
|
||||||
@@ -16,9 +16,13 @@ use crate::config::{KV, KVS};
|
|||||||
use rustfs_config::{
|
use rustfs_config::{
|
||||||
COMMENT_KEY, DEFAULT_LIMIT, ENABLE_KEY, EVENT_DEFAULT_DIR, EnableState, MQTT_BROKER, MQTT_KEEP_ALIVE_INTERVAL, MQTT_PASSWORD,
|
COMMENT_KEY, DEFAULT_LIMIT, ENABLE_KEY, EVENT_DEFAULT_DIR, EnableState, MQTT_BROKER, MQTT_KEEP_ALIVE_INTERVAL, MQTT_PASSWORD,
|
||||||
MQTT_QOS, MQTT_QUEUE_DIR, MQTT_QUEUE_LIMIT, MQTT_RECONNECT_INTERVAL, MQTT_TLS_CA, MQTT_TLS_CLIENT_CERT, MQTT_TLS_CLIENT_KEY,
|
MQTT_QOS, MQTT_QUEUE_DIR, MQTT_QUEUE_LIMIT, MQTT_RECONNECT_INTERVAL, MQTT_TLS_CA, MQTT_TLS_CLIENT_CERT, MQTT_TLS_CLIENT_KEY,
|
||||||
MQTT_TLS_POLICY, MQTT_TLS_TRUST_LEAF_AS_CA, MQTT_TOPIC, MQTT_USERNAME, MQTT_WS_PATH_ALLOWLIST, WEBHOOK_AUTH_TOKEN,
|
MQTT_TLS_POLICY, MQTT_TLS_TRUST_LEAF_AS_CA, MQTT_TOPIC, MQTT_USERNAME, MQTT_WS_PATH_ALLOWLIST, NATS_ADDRESS,
|
||||||
WEBHOOK_BATCH_SIZE, WEBHOOK_CLIENT_CA, WEBHOOK_CLIENT_CERT, WEBHOOK_CLIENT_KEY, WEBHOOK_ENDPOINT, WEBHOOK_HTTP_TIMEOUT,
|
NATS_CREDENTIALS_FILE, NATS_PASSWORD, NATS_QUEUE_DIR, NATS_QUEUE_LIMIT, NATS_SUBJECT, NATS_TLS_CA, NATS_TLS_CLIENT_CERT,
|
||||||
WEBHOOK_MAX_RETRY, WEBHOOK_QUEUE_DIR, WEBHOOK_QUEUE_LIMIT, WEBHOOK_RETRY_INTERVAL, WEBHOOK_SKIP_TLS_VERIFY,
|
NATS_TLS_CLIENT_KEY, NATS_TLS_REQUIRED, NATS_TOKEN, NATS_USERNAME, PULSAR_AUTH_TOKEN, PULSAR_BROKER, PULSAR_PASSWORD,
|
||||||
|
PULSAR_QUEUE_DIR, PULSAR_QUEUE_LIMIT, PULSAR_TLS_ALLOW_INSECURE, PULSAR_TLS_CA, PULSAR_TLS_HOSTNAME_VERIFICATION,
|
||||||
|
PULSAR_TOPIC, PULSAR_USERNAME, WEBHOOK_AUTH_TOKEN, WEBHOOK_BATCH_SIZE, WEBHOOK_CLIENT_CA, WEBHOOK_CLIENT_CERT,
|
||||||
|
WEBHOOK_CLIENT_KEY, WEBHOOK_ENDPOINT, WEBHOOK_HTTP_TIMEOUT, WEBHOOK_MAX_RETRY, WEBHOOK_QUEUE_DIR, WEBHOOK_QUEUE_LIMIT,
|
||||||
|
WEBHOOK_RETRY_INTERVAL, WEBHOOK_SKIP_TLS_VERIFY,
|
||||||
};
|
};
|
||||||
use std::sync::LazyLock;
|
use std::sync::LazyLock;
|
||||||
|
|
||||||
@@ -192,3 +196,144 @@ pub static DEFAULT_AUDIT_MQTT_KVS: LazyLock<KVS> = LazyLock::new(|| {
|
|||||||
},
|
},
|
||||||
])
|
])
|
||||||
});
|
});
|
||||||
|
|
||||||
|
pub static DEFAULT_AUDIT_NATS_KVS: LazyLock<KVS> = LazyLock::new(|| {
|
||||||
|
KVS(vec![
|
||||||
|
KV {
|
||||||
|
key: ENABLE_KEY.to_owned(),
|
||||||
|
value: EnableState::Off.to_string(),
|
||||||
|
hidden_if_empty: false,
|
||||||
|
},
|
||||||
|
KV {
|
||||||
|
key: NATS_ADDRESS.to_owned(),
|
||||||
|
value: "".to_owned(),
|
||||||
|
hidden_if_empty: false,
|
||||||
|
},
|
||||||
|
KV {
|
||||||
|
key: NATS_SUBJECT.to_owned(),
|
||||||
|
value: "".to_owned(),
|
||||||
|
hidden_if_empty: false,
|
||||||
|
},
|
||||||
|
KV {
|
||||||
|
key: NATS_USERNAME.to_owned(),
|
||||||
|
value: "".to_owned(),
|
||||||
|
hidden_if_empty: false,
|
||||||
|
},
|
||||||
|
KV {
|
||||||
|
key: NATS_PASSWORD.to_owned(),
|
||||||
|
value: "".to_owned(),
|
||||||
|
hidden_if_empty: true,
|
||||||
|
},
|
||||||
|
KV {
|
||||||
|
key: NATS_TOKEN.to_owned(),
|
||||||
|
value: "".to_owned(),
|
||||||
|
hidden_if_empty: true,
|
||||||
|
},
|
||||||
|
KV {
|
||||||
|
key: NATS_CREDENTIALS_FILE.to_owned(),
|
||||||
|
value: "".to_owned(),
|
||||||
|
hidden_if_empty: true,
|
||||||
|
},
|
||||||
|
KV {
|
||||||
|
key: NATS_TLS_CA.to_owned(),
|
||||||
|
value: "".to_owned(),
|
||||||
|
hidden_if_empty: true,
|
||||||
|
},
|
||||||
|
KV {
|
||||||
|
key: NATS_TLS_CLIENT_CERT.to_owned(),
|
||||||
|
value: "".to_owned(),
|
||||||
|
hidden_if_empty: true,
|
||||||
|
},
|
||||||
|
KV {
|
||||||
|
key: NATS_TLS_CLIENT_KEY.to_owned(),
|
||||||
|
value: "".to_owned(),
|
||||||
|
hidden_if_empty: true,
|
||||||
|
},
|
||||||
|
KV {
|
||||||
|
key: NATS_TLS_REQUIRED.to_owned(),
|
||||||
|
value: EnableState::Off.to_string(),
|
||||||
|
hidden_if_empty: false,
|
||||||
|
},
|
||||||
|
KV {
|
||||||
|
key: NATS_QUEUE_DIR.to_owned(),
|
||||||
|
value: EVENT_DEFAULT_DIR.to_owned(),
|
||||||
|
hidden_if_empty: false,
|
||||||
|
},
|
||||||
|
KV {
|
||||||
|
key: NATS_QUEUE_LIMIT.to_owned(),
|
||||||
|
value: DEFAULT_LIMIT.to_string(),
|
||||||
|
hidden_if_empty: false,
|
||||||
|
},
|
||||||
|
KV {
|
||||||
|
key: COMMENT_KEY.to_owned(),
|
||||||
|
value: "".to_owned(),
|
||||||
|
hidden_if_empty: false,
|
||||||
|
},
|
||||||
|
])
|
||||||
|
});
|
||||||
|
|
||||||
|
#[allow(dead_code)]
|
||||||
|
pub static DEFAULT_AUDIT_PULSAR_KVS: LazyLock<KVS> = LazyLock::new(|| {
|
||||||
|
KVS(vec![
|
||||||
|
KV {
|
||||||
|
key: ENABLE_KEY.to_owned(),
|
||||||
|
value: EnableState::Off.to_string(),
|
||||||
|
hidden_if_empty: false,
|
||||||
|
},
|
||||||
|
KV {
|
||||||
|
key: PULSAR_BROKER.to_owned(),
|
||||||
|
value: "".to_owned(),
|
||||||
|
hidden_if_empty: false,
|
||||||
|
},
|
||||||
|
KV {
|
||||||
|
key: PULSAR_TOPIC.to_owned(),
|
||||||
|
value: "".to_owned(),
|
||||||
|
hidden_if_empty: false,
|
||||||
|
},
|
||||||
|
KV {
|
||||||
|
key: PULSAR_AUTH_TOKEN.to_owned(),
|
||||||
|
value: "".to_owned(),
|
||||||
|
hidden_if_empty: true,
|
||||||
|
},
|
||||||
|
KV {
|
||||||
|
key: PULSAR_USERNAME.to_owned(),
|
||||||
|
value: "".to_owned(),
|
||||||
|
hidden_if_empty: false,
|
||||||
|
},
|
||||||
|
KV {
|
||||||
|
key: PULSAR_PASSWORD.to_owned(),
|
||||||
|
value: "".to_owned(),
|
||||||
|
hidden_if_empty: true,
|
||||||
|
},
|
||||||
|
KV {
|
||||||
|
key: PULSAR_TLS_CA.to_owned(),
|
||||||
|
value: "".to_owned(),
|
||||||
|
hidden_if_empty: true,
|
||||||
|
},
|
||||||
|
KV {
|
||||||
|
key: PULSAR_TLS_ALLOW_INSECURE.to_owned(),
|
||||||
|
value: EnableState::Off.to_string(),
|
||||||
|
hidden_if_empty: false,
|
||||||
|
},
|
||||||
|
KV {
|
||||||
|
key: PULSAR_TLS_HOSTNAME_VERIFICATION.to_owned(),
|
||||||
|
value: EnableState::On.to_string(),
|
||||||
|
hidden_if_empty: false,
|
||||||
|
},
|
||||||
|
KV {
|
||||||
|
key: PULSAR_QUEUE_DIR.to_owned(),
|
||||||
|
value: EVENT_DEFAULT_DIR.to_owned(),
|
||||||
|
hidden_if_empty: false,
|
||||||
|
},
|
||||||
|
KV {
|
||||||
|
key: PULSAR_QUEUE_LIMIT.to_owned(),
|
||||||
|
value: DEFAULT_LIMIT.to_string(),
|
||||||
|
hidden_if_empty: false,
|
||||||
|
},
|
||||||
|
KV {
|
||||||
|
key: COMMENT_KEY.to_owned(),
|
||||||
|
value: "".to_owned(),
|
||||||
|
hidden_if_empty: false,
|
||||||
|
},
|
||||||
|
])
|
||||||
|
});
|
||||||
|
|||||||
+143
-109
@@ -18,8 +18,14 @@ use crate::error::{Error, Result};
|
|||||||
use crate::global::is_first_cluster_node_local;
|
use crate::global::is_first_cluster_node_local;
|
||||||
use crate::store_api::{ObjectInfo, ObjectOptions, PutObjReader, StorageAPI};
|
use crate::store_api::{ObjectInfo, ObjectOptions, PutObjReader, StorageAPI};
|
||||||
use http::HeaderMap;
|
use http::HeaderMap;
|
||||||
use rustfs_config::audit::{AUDIT_MQTT_KEYS, AUDIT_MQTT_SUB_SYS, AUDIT_WEBHOOK_KEYS, AUDIT_WEBHOOK_SUB_SYS};
|
use rustfs_config::audit::{
|
||||||
use rustfs_config::notify::{NOTIFY_MQTT_KEYS, NOTIFY_MQTT_SUB_SYS, NOTIFY_WEBHOOK_KEYS, NOTIFY_WEBHOOK_SUB_SYS};
|
AUDIT_MQTT_KEYS, AUDIT_MQTT_SUB_SYS, AUDIT_NATS_KEYS, AUDIT_NATS_SUB_SYS, AUDIT_PULSAR_KEYS, AUDIT_PULSAR_SUB_SYS,
|
||||||
|
AUDIT_WEBHOOK_KEYS, AUDIT_WEBHOOK_SUB_SYS,
|
||||||
|
};
|
||||||
|
use rustfs_config::notify::{
|
||||||
|
NOTIFY_MQTT_KEYS, NOTIFY_MQTT_SUB_SYS, NOTIFY_NATS_KEYS, NOTIFY_NATS_SUB_SYS, NOTIFY_PULSAR_KEYS, NOTIFY_PULSAR_SUB_SYS,
|
||||||
|
NOTIFY_WEBHOOK_KEYS, NOTIFY_WEBHOOK_SUB_SYS,
|
||||||
|
};
|
||||||
use rustfs_config::oidc::{IDENTITY_OPENID_KEYS, IDENTITY_OPENID_SUB_SYS, OIDC_REDIRECT_URI_DYNAMIC};
|
use rustfs_config::oidc::{IDENTITY_OPENID_KEYS, IDENTITY_OPENID_SUB_SYS, OIDC_REDIRECT_URI_DYNAMIC};
|
||||||
use rustfs_config::{COMMENT_KEY, DEFAULT_DELIMITER, ENABLE_KEY, EnableState, RUSTFS_REGION};
|
use rustfs_config::{COMMENT_KEY, DEFAULT_DELIMITER, ENABLE_KEY, EnableState, RUSTFS_REGION};
|
||||||
use rustfs_utils::path::SLASH_SEPARATOR;
|
use rustfs_utils::path::SLASH_SEPARATOR;
|
||||||
@@ -42,6 +48,72 @@ static SUB_SYSTEMS_DYNAMIC: LazyLock<HashSet<String>> = LazyLock::new(|| {
|
|||||||
h
|
h
|
||||||
});
|
});
|
||||||
|
|
||||||
|
#[derive(Clone, Copy)]
|
||||||
|
struct TargetConfigDescriptor {
|
||||||
|
external_key: &'static str,
|
||||||
|
subsystem_key: &'static str,
|
||||||
|
default_kvs: &'static LazyLock<KVS>,
|
||||||
|
valid_keys: &'static [&'static str],
|
||||||
|
}
|
||||||
|
|
||||||
|
fn notify_target_descriptors() -> [TargetConfigDescriptor; 4] {
|
||||||
|
[
|
||||||
|
TargetConfigDescriptor {
|
||||||
|
external_key: "webhook",
|
||||||
|
subsystem_key: NOTIFY_WEBHOOK_SUB_SYS,
|
||||||
|
default_kvs: ¬ify::DEFAULT_NOTIFY_WEBHOOK_KVS,
|
||||||
|
valid_keys: NOTIFY_WEBHOOK_KEYS,
|
||||||
|
},
|
||||||
|
TargetConfigDescriptor {
|
||||||
|
external_key: "mqtt",
|
||||||
|
subsystem_key: NOTIFY_MQTT_SUB_SYS,
|
||||||
|
default_kvs: ¬ify::DEFAULT_NOTIFY_MQTT_KVS,
|
||||||
|
valid_keys: NOTIFY_MQTT_KEYS,
|
||||||
|
},
|
||||||
|
TargetConfigDescriptor {
|
||||||
|
external_key: "nats",
|
||||||
|
subsystem_key: NOTIFY_NATS_SUB_SYS,
|
||||||
|
default_kvs: ¬ify::DEFAULT_NOTIFY_NATS_KVS,
|
||||||
|
valid_keys: NOTIFY_NATS_KEYS,
|
||||||
|
},
|
||||||
|
TargetConfigDescriptor {
|
||||||
|
external_key: "pulsar",
|
||||||
|
subsystem_key: NOTIFY_PULSAR_SUB_SYS,
|
||||||
|
default_kvs: ¬ify::DEFAULT_NOTIFY_PULSAR_KVS,
|
||||||
|
valid_keys: NOTIFY_PULSAR_KEYS,
|
||||||
|
},
|
||||||
|
]
|
||||||
|
}
|
||||||
|
|
||||||
|
fn audit_target_descriptors() -> [TargetConfigDescriptor; 4] {
|
||||||
|
[
|
||||||
|
TargetConfigDescriptor {
|
||||||
|
external_key: "webhook",
|
||||||
|
subsystem_key: AUDIT_WEBHOOK_SUB_SYS,
|
||||||
|
default_kvs: &audit::DEFAULT_AUDIT_WEBHOOK_KVS,
|
||||||
|
valid_keys: AUDIT_WEBHOOK_KEYS,
|
||||||
|
},
|
||||||
|
TargetConfigDescriptor {
|
||||||
|
external_key: "mqtt",
|
||||||
|
subsystem_key: AUDIT_MQTT_SUB_SYS,
|
||||||
|
default_kvs: &audit::DEFAULT_AUDIT_MQTT_KVS,
|
||||||
|
valid_keys: AUDIT_MQTT_KEYS,
|
||||||
|
},
|
||||||
|
TargetConfigDescriptor {
|
||||||
|
external_key: "nats",
|
||||||
|
subsystem_key: AUDIT_NATS_SUB_SYS,
|
||||||
|
default_kvs: &audit::DEFAULT_AUDIT_NATS_KVS,
|
||||||
|
valid_keys: AUDIT_NATS_KEYS,
|
||||||
|
},
|
||||||
|
TargetConfigDescriptor {
|
||||||
|
external_key: "pulsar",
|
||||||
|
subsystem_key: AUDIT_PULSAR_SUB_SYS,
|
||||||
|
default_kvs: &audit::DEFAULT_AUDIT_PULSAR_KVS,
|
||||||
|
valid_keys: AUDIT_PULSAR_KEYS,
|
||||||
|
},
|
||||||
|
]
|
||||||
|
}
|
||||||
|
|
||||||
#[instrument(skip(api))]
|
#[instrument(skip(api))]
|
||||||
pub async fn read_config<S: StorageAPI>(api: Arc<S>, file: &str) -> Result<Vec<u8>> {
|
pub async fn read_config<S: StorageAPI>(api: Arc<S>, file: &str) -> Result<Vec<u8>> {
|
||||||
let (data, _obj) = read_config_with_metadata(api, file, &ObjectOptions::default()).await?;
|
let (data, _obj) = read_config_with_metadata(api, file, &ObjectOptions::default()).await?;
|
||||||
@@ -363,29 +435,31 @@ fn apply_external_notify_section(
|
|||||||
applied
|
applied
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn apply_external_target_descriptors(
|
||||||
|
cfg: &mut Config,
|
||||||
|
section_obj: &Map<String, Value>,
|
||||||
|
descriptors: &[TargetConfigDescriptor],
|
||||||
|
) -> bool {
|
||||||
|
let mut applied = false;
|
||||||
|
for descriptor in descriptors {
|
||||||
|
applied |= apply_external_notify_section(
|
||||||
|
cfg,
|
||||||
|
section_obj,
|
||||||
|
descriptor.external_key,
|
||||||
|
descriptor.subsystem_key,
|
||||||
|
descriptor.default_kvs,
|
||||||
|
descriptor.valid_keys,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
applied
|
||||||
|
}
|
||||||
|
|
||||||
fn apply_external_notify_map(cfg: &mut Config, root: &Map<String, Value>) -> bool {
|
fn apply_external_notify_map(cfg: &mut Config, root: &Map<String, Value>) -> bool {
|
||||||
let Some(Value::Object(notify_obj)) = root.get("notify") else {
|
let Some(Value::Object(notify_obj)) = root.get("notify") else {
|
||||||
return false;
|
return false;
|
||||||
};
|
};
|
||||||
|
|
||||||
let mut applied = false;
|
apply_external_target_descriptors(cfg, notify_obj, ¬ify_target_descriptors())
|
||||||
applied |= apply_external_notify_section(
|
|
||||||
cfg,
|
|
||||||
notify_obj,
|
|
||||||
"webhook",
|
|
||||||
NOTIFY_WEBHOOK_SUB_SYS,
|
|
||||||
¬ify::DEFAULT_NOTIFY_WEBHOOK_KVS,
|
|
||||||
NOTIFY_WEBHOOK_KEYS,
|
|
||||||
);
|
|
||||||
applied |= apply_external_notify_section(
|
|
||||||
cfg,
|
|
||||||
notify_obj,
|
|
||||||
"mqtt",
|
|
||||||
NOTIFY_MQTT_SUB_SYS,
|
|
||||||
¬ify::DEFAULT_NOTIFY_MQTT_KVS,
|
|
||||||
NOTIFY_MQTT_KEYS,
|
|
||||||
);
|
|
||||||
applied
|
|
||||||
}
|
}
|
||||||
|
|
||||||
fn apply_external_audit_map(cfg: &mut Config, root: &Map<String, Value>) -> bool {
|
fn apply_external_audit_map(cfg: &mut Config, root: &Map<String, Value>) -> bool {
|
||||||
@@ -394,24 +468,7 @@ fn apply_external_audit_map(cfg: &mut Config, root: &Map<String, Value>) -> bool
|
|||||||
return false;
|
return false;
|
||||||
};
|
};
|
||||||
|
|
||||||
let mut applied = false;
|
apply_external_target_descriptors(cfg, audit_obj, &audit_target_descriptors())
|
||||||
applied |= apply_external_notify_section(
|
|
||||||
cfg,
|
|
||||||
audit_obj,
|
|
||||||
"webhook",
|
|
||||||
AUDIT_WEBHOOK_SUB_SYS,
|
|
||||||
&audit::DEFAULT_AUDIT_WEBHOOK_KVS,
|
|
||||||
AUDIT_WEBHOOK_KEYS,
|
|
||||||
);
|
|
||||||
applied |= apply_external_notify_section(
|
|
||||||
cfg,
|
|
||||||
audit_obj,
|
|
||||||
"mqtt",
|
|
||||||
AUDIT_MQTT_SUB_SYS,
|
|
||||||
&audit::DEFAULT_AUDIT_MQTT_KVS,
|
|
||||||
AUDIT_MQTT_KEYS,
|
|
||||||
);
|
|
||||||
applied
|
|
||||||
}
|
}
|
||||||
|
|
||||||
fn apply_external_storage_class_map(cfg: &mut Config, root: &Map<String, Value>) -> bool {
|
fn apply_external_storage_class_map(cfg: &mut Config, root: &Map<String, Value>) -> bool {
|
||||||
@@ -605,7 +662,15 @@ fn build_semantic_oidc_object(cfg: &Config) -> Map<String, Value> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
fn is_notify_bool_key(key: &str) -> bool {
|
fn is_notify_bool_key(key: &str) -> bool {
|
||||||
key == ENABLE_KEY || key == rustfs_config::WEBHOOK_SKIP_TLS_VERIFY
|
matches!(
|
||||||
|
key,
|
||||||
|
ENABLE_KEY
|
||||||
|
| rustfs_config::WEBHOOK_SKIP_TLS_VERIFY
|
||||||
|
| rustfs_config::MQTT_TLS_TRUST_LEAF_AS_CA
|
||||||
|
| rustfs_config::NATS_TLS_REQUIRED
|
||||||
|
| rustfs_config::PULSAR_TLS_ALLOW_INSECURE
|
||||||
|
| rustfs_config::PULSAR_TLS_HOSTNAME_VERIFICATION
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
fn encode_notify_scalar_value(key: &str, value: &str) -> Value {
|
fn encode_notify_scalar_value(key: &str, value: &str) -> Value {
|
||||||
@@ -703,38 +768,43 @@ fn build_notify_subsystem_object(
|
|||||||
subsystem_obj
|
subsystem_obj
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn build_target_object(cfg: &Config, descriptors: &[TargetConfigDescriptor]) -> Map<String, Value> {
|
||||||
|
let mut target_obj = Map::new();
|
||||||
|
for descriptor in descriptors {
|
||||||
|
let subsystem_obj =
|
||||||
|
build_notify_subsystem_object(cfg, descriptor.subsystem_key, descriptor.default_kvs, descriptor.valid_keys);
|
||||||
|
if !subsystem_obj.is_empty() {
|
||||||
|
target_obj.insert(descriptor.external_key.to_string(), Value::Object(subsystem_obj));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
target_obj
|
||||||
|
}
|
||||||
|
|
||||||
fn build_notify_object(cfg: &Config) -> Map<String, Value> {
|
fn build_notify_object(cfg: &Config) -> Map<String, Value> {
|
||||||
let mut notify_obj = Map::new();
|
build_target_object(cfg, ¬ify_target_descriptors())
|
||||||
|
|
||||||
let webhook_obj =
|
|
||||||
build_notify_subsystem_object(cfg, NOTIFY_WEBHOOK_SUB_SYS, ¬ify::DEFAULT_NOTIFY_WEBHOOK_KVS, NOTIFY_WEBHOOK_KEYS);
|
|
||||||
if !webhook_obj.is_empty() {
|
|
||||||
notify_obj.insert("webhook".to_string(), Value::Object(webhook_obj));
|
|
||||||
}
|
|
||||||
|
|
||||||
let mqtt_obj = build_notify_subsystem_object(cfg, NOTIFY_MQTT_SUB_SYS, ¬ify::DEFAULT_NOTIFY_MQTT_KVS, NOTIFY_MQTT_KEYS);
|
|
||||||
if !mqtt_obj.is_empty() {
|
|
||||||
notify_obj.insert("mqtt".to_string(), Value::Object(mqtt_obj));
|
|
||||||
}
|
|
||||||
|
|
||||||
notify_obj
|
|
||||||
}
|
}
|
||||||
|
|
||||||
fn build_audit_object(cfg: &Config) -> Map<String, Value> {
|
fn build_audit_object(cfg: &Config) -> Map<String, Value> {
|
||||||
let mut audit_obj = Map::new();
|
build_target_object(cfg, &audit_target_descriptors())
|
||||||
|
}
|
||||||
|
|
||||||
let webhook_obj =
|
fn sync_rendered_target_object(
|
||||||
build_notify_subsystem_object(cfg, AUDIT_WEBHOOK_SUB_SYS, &audit::DEFAULT_AUDIT_WEBHOOK_KVS, AUDIT_WEBHOOK_KEYS);
|
target_obj: &mut Map<String, Value>,
|
||||||
if !webhook_obj.is_empty() {
|
rendered_target: &Map<String, Value>,
|
||||||
audit_obj.insert("webhook".to_string(), Value::Object(webhook_obj));
|
descriptors: &[TargetConfigDescriptor],
|
||||||
|
) {
|
||||||
|
for descriptor in descriptors {
|
||||||
|
match rendered_target.get(descriptor.external_key) {
|
||||||
|
Some(Value::Object(v)) => {
|
||||||
|
target_obj.insert(descriptor.external_key.to_string(), Value::Object(v.clone()));
|
||||||
|
target_obj.remove(descriptor.subsystem_key);
|
||||||
|
}
|
||||||
|
_ => {
|
||||||
|
target_obj.remove(descriptor.external_key);
|
||||||
|
target_obj.remove(descriptor.subsystem_key);
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
let mqtt_obj = build_notify_subsystem_object(cfg, AUDIT_MQTT_SUB_SYS, &audit::DEFAULT_AUDIT_MQTT_KVS, AUDIT_MQTT_KEYS);
|
|
||||||
if !mqtt_obj.is_empty() {
|
|
||||||
audit_obj.insert("mqtt".to_string(), Value::Object(mqtt_obj));
|
|
||||||
}
|
|
||||||
|
|
||||||
audit_obj
|
|
||||||
}
|
}
|
||||||
|
|
||||||
fn encode_server_config_blob(cfg: &Config, seed: Option<&[u8]>) -> Result<Vec<u8>> {
|
fn encode_server_config_blob(cfg: &Config, seed: Option<&[u8]>) -> Result<Vec<u8>> {
|
||||||
@@ -771,67 +841,31 @@ fn encode_server_config_blob(cfg: &Config, seed: Option<&[u8]>) -> Result<Vec<u8
|
|||||||
_ => Map::new(),
|
_ => Map::new(),
|
||||||
};
|
};
|
||||||
let rendered_notify = build_notify_object(cfg);
|
let rendered_notify = build_notify_object(cfg);
|
||||||
match rendered_notify.get("webhook") {
|
sync_rendered_target_object(&mut notify_obj, &rendered_notify, ¬ify_target_descriptors());
|
||||||
Some(Value::Object(v)) => {
|
|
||||||
notify_obj.insert("webhook".to_string(), Value::Object(v.clone()));
|
|
||||||
notify_obj.remove(NOTIFY_WEBHOOK_SUB_SYS);
|
|
||||||
}
|
|
||||||
_ => {
|
|
||||||
notify_obj.remove("webhook");
|
|
||||||
notify_obj.remove(NOTIFY_WEBHOOK_SUB_SYS);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
match rendered_notify.get("mqtt") {
|
|
||||||
Some(Value::Object(v)) => {
|
|
||||||
notify_obj.insert("mqtt".to_string(), Value::Object(v.clone()));
|
|
||||||
notify_obj.remove(NOTIFY_MQTT_SUB_SYS);
|
|
||||||
}
|
|
||||||
_ => {
|
|
||||||
notify_obj.remove("mqtt");
|
|
||||||
notify_obj.remove(NOTIFY_MQTT_SUB_SYS);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if notify_obj.is_empty() {
|
if notify_obj.is_empty() {
|
||||||
root.remove("notify");
|
root.remove("notify");
|
||||||
} else {
|
} else {
|
||||||
root.insert("notify".to_string(), Value::Object(notify_obj));
|
root.insert("notify".to_string(), Value::Object(notify_obj));
|
||||||
}
|
}
|
||||||
root.remove(NOTIFY_WEBHOOK_SUB_SYS);
|
for descriptor in notify_target_descriptors() {
|
||||||
root.remove(NOTIFY_MQTT_SUB_SYS);
|
root.remove(descriptor.subsystem_key);
|
||||||
|
}
|
||||||
|
|
||||||
let mut logger_obj = match root.remove("logger") {
|
let mut logger_obj = match root.remove("logger") {
|
||||||
Some(Value::Object(v)) => v,
|
Some(Value::Object(v)) => v,
|
||||||
_ => Map::new(),
|
_ => Map::new(),
|
||||||
};
|
};
|
||||||
let rendered_audit = build_audit_object(cfg);
|
let rendered_audit = build_audit_object(cfg);
|
||||||
match rendered_audit.get("webhook") {
|
sync_rendered_target_object(&mut logger_obj, &rendered_audit, &audit_target_descriptors());
|
||||||
Some(Value::Object(v)) => {
|
|
||||||
logger_obj.insert("webhook".to_string(), Value::Object(v.clone()));
|
|
||||||
logger_obj.remove(AUDIT_WEBHOOK_SUB_SYS);
|
|
||||||
}
|
|
||||||
_ => {
|
|
||||||
logger_obj.remove("webhook");
|
|
||||||
logger_obj.remove(AUDIT_WEBHOOK_SUB_SYS);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
match rendered_audit.get("mqtt") {
|
|
||||||
Some(Value::Object(v)) => {
|
|
||||||
logger_obj.insert("mqtt".to_string(), Value::Object(v.clone()));
|
|
||||||
logger_obj.remove(AUDIT_MQTT_SUB_SYS);
|
|
||||||
}
|
|
||||||
_ => {
|
|
||||||
logger_obj.remove("mqtt");
|
|
||||||
logger_obj.remove(AUDIT_MQTT_SUB_SYS);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if logger_obj.is_empty() {
|
if logger_obj.is_empty() {
|
||||||
root.remove("logger");
|
root.remove("logger");
|
||||||
} else {
|
} else {
|
||||||
root.insert("logger".to_string(), Value::Object(logger_obj));
|
root.insert("logger".to_string(), Value::Object(logger_obj));
|
||||||
}
|
}
|
||||||
root.remove("audit");
|
root.remove("audit");
|
||||||
root.remove(AUDIT_WEBHOOK_SUB_SYS);
|
for descriptor in audit_target_descriptors() {
|
||||||
root.remove(AUDIT_MQTT_SUB_SYS);
|
root.remove(descriptor.subsystem_key);
|
||||||
|
}
|
||||||
|
|
||||||
Ok(serde_json::to_vec(&Value::Object(root))?)
|
Ok(serde_json::to_vec(&Value::Object(root))?)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -25,8 +25,8 @@ use crate::store::ECStore;
|
|||||||
use com::{STORAGE_CLASS_SUB_SYS, lookup_configs, read_config_without_migrate};
|
use com::{STORAGE_CLASS_SUB_SYS, lookup_configs, read_config_without_migrate};
|
||||||
use rustfs_config::COMMENT_KEY;
|
use rustfs_config::COMMENT_KEY;
|
||||||
use rustfs_config::DEFAULT_DELIMITER;
|
use rustfs_config::DEFAULT_DELIMITER;
|
||||||
use rustfs_config::audit::{AUDIT_MQTT_SUB_SYS, AUDIT_WEBHOOK_SUB_SYS};
|
use rustfs_config::audit::{AUDIT_MQTT_SUB_SYS, AUDIT_NATS_SUB_SYS, AUDIT_PULSAR_SUB_SYS, AUDIT_WEBHOOK_SUB_SYS};
|
||||||
use rustfs_config::notify::{NOTIFY_MQTT_SUB_SYS, NOTIFY_WEBHOOK_SUB_SYS};
|
use rustfs_config::notify::{NOTIFY_MQTT_SUB_SYS, NOTIFY_NATS_SUB_SYS, NOTIFY_PULSAR_SUB_SYS, NOTIFY_WEBHOOK_SUB_SYS};
|
||||||
use rustfs_config::oidc::IDENTITY_OPENID_SUB_SYS;
|
use rustfs_config::oidc::IDENTITY_OPENID_SUB_SYS;
|
||||||
use serde::{Deserialize, Serialize};
|
use serde::{Deserialize, Serialize};
|
||||||
use std::collections::HashMap;
|
use std::collections::HashMap;
|
||||||
@@ -241,6 +241,10 @@ pub fn init() {
|
|||||||
kvs.insert(AUDIT_WEBHOOK_SUB_SYS.to_owned(), audit::DEFAULT_AUDIT_WEBHOOK_KVS.clone());
|
kvs.insert(AUDIT_WEBHOOK_SUB_SYS.to_owned(), audit::DEFAULT_AUDIT_WEBHOOK_KVS.clone());
|
||||||
kvs.insert(NOTIFY_MQTT_SUB_SYS.to_owned(), notify::DEFAULT_NOTIFY_MQTT_KVS.clone());
|
kvs.insert(NOTIFY_MQTT_SUB_SYS.to_owned(), notify::DEFAULT_NOTIFY_MQTT_KVS.clone());
|
||||||
kvs.insert(AUDIT_MQTT_SUB_SYS.to_owned(), audit::DEFAULT_AUDIT_MQTT_KVS.clone());
|
kvs.insert(AUDIT_MQTT_SUB_SYS.to_owned(), audit::DEFAULT_AUDIT_MQTT_KVS.clone());
|
||||||
|
kvs.insert(NOTIFY_NATS_SUB_SYS.to_owned(), notify::DEFAULT_NOTIFY_NATS_KVS.clone());
|
||||||
|
kvs.insert(AUDIT_NATS_SUB_SYS.to_owned(), audit::DEFAULT_AUDIT_NATS_KVS.clone());
|
||||||
|
kvs.insert(NOTIFY_PULSAR_SUB_SYS.to_owned(), notify::DEFAULT_NOTIFY_PULSAR_KVS.clone());
|
||||||
|
kvs.insert(AUDIT_PULSAR_SUB_SYS.to_owned(), audit::DEFAULT_AUDIT_PULSAR_KVS.clone());
|
||||||
kvs.insert(IDENTITY_OPENID_SUB_SYS.to_owned(), oidc::DEFAULT_IDENTITY_OPENID_KVS.clone());
|
kvs.insert(IDENTITY_OPENID_SUB_SYS.to_owned(), oidc::DEFAULT_IDENTITY_OPENID_KVS.clone());
|
||||||
|
|
||||||
// Register all default configurations
|
// Register all default configurations
|
||||||
|
|||||||
@@ -16,9 +16,12 @@ use crate::config::{KV, KVS};
|
|||||||
use rustfs_config::{
|
use rustfs_config::{
|
||||||
COMMENT_KEY, DEFAULT_LIMIT, ENABLE_KEY, EVENT_DEFAULT_DIR, EnableState, MQTT_BROKER, MQTT_KEEP_ALIVE_INTERVAL, MQTT_PASSWORD,
|
COMMENT_KEY, DEFAULT_LIMIT, ENABLE_KEY, EVENT_DEFAULT_DIR, EnableState, MQTT_BROKER, MQTT_KEEP_ALIVE_INTERVAL, MQTT_PASSWORD,
|
||||||
MQTT_QOS, MQTT_QUEUE_DIR, MQTT_QUEUE_LIMIT, MQTT_RECONNECT_INTERVAL, MQTT_TLS_CA, MQTT_TLS_CLIENT_CERT, MQTT_TLS_CLIENT_KEY,
|
MQTT_QOS, MQTT_QUEUE_DIR, MQTT_QUEUE_LIMIT, MQTT_RECONNECT_INTERVAL, MQTT_TLS_CA, MQTT_TLS_CLIENT_CERT, MQTT_TLS_CLIENT_KEY,
|
||||||
MQTT_TLS_POLICY, MQTT_TLS_TRUST_LEAF_AS_CA, MQTT_TOPIC, MQTT_USERNAME, MQTT_WS_PATH_ALLOWLIST, WEBHOOK_AUTH_TOKEN,
|
MQTT_TLS_POLICY, MQTT_TLS_TRUST_LEAF_AS_CA, MQTT_TOPIC, MQTT_USERNAME, MQTT_WS_PATH_ALLOWLIST, NATS_ADDRESS,
|
||||||
WEBHOOK_CLIENT_CA, WEBHOOK_CLIENT_CERT, WEBHOOK_CLIENT_KEY, WEBHOOK_ENDPOINT, WEBHOOK_QUEUE_DIR, WEBHOOK_QUEUE_LIMIT,
|
NATS_CREDENTIALS_FILE, NATS_PASSWORD, NATS_QUEUE_DIR, NATS_QUEUE_LIMIT, NATS_SUBJECT, NATS_TLS_CA, NATS_TLS_CLIENT_CERT,
|
||||||
WEBHOOK_SKIP_TLS_VERIFY,
|
NATS_TLS_CLIENT_KEY, NATS_TLS_REQUIRED, NATS_TOKEN, NATS_USERNAME, PULSAR_AUTH_TOKEN, PULSAR_BROKER, PULSAR_PASSWORD,
|
||||||
|
PULSAR_QUEUE_DIR, PULSAR_QUEUE_LIMIT, PULSAR_TLS_ALLOW_INSECURE, PULSAR_TLS_CA, PULSAR_TLS_HOSTNAME_VERIFICATION,
|
||||||
|
PULSAR_TOPIC, PULSAR_USERNAME, WEBHOOK_AUTH_TOKEN, WEBHOOK_CLIENT_CA, WEBHOOK_CLIENT_CERT, WEBHOOK_CLIENT_KEY,
|
||||||
|
WEBHOOK_ENDPOINT, WEBHOOK_QUEUE_DIR, WEBHOOK_QUEUE_LIMIT, WEBHOOK_SKIP_TLS_VERIFY,
|
||||||
};
|
};
|
||||||
use std::sync::LazyLock;
|
use std::sync::LazyLock;
|
||||||
|
|
||||||
@@ -171,3 +174,143 @@ pub static DEFAULT_NOTIFY_MQTT_KVS: LazyLock<KVS> = LazyLock::new(|| {
|
|||||||
},
|
},
|
||||||
])
|
])
|
||||||
});
|
});
|
||||||
|
|
||||||
|
pub static DEFAULT_NOTIFY_NATS_KVS: LazyLock<KVS> = LazyLock::new(|| {
|
||||||
|
KVS(vec![
|
||||||
|
KV {
|
||||||
|
key: ENABLE_KEY.to_owned(),
|
||||||
|
value: EnableState::Off.to_string(),
|
||||||
|
hidden_if_empty: false,
|
||||||
|
},
|
||||||
|
KV {
|
||||||
|
key: NATS_ADDRESS.to_owned(),
|
||||||
|
value: "".to_owned(),
|
||||||
|
hidden_if_empty: false,
|
||||||
|
},
|
||||||
|
KV {
|
||||||
|
key: NATS_SUBJECT.to_owned(),
|
||||||
|
value: "".to_owned(),
|
||||||
|
hidden_if_empty: false,
|
||||||
|
},
|
||||||
|
KV {
|
||||||
|
key: NATS_USERNAME.to_owned(),
|
||||||
|
value: "".to_owned(),
|
||||||
|
hidden_if_empty: false,
|
||||||
|
},
|
||||||
|
KV {
|
||||||
|
key: NATS_PASSWORD.to_owned(),
|
||||||
|
value: "".to_owned(),
|
||||||
|
hidden_if_empty: true,
|
||||||
|
},
|
||||||
|
KV {
|
||||||
|
key: NATS_TOKEN.to_owned(),
|
||||||
|
value: "".to_owned(),
|
||||||
|
hidden_if_empty: true,
|
||||||
|
},
|
||||||
|
KV {
|
||||||
|
key: NATS_CREDENTIALS_FILE.to_owned(),
|
||||||
|
value: "".to_owned(),
|
||||||
|
hidden_if_empty: true,
|
||||||
|
},
|
||||||
|
KV {
|
||||||
|
key: NATS_TLS_CA.to_owned(),
|
||||||
|
value: "".to_owned(),
|
||||||
|
hidden_if_empty: true,
|
||||||
|
},
|
||||||
|
KV {
|
||||||
|
key: NATS_TLS_CLIENT_CERT.to_owned(),
|
||||||
|
value: "".to_owned(),
|
||||||
|
hidden_if_empty: true,
|
||||||
|
},
|
||||||
|
KV {
|
||||||
|
key: NATS_TLS_CLIENT_KEY.to_owned(),
|
||||||
|
value: "".to_owned(),
|
||||||
|
hidden_if_empty: true,
|
||||||
|
},
|
||||||
|
KV {
|
||||||
|
key: NATS_TLS_REQUIRED.to_owned(),
|
||||||
|
value: EnableState::Off.to_string(),
|
||||||
|
hidden_if_empty: false,
|
||||||
|
},
|
||||||
|
KV {
|
||||||
|
key: NATS_QUEUE_DIR.to_owned(),
|
||||||
|
value: EVENT_DEFAULT_DIR.to_owned(),
|
||||||
|
hidden_if_empty: false,
|
||||||
|
},
|
||||||
|
KV {
|
||||||
|
key: NATS_QUEUE_LIMIT.to_owned(),
|
||||||
|
value: DEFAULT_LIMIT.to_string(),
|
||||||
|
hidden_if_empty: false,
|
||||||
|
},
|
||||||
|
KV {
|
||||||
|
key: COMMENT_KEY.to_owned(),
|
||||||
|
value: "".to_owned(),
|
||||||
|
hidden_if_empty: false,
|
||||||
|
},
|
||||||
|
])
|
||||||
|
});
|
||||||
|
|
||||||
|
pub static DEFAULT_NOTIFY_PULSAR_KVS: LazyLock<KVS> = LazyLock::new(|| {
|
||||||
|
KVS(vec![
|
||||||
|
KV {
|
||||||
|
key: ENABLE_KEY.to_owned(),
|
||||||
|
value: EnableState::Off.to_string(),
|
||||||
|
hidden_if_empty: false,
|
||||||
|
},
|
||||||
|
KV {
|
||||||
|
key: PULSAR_BROKER.to_owned(),
|
||||||
|
value: "".to_owned(),
|
||||||
|
hidden_if_empty: false,
|
||||||
|
},
|
||||||
|
KV {
|
||||||
|
key: PULSAR_TOPIC.to_owned(),
|
||||||
|
value: "".to_owned(),
|
||||||
|
hidden_if_empty: false,
|
||||||
|
},
|
||||||
|
KV {
|
||||||
|
key: PULSAR_AUTH_TOKEN.to_owned(),
|
||||||
|
value: "".to_owned(),
|
||||||
|
hidden_if_empty: true,
|
||||||
|
},
|
||||||
|
KV {
|
||||||
|
key: PULSAR_USERNAME.to_owned(),
|
||||||
|
value: "".to_owned(),
|
||||||
|
hidden_if_empty: false,
|
||||||
|
},
|
||||||
|
KV {
|
||||||
|
key: PULSAR_PASSWORD.to_owned(),
|
||||||
|
value: "".to_owned(),
|
||||||
|
hidden_if_empty: true,
|
||||||
|
},
|
||||||
|
KV {
|
||||||
|
key: PULSAR_TLS_CA.to_owned(),
|
||||||
|
value: "".to_owned(),
|
||||||
|
hidden_if_empty: true,
|
||||||
|
},
|
||||||
|
KV {
|
||||||
|
key: PULSAR_TLS_ALLOW_INSECURE.to_owned(),
|
||||||
|
value: EnableState::Off.to_string(),
|
||||||
|
hidden_if_empty: false,
|
||||||
|
},
|
||||||
|
KV {
|
||||||
|
key: PULSAR_TLS_HOSTNAME_VERIFICATION.to_owned(),
|
||||||
|
value: EnableState::On.to_string(),
|
||||||
|
hidden_if_empty: false,
|
||||||
|
},
|
||||||
|
KV {
|
||||||
|
key: PULSAR_QUEUE_DIR.to_owned(),
|
||||||
|
value: EVENT_DEFAULT_DIR.to_owned(),
|
||||||
|
hidden_if_empty: false,
|
||||||
|
},
|
||||||
|
KV {
|
||||||
|
key: PULSAR_QUEUE_LIMIT.to_owned(),
|
||||||
|
value: DEFAULT_LIMIT.to_string(),
|
||||||
|
hidden_if_empty: false,
|
||||||
|
},
|
||||||
|
KV {
|
||||||
|
key: COMMENT_KEY.to_owned(),
|
||||||
|
value: "".to_owned(),
|
||||||
|
hidden_if_empty: false,
|
||||||
|
},
|
||||||
|
])
|
||||||
|
});
|
||||||
|
|||||||
+48
-176
@@ -14,28 +14,19 @@
|
|||||||
|
|
||||||
use crate::Event;
|
use crate::Event;
|
||||||
use async_trait::async_trait;
|
use async_trait::async_trait;
|
||||||
use hashbrown::HashSet;
|
use rustfs_config::EVENT_DEFAULT_DIR;
|
||||||
use rumqttc::QoS;
|
use rustfs_config::notify::{NOTIFY_MQTT_KEYS, NOTIFY_NATS_KEYS, NOTIFY_PULSAR_KEYS, NOTIFY_WEBHOOK_KEYS};
|
||||||
use rustfs_config::notify::{ENV_NOTIFY_MQTT_KEYS, ENV_NOTIFY_WEBHOOK_KEYS, NOTIFY_MQTT_KEYS, NOTIFY_WEBHOOK_KEYS};
|
|
||||||
use rustfs_config::{
|
|
||||||
DEFAULT_LIMIT, EVENT_DEFAULT_DIR, MQTT_BROKER, MQTT_KEEP_ALIVE_INTERVAL, MQTT_PASSWORD, MQTT_QOS, MQTT_QUEUE_DIR,
|
|
||||||
MQTT_QUEUE_LIMIT, MQTT_RECONNECT_INTERVAL, MQTT_TLS_CA, MQTT_TLS_CLIENT_CERT, MQTT_TLS_CLIENT_KEY, MQTT_TLS_POLICY,
|
|
||||||
MQTT_TLS_TRUST_LEAF_AS_CA, MQTT_TOPIC, MQTT_USERNAME, MQTT_WS_PATH_ALLOWLIST, RUSTFS_WEBHOOK_SKIP_TLS_VERIFY_DEFAULT,
|
|
||||||
WEBHOOK_AUTH_TOKEN, WEBHOOK_CLIENT_CA, WEBHOOK_CLIENT_CERT, WEBHOOK_CLIENT_KEY, WEBHOOK_ENDPOINT, WEBHOOK_QUEUE_DIR,
|
|
||||||
WEBHOOK_QUEUE_LIMIT, WEBHOOK_SKIP_TLS_VERIFY,
|
|
||||||
};
|
|
||||||
use rustfs_ecstore::config::KVS;
|
use rustfs_ecstore::config::KVS;
|
||||||
use rustfs_targets::{
|
use rustfs_targets::{
|
||||||
Target,
|
Target,
|
||||||
error::TargetError,
|
config::{
|
||||||
target::{
|
build_mqtt_args, build_nats_args, build_pulsar_args, build_webhook_args, validate_mqtt_config, validate_nats_config,
|
||||||
mqtt::{MQTTArgs, MQTTTlsConfig, validate_mqtt_broker_url},
|
validate_pulsar_config, validate_webhook_config,
|
||||||
webhook::WebhookArgs,
|
|
||||||
},
|
},
|
||||||
|
error::TargetError,
|
||||||
|
target::TargetType,
|
||||||
};
|
};
|
||||||
use std::time::Duration;
|
use std::collections::HashSet;
|
||||||
use tracing::{debug, warn};
|
|
||||||
use url::Url;
|
|
||||||
|
|
||||||
/// Trait for creating targets from configuration
|
/// Trait for creating targets from configuration
|
||||||
#[async_trait]
|
#[async_trait]
|
||||||
@@ -49,10 +40,6 @@ pub trait TargetFactory: Send + Sync {
|
|||||||
/// Returns a set of valid configuration field names for this target type.
|
/// Returns a set of valid configuration field names for this target type.
|
||||||
/// This is used to filter environment variables.
|
/// This is used to filter environment variables.
|
||||||
fn get_valid_fields(&self) -> HashSet<String>;
|
fn get_valid_fields(&self) -> HashSet<String>;
|
||||||
|
|
||||||
/// Returns a set of valid configuration env field names for this target type.
|
|
||||||
/// This is used to filter environment variables.
|
|
||||||
fn get_valid_env_fields(&self) -> HashSet<String>;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Factory for creating Webhook targets
|
/// Factory for creating Webhook targets
|
||||||
@@ -61,75 +48,18 @@ pub struct WebhookTargetFactory;
|
|||||||
#[async_trait]
|
#[async_trait]
|
||||||
impl TargetFactory for WebhookTargetFactory {
|
impl TargetFactory for WebhookTargetFactory {
|
||||||
async fn create_target(&self, id: String, config: &KVS) -> Result<Box<dyn Target<Event> + Send + Sync>, TargetError> {
|
async fn create_target(&self, id: String, config: &KVS) -> Result<Box<dyn Target<Event> + Send + Sync>, TargetError> {
|
||||||
// All config values are now read directly from the merged `config` KVS.
|
let args = build_webhook_args(config, EVENT_DEFAULT_DIR, TargetType::NotifyEvent)?;
|
||||||
let endpoint = config
|
|
||||||
.lookup(WEBHOOK_ENDPOINT)
|
|
||||||
.ok_or_else(|| TargetError::Configuration("Missing webhook endpoint".to_string()))?;
|
|
||||||
let parsed_endpoint = endpoint.trim();
|
|
||||||
let endpoint_url = Url::parse(parsed_endpoint)
|
|
||||||
.map_err(|e| TargetError::Configuration(format!("Invalid endpoint URL: {e} (value: '{parsed_endpoint}')")))?;
|
|
||||||
|
|
||||||
let args = WebhookArgs {
|
|
||||||
enable: true, // If we are here, it's already enabled.
|
|
||||||
endpoint: endpoint_url,
|
|
||||||
auth_token: config.lookup(WEBHOOK_AUTH_TOKEN).unwrap_or_default(),
|
|
||||||
queue_dir: config
|
|
||||||
.lookup(WEBHOOK_QUEUE_DIR)
|
|
||||||
.unwrap_or_else(|| EVENT_DEFAULT_DIR.to_string()),
|
|
||||||
queue_limit: config
|
|
||||||
.lookup(WEBHOOK_QUEUE_LIMIT)
|
|
||||||
.and_then(|v| v.parse::<u64>().ok())
|
|
||||||
.unwrap_or(DEFAULT_LIMIT),
|
|
||||||
client_cert: config.lookup(WEBHOOK_CLIENT_CERT).unwrap_or_default(),
|
|
||||||
client_key: config.lookup(WEBHOOK_CLIENT_KEY).unwrap_or_default(),
|
|
||||||
client_ca: config.lookup(WEBHOOK_CLIENT_CA).unwrap_or_default(),
|
|
||||||
skip_tls_verify: config
|
|
||||||
.lookup(WEBHOOK_SKIP_TLS_VERIFY)
|
|
||||||
.and_then(|v| v.parse::<bool>().ok())
|
|
||||||
.unwrap_or(RUSTFS_WEBHOOK_SKIP_TLS_VERIFY_DEFAULT),
|
|
||||||
target_type: rustfs_targets::target::TargetType::NotifyEvent,
|
|
||||||
};
|
|
||||||
|
|
||||||
let target = rustfs_targets::target::webhook::WebhookTarget::new(id, args)?;
|
let target = rustfs_targets::target::webhook::WebhookTarget::new(id, args)?;
|
||||||
Ok(Box::new(target))
|
Ok(Box::new(target))
|
||||||
}
|
}
|
||||||
|
|
||||||
fn validate_config(&self, _id: &str, config: &KVS) -> Result<(), TargetError> {
|
fn validate_config(&self, _id: &str, config: &KVS) -> Result<(), TargetError> {
|
||||||
// Validation also uses the merged `config` KVS directly.
|
validate_webhook_config(config, EVENT_DEFAULT_DIR)
|
||||||
let endpoint = config
|
|
||||||
.lookup(WEBHOOK_ENDPOINT)
|
|
||||||
.ok_or_else(|| TargetError::Configuration("Missing webhook endpoint".to_string()))?;
|
|
||||||
debug!("endpoint: {}", endpoint);
|
|
||||||
let parsed_endpoint = endpoint.trim();
|
|
||||||
Url::parse(parsed_endpoint)
|
|
||||||
.map_err(|e| TargetError::Configuration(format!("Invalid endpoint URL: {e} (value: '{parsed_endpoint}')")))?;
|
|
||||||
|
|
||||||
let client_cert = config.lookup(WEBHOOK_CLIENT_CERT).unwrap_or_default();
|
|
||||||
let client_key = config.lookup(WEBHOOK_CLIENT_KEY).unwrap_or_default();
|
|
||||||
|
|
||||||
if client_cert.is_empty() != client_key.is_empty() {
|
|
||||||
return Err(TargetError::Configuration(
|
|
||||||
"Both client_cert and client_key must be specified together".to_string(),
|
|
||||||
));
|
|
||||||
}
|
|
||||||
|
|
||||||
let queue_dir = config
|
|
||||||
.lookup(WEBHOOK_QUEUE_DIR)
|
|
||||||
.unwrap_or_else(|| EVENT_DEFAULT_DIR.to_string());
|
|
||||||
if !queue_dir.is_empty() && !std::path::Path::new(&queue_dir).is_absolute() {
|
|
||||||
return Err(TargetError::Configuration("Webhook queue directory must be an absolute path".to_string()));
|
|
||||||
}
|
|
||||||
|
|
||||||
Ok(())
|
|
||||||
}
|
}
|
||||||
|
|
||||||
fn get_valid_fields(&self) -> HashSet<String> {
|
fn get_valid_fields(&self) -> HashSet<String> {
|
||||||
NOTIFY_WEBHOOK_KEYS.iter().map(|s| s.to_string()).collect()
|
NOTIFY_WEBHOOK_KEYS.iter().map(|s| s.to_string()).collect()
|
||||||
}
|
}
|
||||||
|
|
||||||
fn get_valid_env_fields(&self) -> HashSet<String> {
|
|
||||||
ENV_NOTIFY_WEBHOOK_KEYS.iter().map(|s| s.to_string()).collect()
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Factory for creating MQTT targets
|
/// Factory for creating MQTT targets
|
||||||
@@ -138,112 +68,54 @@ pub struct MQTTTargetFactory;
|
|||||||
#[async_trait]
|
#[async_trait]
|
||||||
impl TargetFactory for MQTTTargetFactory {
|
impl TargetFactory for MQTTTargetFactory {
|
||||||
async fn create_target(&self, id: String, config: &KVS) -> Result<Box<dyn Target<Event> + Send + Sync>, TargetError> {
|
async fn create_target(&self, id: String, config: &KVS) -> Result<Box<dyn Target<Event> + Send + Sync>, TargetError> {
|
||||||
let broker = config
|
let args = build_mqtt_args(config, EVENT_DEFAULT_DIR, TargetType::NotifyEvent)?;
|
||||||
.lookup(MQTT_BROKER)
|
|
||||||
.ok_or_else(|| TargetError::Configuration("Missing MQTT broker".to_string()))?;
|
|
||||||
let broker_url = Url::parse(&broker)
|
|
||||||
.map_err(|e| TargetError::Configuration(format!("Invalid broker URL: {e} (value: '{broker}')")))?;
|
|
||||||
|
|
||||||
let topic = config
|
|
||||||
.lookup(MQTT_TOPIC)
|
|
||||||
.ok_or_else(|| TargetError::Configuration("Missing MQTT topic".to_string()))?;
|
|
||||||
|
|
||||||
let args = MQTTArgs {
|
|
||||||
enable: true, // Assumed enabled.
|
|
||||||
broker: broker_url,
|
|
||||||
topic,
|
|
||||||
qos: config
|
|
||||||
.lookup(MQTT_QOS)
|
|
||||||
.and_then(|v| v.parse::<u8>().ok())
|
|
||||||
.map(|q| match q {
|
|
||||||
0 => QoS::AtMostOnce,
|
|
||||||
1 => QoS::AtLeastOnce,
|
|
||||||
2 => QoS::ExactlyOnce,
|
|
||||||
_ => QoS::AtLeastOnce,
|
|
||||||
})
|
|
||||||
.unwrap_or(QoS::AtLeastOnce),
|
|
||||||
username: config.lookup(MQTT_USERNAME).unwrap_or_default(),
|
|
||||||
password: config.lookup(MQTT_PASSWORD).unwrap_or_default(),
|
|
||||||
max_reconnect_interval: config
|
|
||||||
.lookup(MQTT_RECONNECT_INTERVAL)
|
|
||||||
.and_then(|v| v.parse::<u64>().ok())
|
|
||||||
.map(Duration::from_secs)
|
|
||||||
.unwrap_or_else(|| Duration::from_secs(5)),
|
|
||||||
keep_alive: config
|
|
||||||
.lookup(MQTT_KEEP_ALIVE_INTERVAL)
|
|
||||||
.and_then(|v| v.parse::<u64>().ok())
|
|
||||||
.map(Duration::from_secs)
|
|
||||||
.unwrap_or_else(|| Duration::from_secs(30)),
|
|
||||||
tls: MQTTTlsConfig::from_values(
|
|
||||||
config.lookup(MQTT_TLS_POLICY).as_deref(),
|
|
||||||
config.lookup(MQTT_TLS_CA).as_deref(),
|
|
||||||
config.lookup(MQTT_TLS_CLIENT_CERT).as_deref(),
|
|
||||||
config.lookup(MQTT_TLS_CLIENT_KEY).as_deref(),
|
|
||||||
config.lookup(MQTT_TLS_TRUST_LEAF_AS_CA).as_deref(),
|
|
||||||
config.lookup(MQTT_WS_PATH_ALLOWLIST).as_deref(),
|
|
||||||
)?,
|
|
||||||
queue_dir: config.lookup(MQTT_QUEUE_DIR).unwrap_or_else(|| EVENT_DEFAULT_DIR.to_string()),
|
|
||||||
queue_limit: config
|
|
||||||
.lookup(MQTT_QUEUE_LIMIT)
|
|
||||||
.and_then(|v| v.parse::<u64>().ok())
|
|
||||||
.unwrap_or(DEFAULT_LIMIT),
|
|
||||||
target_type: rustfs_targets::target::TargetType::NotifyEvent,
|
|
||||||
};
|
|
||||||
|
|
||||||
let target = rustfs_targets::target::mqtt::MQTTTarget::new(id, args)?;
|
let target = rustfs_targets::target::mqtt::MQTTTarget::new(id, args)?;
|
||||||
Ok(Box::new(target))
|
Ok(Box::new(target))
|
||||||
}
|
}
|
||||||
|
|
||||||
fn validate_config(&self, _id: &str, config: &KVS) -> Result<(), TargetError> {
|
fn validate_config(&self, _id: &str, config: &KVS) -> Result<(), TargetError> {
|
||||||
let broker = config
|
validate_mqtt_config(config)
|
||||||
.lookup(MQTT_BROKER)
|
|
||||||
.ok_or_else(|| TargetError::Configuration("Missing MQTT broker".to_string()))?;
|
|
||||||
let url = Url::parse(&broker)
|
|
||||||
.map_err(|e| TargetError::Configuration(format!("Invalid broker URL: {e} (value: '{broker}')")))?;
|
|
||||||
|
|
||||||
let tls = MQTTTlsConfig::from_values(
|
|
||||||
config.lookup(MQTT_TLS_POLICY).as_deref(),
|
|
||||||
config.lookup(MQTT_TLS_CA).as_deref(),
|
|
||||||
config.lookup(MQTT_TLS_CLIENT_CERT).as_deref(),
|
|
||||||
config.lookup(MQTT_TLS_CLIENT_KEY).as_deref(),
|
|
||||||
config.lookup(MQTT_TLS_TRUST_LEAF_AS_CA).as_deref(),
|
|
||||||
config.lookup(MQTT_WS_PATH_ALLOWLIST).as_deref(),
|
|
||||||
)?;
|
|
||||||
validate_mqtt_broker_url(&url, &tls)?;
|
|
||||||
|
|
||||||
if config.lookup(MQTT_TOPIC).is_none() {
|
|
||||||
return Err(TargetError::Configuration("Missing MQTT topic".to_string()));
|
|
||||||
}
|
|
||||||
|
|
||||||
if let Some(qos_str) = config.lookup(MQTT_QOS) {
|
|
||||||
let qos = qos_str
|
|
||||||
.parse::<u8>()
|
|
||||||
.map_err(|_| TargetError::Configuration("Invalid QoS value".to_string()))?;
|
|
||||||
if qos > 2 {
|
|
||||||
return Err(TargetError::Configuration("QoS must be 0, 1, or 2".to_string()));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
let queue_dir = config.lookup(MQTT_QUEUE_DIR).unwrap_or_default();
|
|
||||||
if !queue_dir.is_empty() {
|
|
||||||
if !std::path::Path::new(&queue_dir).is_absolute() {
|
|
||||||
return Err(TargetError::Configuration("MQTT queue directory must be an absolute path".to_string()));
|
|
||||||
}
|
|
||||||
if let Some(qos_str) = config.lookup(MQTT_QOS)
|
|
||||||
&& qos_str == "0"
|
|
||||||
{
|
|
||||||
warn!("Using queue_dir with QoS 0 may result in event loss");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Ok(())
|
|
||||||
}
|
}
|
||||||
|
|
||||||
fn get_valid_fields(&self) -> HashSet<String> {
|
fn get_valid_fields(&self) -> HashSet<String> {
|
||||||
NOTIFY_MQTT_KEYS.iter().map(|s| s.to_string()).collect()
|
NOTIFY_MQTT_KEYS.iter().map(|s| s.to_string()).collect()
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
fn get_valid_env_fields(&self) -> HashSet<String> {
|
pub struct NATSTargetFactory;
|
||||||
ENV_NOTIFY_MQTT_KEYS.iter().map(|s| s.to_string()).collect()
|
|
||||||
|
#[async_trait]
|
||||||
|
impl TargetFactory for NATSTargetFactory {
|
||||||
|
async fn create_target(&self, id: String, config: &KVS) -> Result<Box<dyn Target<Event> + Send + Sync>, TargetError> {
|
||||||
|
let args = build_nats_args(config, EVENT_DEFAULT_DIR, TargetType::NotifyEvent)?;
|
||||||
|
let target = rustfs_targets::target::nats::NATSTarget::new(id, args)?;
|
||||||
|
Ok(Box::new(target))
|
||||||
|
}
|
||||||
|
|
||||||
|
fn validate_config(&self, _id: &str, config: &KVS) -> Result<(), TargetError> {
|
||||||
|
validate_nats_config(config, EVENT_DEFAULT_DIR)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn get_valid_fields(&self) -> HashSet<String> {
|
||||||
|
NOTIFY_NATS_KEYS.iter().map(|s| s.to_string()).collect()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub struct PulsarTargetFactory;
|
||||||
|
|
||||||
|
#[async_trait]
|
||||||
|
impl TargetFactory for PulsarTargetFactory {
|
||||||
|
async fn create_target(&self, id: String, config: &KVS) -> Result<Box<dyn Target<Event> + Send + Sync>, TargetError> {
|
||||||
|
let args = build_pulsar_args(config, EVENT_DEFAULT_DIR, TargetType::NotifyEvent)?;
|
||||||
|
let target = rustfs_targets::target::pulsar::PulsarTarget::new(id, args)?;
|
||||||
|
Ok(Box::new(target))
|
||||||
|
}
|
||||||
|
|
||||||
|
fn validate_config(&self, _id: &str, config: &KVS) -> Result<(), TargetError> {
|
||||||
|
validate_pulsar_config(config, EVENT_DEFAULT_DIR)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn get_valid_fields(&self) -> HashSet<String> {
|
||||||
|
NOTIFY_PULSAR_KEYS.iter().map(|s| s.to_string()).collect()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -19,7 +19,8 @@ use crate::{
|
|||||||
};
|
};
|
||||||
use hashbrown::HashMap;
|
use hashbrown::HashMap;
|
||||||
use rustfs_config::notify::{
|
use rustfs_config::notify::{
|
||||||
DEFAULT_NOTIFY_TARGET_STREAM_CONCURRENCY, ENV_NOTIFY_TARGET_STREAM_CONCURRENCY, NOTIFY_MQTT_SUB_SYS, NOTIFY_WEBHOOK_SUB_SYS,
|
DEFAULT_NOTIFY_TARGET_STREAM_CONCURRENCY, ENV_NOTIFY_TARGET_STREAM_CONCURRENCY, NOTIFY_MQTT_SUB_SYS, NOTIFY_NATS_SUB_SYS,
|
||||||
|
NOTIFY_PULSAR_SUB_SYS, NOTIFY_WEBHOOK_SUB_SYS,
|
||||||
};
|
};
|
||||||
use rustfs_ecstore::config::{Config, KVS};
|
use rustfs_ecstore::config::{Config, KVS};
|
||||||
use rustfs_s3_common::EventName;
|
use rustfs_s3_common::EventName;
|
||||||
@@ -40,6 +41,8 @@ fn subsystem_target_type(target_type: &str) -> &str {
|
|||||||
match target_type {
|
match target_type {
|
||||||
NOTIFY_WEBHOOK_SUB_SYS => "webhook",
|
NOTIFY_WEBHOOK_SUB_SYS => "webhook",
|
||||||
NOTIFY_MQTT_SUB_SYS => "mqtt",
|
NOTIFY_MQTT_SUB_SYS => "mqtt",
|
||||||
|
NOTIFY_NATS_SUB_SYS => "nats",
|
||||||
|
NOTIFY_PULSAR_SUB_SYS => "pulsar",
|
||||||
_ => target_type,
|
_ => target_type,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -746,4 +749,18 @@ mod tests {
|
|||||||
assert_eq!(target_id.id, "analytics");
|
assert_eq!(target_id.id, "analytics");
|
||||||
assert_eq!(target_id.name, "mqtt");
|
assert_eq!(target_id.name, "mqtt");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn runtime_target_id_for_subsystem_maps_notify_nats_to_runtime_type() {
|
||||||
|
let target_id = runtime_target_id_for_subsystem(NOTIFY_NATS_SUB_SYS, "Bus");
|
||||||
|
assert_eq!(target_id.id, "bus");
|
||||||
|
assert_eq!(target_id.name, "nats");
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn runtime_target_id_for_subsystem_maps_notify_pulsar_to_runtime_type() {
|
||||||
|
let target_id = runtime_target_id_for_subsystem(NOTIFY_PULSAR_SUB_SYS, "Ledger");
|
||||||
|
assert_eq!(target_id.id, "ledger");
|
||||||
|
assert_eq!(target_id.name, "pulsar");
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
+15
-135
@@ -13,15 +13,14 @@
|
|||||||
// limitations under the License.
|
// limitations under the License.
|
||||||
|
|
||||||
use crate::Event;
|
use crate::Event;
|
||||||
use crate::factory::{MQTTTargetFactory, TargetFactory, WebhookTargetFactory};
|
use crate::factory::{MQTTTargetFactory, NATSTargetFactory, PulsarTargetFactory, TargetFactory, WebhookTargetFactory};
|
||||||
use futures::stream::{FuturesUnordered, StreamExt};
|
use futures::stream::{FuturesUnordered, StreamExt};
|
||||||
use hashbrown::{HashMap, HashSet};
|
use hashbrown::HashMap;
|
||||||
use rustfs_config::{DEFAULT_DELIMITER, ENABLE_KEY, ENV_PREFIX, EnableState, notify::NOTIFY_ROUTE_PREFIX};
|
use rustfs_config::notify::NOTIFY_ROUTE_PREFIX;
|
||||||
use rustfs_ecstore::config::{Config, KVS};
|
use rustfs_ecstore::config::{Config, KVS};
|
||||||
use rustfs_targets::{Target, TargetError, target::ChannelTargetType};
|
use rustfs_targets::{Target, TargetError, config::collect_target_configs, target::ChannelTargetType};
|
||||||
use std::str::FromStr;
|
|
||||||
use std::sync::Arc;
|
use std::sync::Arc;
|
||||||
use tracing::{debug, error, info, warn};
|
use tracing::{error, info};
|
||||||
|
|
||||||
/// Registry for managing target factories
|
/// Registry for managing target factories
|
||||||
pub struct TargetRegistry {
|
pub struct TargetRegistry {
|
||||||
@@ -44,6 +43,8 @@ impl TargetRegistry {
|
|||||||
// Register built-in factories
|
// Register built-in factories
|
||||||
registry.register(ChannelTargetType::Webhook.as_str(), Box::new(WebhookTargetFactory));
|
registry.register(ChannelTargetType::Webhook.as_str(), Box::new(WebhookTargetFactory));
|
||||||
registry.register(ChannelTargetType::Mqtt.as_str(), Box::new(MQTTTargetFactory));
|
registry.register(ChannelTargetType::Mqtt.as_str(), Box::new(MQTTTargetFactory));
|
||||||
|
registry.register(ChannelTargetType::Nats.as_str(), Box::new(NATSTargetFactory));
|
||||||
|
registry.register(ChannelTargetType::Pulsar.as_str(), Box::new(PulsarTargetFactory));
|
||||||
|
|
||||||
registry
|
registry
|
||||||
}
|
}
|
||||||
@@ -85,143 +86,22 @@ impl TargetRegistry {
|
|||||||
&self,
|
&self,
|
||||||
config: &Config,
|
config: &Config,
|
||||||
) -> Result<Vec<Box<dyn Target<Event> + Send + Sync>>, TargetError> {
|
) -> Result<Vec<Box<dyn Target<Event> + Send + Sync>>, TargetError> {
|
||||||
// Collect only environment variables with the relevant prefix to reduce memory usage
|
|
||||||
let all_env: Vec<(String, String)> = std::env::vars().filter(|(key, _)| key.starts_with(ENV_PREFIX)).collect();
|
|
||||||
// A collection of asynchronous tasks for concurrently executing target creation
|
|
||||||
let mut tasks = FuturesUnordered::new();
|
let mut tasks = FuturesUnordered::new();
|
||||||
// 1. Traverse all registered plants and process them by target type
|
|
||||||
for (target_type, factory) in &self.factories {
|
for (target_type, factory) in &self.factories {
|
||||||
tracing::Span::current().record("target_type", target_type.as_str());
|
tracing::Span::current().record("target_type", target_type.as_str());
|
||||||
info!("Start working on target types...");
|
info!("Start working on target types...");
|
||||||
|
|
||||||
// 2. Prepare the configuration source
|
|
||||||
// 2.1. Get the configuration segment in the file, e.g. 'notify_webhook'
|
|
||||||
let section_name = format!("{NOTIFY_ROUTE_PREFIX}{target_type}").to_lowercase();
|
|
||||||
let file_configs = config.0.get(§ion_name).cloned().unwrap_or_default();
|
|
||||||
// 2.2. Get the default configuration for that type
|
|
||||||
let default_cfg = file_configs.get(DEFAULT_DELIMITER).cloned().unwrap_or_default();
|
|
||||||
debug!(?default_cfg, "Get the default configuration");
|
|
||||||
|
|
||||||
// *** Optimization point 1: Get all legitimate fields of the current target type ***
|
|
||||||
let valid_fields = factory.get_valid_fields();
|
let valid_fields = factory.get_valid_fields();
|
||||||
debug!(?valid_fields, "Get the legitimate configuration fields");
|
for (id, merged_config) in collect_target_configs(config, NOTIFY_ROUTE_PREFIX, target_type, &valid_fields) {
|
||||||
|
info!(instance_id = %id, "Target is enabled, ready to create a task");
|
||||||
// 3. Resolve instance IDs and configuration overrides from environment variables
|
let tid = id.clone();
|
||||||
let mut instance_ids_from_env = HashSet::new();
|
let merged_config_arc = Arc::new(merged_config);
|
||||||
// 3.1. Instance discovery: Based on the '..._ENABLE_INSTANCEID' format
|
tasks.push(async move {
|
||||||
let enable_prefix =
|
let result = factory.create_target(tid.clone(), &merged_config_arc).await;
|
||||||
format!("{ENV_PREFIX}{NOTIFY_ROUTE_PREFIX}{target_type}{DEFAULT_DELIMITER}{ENABLE_KEY}{DEFAULT_DELIMITER}")
|
(tid, result)
|
||||||
.to_uppercase();
|
});
|
||||||
for (key, value) in &all_env {
|
|
||||||
if EnableState::from_str(value).ok().map(|s| s.is_enabled()).unwrap_or(false)
|
|
||||||
&& let Some(id) = key.strip_prefix(&enable_prefix)
|
|
||||||
&& !id.is_empty()
|
|
||||||
{
|
|
||||||
instance_ids_from_env.insert(id.to_lowercase());
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// 3.2. Parse all relevant environment variable configurations
|
|
||||||
// 3.2.1. Build environment variable prefixes such as 'RUSTFS_NOTIFY_WEBHOOK_'
|
|
||||||
let env_prefix = format!("{ENV_PREFIX}{NOTIFY_ROUTE_PREFIX}{target_type}{DEFAULT_DELIMITER}").to_uppercase();
|
|
||||||
// 3.2.2. 'env_overrides' is used to store configurations parsed from environment variables in the format: {instance id -> {field -> value}}
|
|
||||||
let mut env_overrides: HashMap<String, HashMap<String, String>> = HashMap::new();
|
|
||||||
for (key, value) in &all_env {
|
|
||||||
if let Some(rest) = key.strip_prefix(&env_prefix) {
|
|
||||||
// Use rsplitn to split from the right side to properly extract the INSTANCE_ID at the end
|
|
||||||
// Format: <FIELD_NAME>_<INSTANCE_ID> or <FIELD_NAME>
|
|
||||||
let mut parts = rest.rsplitn(2, DEFAULT_DELIMITER);
|
|
||||||
|
|
||||||
// The first part from the right is INSTANCE_ID
|
|
||||||
let instance_id_part = parts.next().unwrap_or(DEFAULT_DELIMITER);
|
|
||||||
// The remaining part is FIELD_NAME
|
|
||||||
let field_name_part = parts.next();
|
|
||||||
|
|
||||||
let (field_name, instance_id) = match field_name_part {
|
|
||||||
// Case 1: The format is <FIELD_NAME>_<INSTANCE_ID>
|
|
||||||
// e.g., rest = "ENDPOINT_PRIMARY" -> field_name="ENDPOINT", instance_id="PRIMARY"
|
|
||||||
Some(field) => (field.to_lowercase(), instance_id_part.to_lowercase()),
|
|
||||||
// Case 2: The format is <FIELD_NAME> (without INSTANCE_ID)
|
|
||||||
// e.g., rest = "ENABLE" -> field_name="ENABLE", instance_id="" (Universal configuration `_ DEFAULT_DELIMITER`)
|
|
||||||
None => (instance_id_part.to_lowercase(), DEFAULT_DELIMITER.to_string()),
|
|
||||||
};
|
|
||||||
|
|
||||||
// *** Optimization point 2: Verify whether the parsed field_name is legal ***
|
|
||||||
if !field_name.is_empty() && valid_fields.contains(&field_name) {
|
|
||||||
debug!(
|
|
||||||
instance_id = %if instance_id.is_empty() { DEFAULT_DELIMITER } else { &instance_id },
|
|
||||||
%field_name,
|
|
||||||
%value,
|
|
||||||
"Parsing to environment variables"
|
|
||||||
);
|
|
||||||
env_overrides
|
|
||||||
.entry(instance_id)
|
|
||||||
.or_default()
|
|
||||||
.insert(field_name, value.clone());
|
|
||||||
} else {
|
|
||||||
// Ignore illegal field names
|
|
||||||
warn!(
|
|
||||||
field_name = %field_name,
|
|
||||||
"Ignore environment variable fields, not found in the list of valid fields for target type {}",
|
|
||||||
target_type
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
debug!(?env_overrides, "Complete the environment variable analysis");
|
|
||||||
|
|
||||||
// 4. Determine all instance IDs that need to be processed
|
|
||||||
let mut all_instance_ids: HashSet<String> =
|
|
||||||
file_configs.keys().filter(|k| *k != DEFAULT_DELIMITER).cloned().collect();
|
|
||||||
all_instance_ids.extend(instance_ids_from_env);
|
|
||||||
debug!(?all_instance_ids, "Determine all instance IDs");
|
|
||||||
|
|
||||||
// 5. Merge configurations and create tasks for each instance
|
|
||||||
for id in all_instance_ids {
|
|
||||||
// 5.1. Merge configuration, priority: Environment variables > File instance configuration > File default configuration
|
|
||||||
let mut merged_config = default_cfg.clone();
|
|
||||||
// Instance-specific configuration in application files
|
|
||||||
if let Some(file_instance_cfg) = file_configs.get(&id) {
|
|
||||||
merged_config.extend(file_instance_cfg.clone());
|
|
||||||
}
|
|
||||||
// Application instance-specific environment variable configuration
|
|
||||||
if let Some(env_instance_cfg) = env_overrides.get(&id) {
|
|
||||||
// Convert HashMap<String, String> to KVS
|
|
||||||
let mut kvs_from_env = KVS::new();
|
|
||||||
for (k, v) in env_instance_cfg {
|
|
||||||
kvs_from_env.insert(k.clone(), v.clone());
|
|
||||||
}
|
|
||||||
merged_config.extend(kvs_from_env);
|
|
||||||
}
|
|
||||||
debug!(instance_id = %id, ?merged_config, "Complete configuration merge");
|
|
||||||
|
|
||||||
// 5.2. Check if the instance is enabled
|
|
||||||
let enabled = merged_config
|
|
||||||
.lookup(ENABLE_KEY)
|
|
||||||
.map(|v| {
|
|
||||||
EnableState::from_str(v.as_str())
|
|
||||||
.ok()
|
|
||||||
.map(|s| s.is_enabled())
|
|
||||||
.unwrap_or(false)
|
|
||||||
})
|
|
||||||
.unwrap_or(false);
|
|
||||||
|
|
||||||
if enabled {
|
|
||||||
info!(instance_id = %id, "Target is enabled, ready to create a task");
|
|
||||||
// 5.3. Create asynchronous tasks for enabled instances
|
|
||||||
let tid = id.clone();
|
|
||||||
let merged_config_arc = Arc::new(merged_config);
|
|
||||||
tasks.push(async move {
|
|
||||||
let result = factory.create_target(tid.clone(), &merged_config_arc).await;
|
|
||||||
(tid, result)
|
|
||||||
});
|
|
||||||
} else {
|
|
||||||
info!(instance_id = %id, "Skip disabled target");
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// 6. Concurrently execute all creation tasks and collect results
|
|
||||||
let mut successful_targets = Vec::new();
|
let mut successful_targets = Vec::new();
|
||||||
while let Some((id, result)) = tasks.next().await {
|
while let Some((id, result)) = tasks.next().await {
|
||||||
match result {
|
match result {
|
||||||
|
|||||||
@@ -13,10 +13,13 @@ documentation = "https://docs.rs/rustfs-target/latest/rustfs_target/"
|
|||||||
|
|
||||||
[dependencies]
|
[dependencies]
|
||||||
rustfs-config = { workspace = true, features = ["notify", "constants", "audit"] }
|
rustfs-config = { workspace = true, features = ["notify", "constants", "audit"] }
|
||||||
|
rustfs-ecstore = { workspace = true }
|
||||||
rustfs-utils = { workspace = true, features = ["notify", "tls"] }
|
rustfs-utils = { workspace = true, features = ["notify", "tls"] }
|
||||||
rustfs-s3-common = { workspace = true }
|
rustfs-s3-common = { workspace = true }
|
||||||
async-trait = { workspace = true }
|
async-trait = { workspace = true }
|
||||||
|
async-nats = { workspace = true }
|
||||||
hyper-rustls = { workspace = true }
|
hyper-rustls = { workspace = true }
|
||||||
|
pulsar = { workspace = true }
|
||||||
reqwest = { workspace = true }
|
reqwest = { workspace = true }
|
||||||
rumqttc = { workspace = true }
|
rumqttc = { workspace = true }
|
||||||
rustls = { workspace = true }
|
rustls = { workspace = true }
|
||||||
|
|||||||
@@ -92,3 +92,39 @@ pub async fn check_mqtt_broker_available_with_tls(
|
|||||||
Err(_) => Err(crate::TargetError::Timeout("MQTT connection timed out".to_string())),
|
Err(_) => Err(crate::TargetError::Timeout("MQTT connection timed out".to_string())),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub async fn check_nats_server_available(args: &crate::target::nats::NATSArgs) -> Result<(), crate::TargetError> {
|
||||||
|
match tokio::time::timeout(std::time::Duration::from_secs(5), async {
|
||||||
|
let client = crate::target::nats::connect_nats(args).await?;
|
||||||
|
client
|
||||||
|
.flush()
|
||||||
|
.await
|
||||||
|
.map_err(|e| crate::TargetError::Network(format!("NATS connection check failed: {e}")))?;
|
||||||
|
client
|
||||||
|
.drain()
|
||||||
|
.await
|
||||||
|
.map_err(|e| crate::TargetError::Network(format!("Failed to close NATS check connection: {e}")))?;
|
||||||
|
Ok(())
|
||||||
|
})
|
||||||
|
.await
|
||||||
|
{
|
||||||
|
Ok(result) => result,
|
||||||
|
Err(_) => Err(crate::TargetError::Timeout("NATS connection timed out".to_string())),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub async fn check_pulsar_broker_available(args: &crate::target::pulsar::PulsarArgs) -> Result<(), crate::TargetError> {
|
||||||
|
match tokio::time::timeout(std::time::Duration::from_secs(5), async {
|
||||||
|
let client = crate::target::pulsar::connect_pulsar(args).await?;
|
||||||
|
client
|
||||||
|
.lookup_partitioned_topic(args.topic.clone())
|
||||||
|
.await
|
||||||
|
.map_err(|e| crate::TargetError::Network(format!("Pulsar topic lookup failed: {e}")))?;
|
||||||
|
Ok(())
|
||||||
|
})
|
||||||
|
.await
|
||||||
|
{
|
||||||
|
Ok(result) => result,
|
||||||
|
Err(_) => Err(crate::TargetError::Timeout("Pulsar connection timed out".to_string())),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -0,0 +1,182 @@
|
|||||||
|
// 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.
|
||||||
|
|
||||||
|
use crate::TargetError;
|
||||||
|
use crate::target::pulsar::validate_pulsar_broker;
|
||||||
|
use async_nats::ServerAddr;
|
||||||
|
use rustfs_config::{
|
||||||
|
DEFAULT_DELIMITER, ENABLE_KEY, EnableState, NATS_CREDENTIALS_FILE, NATS_PASSWORD, NATS_QUEUE_DIR, NATS_SUBJECT, NATS_TLS_CA,
|
||||||
|
NATS_TLS_CLIENT_CERT, NATS_TLS_CLIENT_KEY, NATS_TOKEN, NATS_USERNAME, PULSAR_AUTH_TOKEN, PULSAR_PASSWORD, PULSAR_QUEUE_DIR,
|
||||||
|
PULSAR_TLS_ALLOW_INSECURE, PULSAR_TLS_CA, PULSAR_TLS_HOSTNAME_VERIFICATION, PULSAR_TOPIC, PULSAR_USERNAME,
|
||||||
|
};
|
||||||
|
use rustfs_ecstore::config::KVS;
|
||||||
|
use std::collections::HashSet;
|
||||||
|
use std::path::Path;
|
||||||
|
use std::str::FromStr;
|
||||||
|
use url::Url;
|
||||||
|
|
||||||
|
pub(super) fn split_env_field_and_instance(rest: &str, valid_fields: &HashSet<String>) -> Option<(String, String)> {
|
||||||
|
let normalized = rest.to_lowercase();
|
||||||
|
if valid_fields.contains(&normalized) {
|
||||||
|
return Some((normalized, DEFAULT_DELIMITER.to_string()));
|
||||||
|
}
|
||||||
|
|
||||||
|
valid_fields
|
||||||
|
.iter()
|
||||||
|
.filter_map(|field| {
|
||||||
|
normalized
|
||||||
|
.strip_prefix(field)
|
||||||
|
.and_then(|suffix| suffix.strip_prefix(DEFAULT_DELIMITER))
|
||||||
|
.filter(|instance_id| !instance_id.is_empty())
|
||||||
|
.map(|instance_id| (field.clone(), instance_id.to_string()))
|
||||||
|
})
|
||||||
|
.max_by_key(|(field, _)| field.len())
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(super) fn is_target_enabled(config: &KVS) -> bool {
|
||||||
|
config
|
||||||
|
.lookup(ENABLE_KEY)
|
||||||
|
.map(|v| {
|
||||||
|
EnableState::from_str(v.as_str())
|
||||||
|
.ok()
|
||||||
|
.map(|s| s.is_enabled())
|
||||||
|
.unwrap_or(false)
|
||||||
|
})
|
||||||
|
.unwrap_or(false)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(super) fn parse_target_bool(value: Option<&str>) -> Option<bool> {
|
||||||
|
let value = value?.trim();
|
||||||
|
if value.is_empty() {
|
||||||
|
return None;
|
||||||
|
}
|
||||||
|
value
|
||||||
|
.parse::<EnableState>()
|
||||||
|
.map(EnableState::is_enabled)
|
||||||
|
.or_else(|_| value.parse::<bool>())
|
||||||
|
.ok()
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(super) fn validate_nats_server_config(server: &ServerAddr, config: &KVS, default_queue_dir: &str) -> Result<(), TargetError> {
|
||||||
|
if config.lookup(NATS_SUBJECT).unwrap_or_default().trim().is_empty() {
|
||||||
|
return Err(TargetError::Configuration("Missing NATS subject".to_string()));
|
||||||
|
}
|
||||||
|
|
||||||
|
if server.has_user_pass() {
|
||||||
|
return Err(TargetError::Configuration("NATS address must not embed username or password".to_string()));
|
||||||
|
}
|
||||||
|
|
||||||
|
let username = config.lookup(NATS_USERNAME).unwrap_or_default();
|
||||||
|
let password = config.lookup(NATS_PASSWORD).unwrap_or_default();
|
||||||
|
let token = config.lookup(NATS_TOKEN).unwrap_or_default();
|
||||||
|
let credentials_file = config.lookup(NATS_CREDENTIALS_FILE).unwrap_or_default();
|
||||||
|
|
||||||
|
let mut auth_methods = 0usize;
|
||||||
|
if !token.is_empty() {
|
||||||
|
auth_methods += 1;
|
||||||
|
}
|
||||||
|
if !credentials_file.is_empty() {
|
||||||
|
auth_methods += 1;
|
||||||
|
if !Path::new(&credentials_file).is_absolute() {
|
||||||
|
return Err(TargetError::Configuration(format!("{NATS_CREDENTIALS_FILE} must be an absolute path")));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if !username.is_empty() || !password.is_empty() {
|
||||||
|
if username.is_empty() != password.is_empty() {
|
||||||
|
return Err(TargetError::Configuration(
|
||||||
|
"NATS username and password must be specified together".to_string(),
|
||||||
|
));
|
||||||
|
}
|
||||||
|
auth_methods += 1;
|
||||||
|
}
|
||||||
|
if auth_methods > 1 {
|
||||||
|
return Err(TargetError::Configuration(
|
||||||
|
"NATS supports only one auth method at a time: token, username/password, or credentials_file".to_string(),
|
||||||
|
));
|
||||||
|
}
|
||||||
|
|
||||||
|
let tls_ca = config.lookup(NATS_TLS_CA).unwrap_or_default();
|
||||||
|
let tls_client_cert = config.lookup(NATS_TLS_CLIENT_CERT).unwrap_or_default();
|
||||||
|
let tls_client_key = config.lookup(NATS_TLS_CLIENT_KEY).unwrap_or_default();
|
||||||
|
if !tls_ca.is_empty() && !Path::new(&tls_ca).is_absolute() {
|
||||||
|
return Err(TargetError::Configuration(format!("{NATS_TLS_CA} must be an absolute path")));
|
||||||
|
}
|
||||||
|
if !tls_client_cert.is_empty() && !Path::new(&tls_client_cert).is_absolute() {
|
||||||
|
return Err(TargetError::Configuration(format!("{NATS_TLS_CLIENT_CERT} must be an absolute path")));
|
||||||
|
}
|
||||||
|
if !tls_client_key.is_empty() && !Path::new(&tls_client_key).is_absolute() {
|
||||||
|
return Err(TargetError::Configuration(format!("{NATS_TLS_CLIENT_KEY} must be an absolute path")));
|
||||||
|
}
|
||||||
|
if tls_client_cert.is_empty() != tls_client_key.is_empty() {
|
||||||
|
return Err(TargetError::Configuration(
|
||||||
|
"NATS tls_client_cert and tls_client_key must be specified together".to_string(),
|
||||||
|
));
|
||||||
|
}
|
||||||
|
|
||||||
|
let queue_dir = config.lookup(NATS_QUEUE_DIR).unwrap_or_else(|| default_queue_dir.to_string());
|
||||||
|
if !queue_dir.is_empty() && !Path::new(&queue_dir).is_absolute() {
|
||||||
|
return Err(TargetError::Configuration("NATS queue directory must be an absolute path".to_string()));
|
||||||
|
}
|
||||||
|
|
||||||
|
let _ = server;
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(super) fn validate_pulsar_broker_config(broker: &str, config: &KVS, default_queue_dir: &str) -> Result<(), TargetError> {
|
||||||
|
let url = validate_pulsar_broker(broker)?;
|
||||||
|
|
||||||
|
if config.lookup(PULSAR_TOPIC).unwrap_or_default().trim().is_empty() {
|
||||||
|
return Err(TargetError::Configuration("Missing Pulsar topic".to_string()));
|
||||||
|
}
|
||||||
|
|
||||||
|
let auth_token = config.lookup(PULSAR_AUTH_TOKEN).unwrap_or_default();
|
||||||
|
let username = config.lookup(PULSAR_USERNAME).unwrap_or_default();
|
||||||
|
let password = config.lookup(PULSAR_PASSWORD).unwrap_or_default();
|
||||||
|
if !auth_token.is_empty() && (!username.is_empty() || !password.is_empty()) {
|
||||||
|
return Err(TargetError::Configuration(
|
||||||
|
"Pulsar supports either auth_token or username/password auth, not both".to_string(),
|
||||||
|
));
|
||||||
|
}
|
||||||
|
if username.is_empty() != password.is_empty() {
|
||||||
|
return Err(TargetError::Configuration(
|
||||||
|
"Pulsar username and password must be specified together".to_string(),
|
||||||
|
));
|
||||||
|
}
|
||||||
|
|
||||||
|
let tls_ca = config.lookup(PULSAR_TLS_CA).unwrap_or_default();
|
||||||
|
let tls_allow_insecure = parse_target_bool(config.lookup(PULSAR_TLS_ALLOW_INSECURE).as_deref()).unwrap_or(false);
|
||||||
|
let tls_hostname_verification = parse_target_bool(config.lookup(PULSAR_TLS_HOSTNAME_VERIFICATION).as_deref()).unwrap_or(true);
|
||||||
|
|
||||||
|
if !tls_ca.is_empty() && !Path::new(&tls_ca).is_absolute() {
|
||||||
|
return Err(TargetError::Configuration("Pulsar tls_ca must be an absolute path".to_string()));
|
||||||
|
}
|
||||||
|
if url.scheme() != "pulsar+ssl" && (!tls_ca.is_empty() || tls_allow_insecure || !tls_hostname_verification) {
|
||||||
|
return Err(TargetError::Configuration(
|
||||||
|
"Pulsar TLS settings are only allowed with pulsar+ssl brokers".to_string(),
|
||||||
|
));
|
||||||
|
}
|
||||||
|
|
||||||
|
let queue_dir = config
|
||||||
|
.lookup(PULSAR_QUEUE_DIR)
|
||||||
|
.unwrap_or_else(|| default_queue_dir.to_string());
|
||||||
|
if !queue_dir.is_empty() && !Path::new(&queue_dir).is_absolute() {
|
||||||
|
return Err(TargetError::Configuration("Pulsar queue directory must be an absolute path".to_string()));
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(super) fn parse_url(value: &str, field_label: &str) -> Result<Url, TargetError> {
|
||||||
|
Url::parse(value).map_err(|e| TargetError::Configuration(format!("Invalid {field_label}: {e} (value: '{value}')")))
|
||||||
|
}
|
||||||
@@ -0,0 +1,239 @@
|
|||||||
|
// 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.
|
||||||
|
|
||||||
|
use super::common::{is_target_enabled, split_env_field_and_instance};
|
||||||
|
use rustfs_config::{DEFAULT_DELIMITER, ENABLE_KEY, ENV_PREFIX, EnableState};
|
||||||
|
use rustfs_ecstore::config::{Config, KVS};
|
||||||
|
use std::collections::{HashMap, HashSet};
|
||||||
|
use std::str::FromStr;
|
||||||
|
use tracing::{debug, warn};
|
||||||
|
|
||||||
|
pub fn collect_target_configs(
|
||||||
|
config: &Config,
|
||||||
|
route_prefix: &str,
|
||||||
|
target_type: &str,
|
||||||
|
valid_fields: &HashSet<String>,
|
||||||
|
) -> Vec<(String, KVS)> {
|
||||||
|
collect_target_configs_from_env(config, route_prefix, target_type, valid_fields, std::env::vars())
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn collect_env_target_instance_ids(route_prefix: &str, target_type: &str, valid_fields: &HashSet<String>) -> HashSet<String> {
|
||||||
|
collect_env_target_instance_ids_from_env(route_prefix, target_type, valid_fields, std::env::vars())
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn collect_env_target_instance_ids_from_env<I>(
|
||||||
|
route_prefix: &str,
|
||||||
|
target_type: &str,
|
||||||
|
valid_fields: &HashSet<String>,
|
||||||
|
env_vars: I,
|
||||||
|
) -> HashSet<String>
|
||||||
|
where
|
||||||
|
I: IntoIterator<Item = (String, String)>,
|
||||||
|
{
|
||||||
|
let env_prefix = format!("{ENV_PREFIX}{route_prefix}{target_type}{DEFAULT_DELIMITER}").to_uppercase();
|
||||||
|
let mut instance_ids = HashSet::new();
|
||||||
|
|
||||||
|
for (key, _value) in env_vars.into_iter().filter(|(key, _)| key.starts_with(ENV_PREFIX)) {
|
||||||
|
let Some(rest) = key.strip_prefix(&env_prefix) else {
|
||||||
|
continue;
|
||||||
|
};
|
||||||
|
let Some((_field_name, instance_id)) = split_env_field_and_instance(rest, valid_fields) else {
|
||||||
|
continue;
|
||||||
|
};
|
||||||
|
if instance_id != DEFAULT_DELIMITER && !instance_id.is_empty() {
|
||||||
|
instance_ids.insert(instance_id);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
instance_ids
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn collect_target_configs_from_env<I>(
|
||||||
|
config: &Config,
|
||||||
|
route_prefix: &str,
|
||||||
|
target_type: &str,
|
||||||
|
valid_fields: &HashSet<String>,
|
||||||
|
env_vars: I,
|
||||||
|
) -> Vec<(String, KVS)>
|
||||||
|
where
|
||||||
|
I: IntoIterator<Item = (String, String)>,
|
||||||
|
{
|
||||||
|
let all_env: Vec<(String, String)> = env_vars.into_iter().filter(|(key, _)| key.starts_with(ENV_PREFIX)).collect();
|
||||||
|
let section_name = format!("{route_prefix}{target_type}").to_lowercase();
|
||||||
|
let file_configs = config.0.get(§ion_name).cloned().unwrap_or_default();
|
||||||
|
let default_cfg = file_configs.get(DEFAULT_DELIMITER).cloned().unwrap_or_default();
|
||||||
|
|
||||||
|
let enable_prefix =
|
||||||
|
format!("{ENV_PREFIX}{route_prefix}{target_type}{DEFAULT_DELIMITER}{ENABLE_KEY}{DEFAULT_DELIMITER}").to_uppercase();
|
||||||
|
let env_prefix = format!("{ENV_PREFIX}{route_prefix}{target_type}{DEFAULT_DELIMITER}").to_uppercase();
|
||||||
|
|
||||||
|
let mut instance_ids_from_env = HashSet::new();
|
||||||
|
let mut env_overrides: HashMap<String, KVS> = HashMap::new();
|
||||||
|
for (key, value) in &all_env {
|
||||||
|
if EnableState::from_str(value).ok().map(|s| s.is_enabled()).unwrap_or(false)
|
||||||
|
&& let Some(id) = key.strip_prefix(&enable_prefix)
|
||||||
|
&& !id.is_empty()
|
||||||
|
{
|
||||||
|
instance_ids_from_env.insert(id.to_lowercase());
|
||||||
|
}
|
||||||
|
|
||||||
|
let Some(rest) = key.strip_prefix(&env_prefix) else {
|
||||||
|
continue;
|
||||||
|
};
|
||||||
|
|
||||||
|
let Some((field_name, instance_id)) = split_env_field_and_instance(rest, valid_fields) else {
|
||||||
|
warn!(
|
||||||
|
field_name = %rest.to_lowercase(),
|
||||||
|
"Ignore environment variable field not found in the valid field list for target type {}",
|
||||||
|
target_type
|
||||||
|
);
|
||||||
|
continue;
|
||||||
|
};
|
||||||
|
|
||||||
|
debug!(
|
||||||
|
instance_id = %if instance_id == DEFAULT_DELIMITER { DEFAULT_DELIMITER } else { &instance_id },
|
||||||
|
%field_name,
|
||||||
|
%value,
|
||||||
|
"Parsed target environment override"
|
||||||
|
);
|
||||||
|
env_overrides
|
||||||
|
.entry(instance_id)
|
||||||
|
.or_default()
|
||||||
|
.insert(field_name, value.clone());
|
||||||
|
}
|
||||||
|
|
||||||
|
let mut effective_default = default_cfg;
|
||||||
|
if let Some(default_env_cfg) = env_overrides.remove(DEFAULT_DELIMITER) {
|
||||||
|
effective_default.extend(default_env_cfg);
|
||||||
|
}
|
||||||
|
|
||||||
|
let mut all_instance_ids: Vec<String> = file_configs
|
||||||
|
.keys()
|
||||||
|
.filter(|key| key.as_str() != DEFAULT_DELIMITER)
|
||||||
|
.cloned()
|
||||||
|
.collect();
|
||||||
|
all_instance_ids.extend(instance_ids_from_env);
|
||||||
|
all_instance_ids.sort();
|
||||||
|
all_instance_ids.dedup();
|
||||||
|
|
||||||
|
let mut merged_configs = Vec::new();
|
||||||
|
for id in all_instance_ids {
|
||||||
|
let mut merged_config = effective_default.clone();
|
||||||
|
if let Some(file_instance_cfg) = file_configs.get(&id) {
|
||||||
|
merged_config.extend(file_instance_cfg.clone());
|
||||||
|
}
|
||||||
|
if let Some(env_instance_cfg) = env_overrides.get(&id) {
|
||||||
|
merged_config.extend(env_instance_cfg.clone());
|
||||||
|
}
|
||||||
|
|
||||||
|
debug!(instance_id = %id, ?merged_config, "Merged target configuration");
|
||||||
|
if is_target_enabled(&merged_config) {
|
||||||
|
merged_configs.push((id, merged_config));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
merged_configs
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod tests {
|
||||||
|
use super::{collect_env_target_instance_ids_from_env, collect_target_configs_from_env};
|
||||||
|
use rustfs_config::notify::NOTIFY_ROUTE_PREFIX;
|
||||||
|
use rustfs_config::{ENABLE_KEY, WEBHOOK_ENDPOINT, WEBHOOK_QUEUE_LIMIT};
|
||||||
|
use rustfs_ecstore::config::{Config, KVS};
|
||||||
|
use std::collections::{HashMap, HashSet};
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn collect_target_configs_applies_default_env_overrides_to_file_targets() {
|
||||||
|
let mut cfg = Config(HashMap::new());
|
||||||
|
let mut subsystem = HashMap::new();
|
||||||
|
|
||||||
|
let mut default_kvs = KVS::new();
|
||||||
|
default_kvs.insert(ENABLE_KEY.to_string(), "off".to_string());
|
||||||
|
subsystem.insert("_".to_string(), default_kvs);
|
||||||
|
|
||||||
|
let mut primary = KVS::new();
|
||||||
|
primary.insert(WEBHOOK_ENDPOINT.to_string(), "https://example.com/primary".to_string());
|
||||||
|
subsystem.insert("primary".to_string(), primary);
|
||||||
|
|
||||||
|
let mut secondary = KVS::new();
|
||||||
|
secondary.insert(WEBHOOK_ENDPOINT.to_string(), "https://example.com/secondary".to_string());
|
||||||
|
subsystem.insert("secondary".to_string(), secondary);
|
||||||
|
|
||||||
|
cfg.0.insert("notify_webhook".to_string(), subsystem);
|
||||||
|
|
||||||
|
let configs = collect_target_configs_from_env(
|
||||||
|
&cfg,
|
||||||
|
NOTIFY_ROUTE_PREFIX,
|
||||||
|
"webhook",
|
||||||
|
&HashSet::from([
|
||||||
|
ENABLE_KEY.to_string(),
|
||||||
|
WEBHOOK_ENDPOINT.to_string(),
|
||||||
|
WEBHOOK_QUEUE_LIMIT.to_string(),
|
||||||
|
]),
|
||||||
|
vec![
|
||||||
|
("RUSTFS_NOTIFY_WEBHOOK_ENABLE".to_string(), "on".to_string()),
|
||||||
|
("RUSTFS_NOTIFY_WEBHOOK_QUEUE_LIMIT".to_string(), "42".to_string()),
|
||||||
|
],
|
||||||
|
);
|
||||||
|
|
||||||
|
let configs: HashMap<String, KVS> = configs.into_iter().collect();
|
||||||
|
assert_eq!(configs.len(), 2);
|
||||||
|
assert_eq!(configs["primary"].lookup(ENABLE_KEY).as_deref(), Some("on"));
|
||||||
|
assert_eq!(configs["secondary"].lookup(ENABLE_KEY).as_deref(), Some("on"));
|
||||||
|
assert_eq!(configs["primary"].lookup(WEBHOOK_QUEUE_LIMIT).as_deref(), Some("42"));
|
||||||
|
assert_eq!(configs["secondary"].lookup(WEBHOOK_QUEUE_LIMIT).as_deref(), Some("42"));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn collect_target_configs_discovers_enabled_instance_from_env() {
|
||||||
|
let cfg = Config(HashMap::new());
|
||||||
|
let configs = collect_target_configs_from_env(
|
||||||
|
&cfg,
|
||||||
|
NOTIFY_ROUTE_PREFIX,
|
||||||
|
"webhook",
|
||||||
|
&HashSet::from([ENABLE_KEY.to_string(), WEBHOOK_ENDPOINT.to_string()]),
|
||||||
|
vec![
|
||||||
|
("RUSTFS_NOTIFY_WEBHOOK_ENABLE_PRIMARY".to_string(), "on".to_string()),
|
||||||
|
(
|
||||||
|
"RUSTFS_NOTIFY_WEBHOOK_ENDPOINT_PRIMARY".to_string(),
|
||||||
|
"https://example.com/from-env".to_string(),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
);
|
||||||
|
|
||||||
|
assert_eq!(configs.len(), 1);
|
||||||
|
assert_eq!(configs[0].0, "primary");
|
||||||
|
assert_eq!(configs[0].1.lookup(WEBHOOK_ENDPOINT).as_deref(), Some("https://example.com/from-env"));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn collect_env_target_instance_ids_handles_keys_with_internal_underscores() {
|
||||||
|
let ids = collect_env_target_instance_ids_from_env(
|
||||||
|
NOTIFY_ROUTE_PREFIX,
|
||||||
|
"webhook",
|
||||||
|
&HashSet::from([
|
||||||
|
ENABLE_KEY.to_string(),
|
||||||
|
WEBHOOK_ENDPOINT.to_string(),
|
||||||
|
WEBHOOK_QUEUE_LIMIT.to_string(),
|
||||||
|
]),
|
||||||
|
vec![
|
||||||
|
("RUSTFS_NOTIFY_WEBHOOK_ENABLE_PRIMARY".to_string(), "on".to_string()),
|
||||||
|
("RUSTFS_NOTIFY_WEBHOOK_QUEUE_LIMIT_PRIMARY".to_string(), "42".to_string()),
|
||||||
|
],
|
||||||
|
);
|
||||||
|
|
||||||
|
assert_eq!(ids, HashSet::from(["primary".to_string()]));
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,26 @@
|
|||||||
|
// 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.
|
||||||
|
|
||||||
|
mod common;
|
||||||
|
mod loader;
|
||||||
|
mod target_args;
|
||||||
|
|
||||||
|
pub use loader::{
|
||||||
|
collect_env_target_instance_ids, collect_env_target_instance_ids_from_env, collect_target_configs,
|
||||||
|
collect_target_configs_from_env,
|
||||||
|
};
|
||||||
|
pub use target_args::{
|
||||||
|
build_mqtt_args, build_nats_args, build_pulsar_args, build_webhook_args, validate_mqtt_config, validate_nats_config,
|
||||||
|
validate_pulsar_config, validate_webhook_config,
|
||||||
|
};
|
||||||
@@ -0,0 +1,267 @@
|
|||||||
|
// 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.
|
||||||
|
|
||||||
|
use super::common::{parse_target_bool, parse_url, validate_nats_server_config, validate_pulsar_broker_config};
|
||||||
|
use crate::error::TargetError;
|
||||||
|
use crate::target::{
|
||||||
|
TargetType,
|
||||||
|
mqtt::{MQTTArgs, MQTTTlsConfig, validate_mqtt_broker_url},
|
||||||
|
nats::{NATSArgs, validate_nats_address},
|
||||||
|
pulsar::{PulsarArgs, validate_pulsar_broker},
|
||||||
|
webhook::WebhookArgs,
|
||||||
|
};
|
||||||
|
use rumqttc::QoS;
|
||||||
|
use rustfs_config::{
|
||||||
|
DEFAULT_LIMIT, MQTT_BROKER, MQTT_KEEP_ALIVE_INTERVAL, MQTT_PASSWORD, MQTT_QOS, MQTT_QUEUE_DIR, MQTT_QUEUE_LIMIT,
|
||||||
|
MQTT_RECONNECT_INTERVAL, MQTT_TLS_CA, MQTT_TLS_CLIENT_CERT, MQTT_TLS_CLIENT_KEY, MQTT_TLS_POLICY, MQTT_TLS_TRUST_LEAF_AS_CA,
|
||||||
|
MQTT_TOPIC, MQTT_USERNAME, MQTT_WS_PATH_ALLOWLIST, NATS_ADDRESS, NATS_CREDENTIALS_FILE, NATS_PASSWORD, NATS_QUEUE_DIR,
|
||||||
|
NATS_QUEUE_LIMIT, NATS_SUBJECT, NATS_TLS_CA, NATS_TLS_CLIENT_CERT, NATS_TLS_CLIENT_KEY, NATS_TLS_REQUIRED, NATS_TOKEN,
|
||||||
|
NATS_USERNAME, PULSAR_AUTH_TOKEN, PULSAR_BROKER, PULSAR_PASSWORD, PULSAR_QUEUE_DIR, PULSAR_QUEUE_LIMIT,
|
||||||
|
PULSAR_TLS_ALLOW_INSECURE, PULSAR_TLS_CA, PULSAR_TLS_HOSTNAME_VERIFICATION, PULSAR_TOPIC, PULSAR_USERNAME,
|
||||||
|
RUSTFS_WEBHOOK_SKIP_TLS_VERIFY_DEFAULT, WEBHOOK_AUTH_TOKEN, WEBHOOK_CLIENT_CA, WEBHOOK_CLIENT_CERT, WEBHOOK_CLIENT_KEY,
|
||||||
|
WEBHOOK_ENDPOINT, WEBHOOK_QUEUE_DIR, WEBHOOK_QUEUE_LIMIT, WEBHOOK_SKIP_TLS_VERIFY,
|
||||||
|
};
|
||||||
|
use rustfs_ecstore::config::KVS;
|
||||||
|
use std::path::Path;
|
||||||
|
use std::time::Duration;
|
||||||
|
|
||||||
|
pub fn build_webhook_args(config: &KVS, default_queue_dir: &str, target_type: TargetType) -> Result<WebhookArgs, TargetError> {
|
||||||
|
let endpoint = config
|
||||||
|
.lookup(WEBHOOK_ENDPOINT)
|
||||||
|
.ok_or_else(|| TargetError::Configuration("Missing webhook endpoint".to_string()))?;
|
||||||
|
let parsed_endpoint = endpoint.trim();
|
||||||
|
let endpoint_url = parse_url(parsed_endpoint, "endpoint URL")?;
|
||||||
|
|
||||||
|
Ok(WebhookArgs {
|
||||||
|
enable: true,
|
||||||
|
endpoint: endpoint_url,
|
||||||
|
auth_token: config.lookup(WEBHOOK_AUTH_TOKEN).unwrap_or_default(),
|
||||||
|
queue_dir: config
|
||||||
|
.lookup(WEBHOOK_QUEUE_DIR)
|
||||||
|
.unwrap_or_else(|| default_queue_dir.to_string()),
|
||||||
|
queue_limit: config
|
||||||
|
.lookup(WEBHOOK_QUEUE_LIMIT)
|
||||||
|
.and_then(|v| v.parse::<u64>().ok())
|
||||||
|
.unwrap_or(DEFAULT_LIMIT),
|
||||||
|
client_cert: config.lookup(WEBHOOK_CLIENT_CERT).unwrap_or_default(),
|
||||||
|
client_key: config.lookup(WEBHOOK_CLIENT_KEY).unwrap_or_default(),
|
||||||
|
client_ca: config.lookup(WEBHOOK_CLIENT_CA).unwrap_or_default(),
|
||||||
|
skip_tls_verify: config
|
||||||
|
.lookup(WEBHOOK_SKIP_TLS_VERIFY)
|
||||||
|
.and_then(|v| v.parse::<bool>().ok())
|
||||||
|
.unwrap_or(RUSTFS_WEBHOOK_SKIP_TLS_VERIFY_DEFAULT),
|
||||||
|
target_type,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn validate_webhook_config(config: &KVS, default_queue_dir: &str) -> Result<(), TargetError> {
|
||||||
|
let endpoint = config
|
||||||
|
.lookup(WEBHOOK_ENDPOINT)
|
||||||
|
.ok_or_else(|| TargetError::Configuration("Missing webhook endpoint".to_string()))?;
|
||||||
|
let parsed_endpoint = endpoint.trim();
|
||||||
|
let _ = parse_url(parsed_endpoint, "endpoint URL")?;
|
||||||
|
|
||||||
|
let client_cert = config.lookup(WEBHOOK_CLIENT_CERT).unwrap_or_default();
|
||||||
|
let client_key = config.lookup(WEBHOOK_CLIENT_KEY).unwrap_or_default();
|
||||||
|
if client_cert.is_empty() != client_key.is_empty() {
|
||||||
|
return Err(TargetError::Configuration(
|
||||||
|
"Both client_cert and client_key must be specified together".to_string(),
|
||||||
|
));
|
||||||
|
}
|
||||||
|
|
||||||
|
let queue_dir = config
|
||||||
|
.lookup(WEBHOOK_QUEUE_DIR)
|
||||||
|
.unwrap_or_else(|| default_queue_dir.to_string());
|
||||||
|
if !queue_dir.is_empty() && !Path::new(&queue_dir).is_absolute() {
|
||||||
|
return Err(TargetError::Configuration("Webhook queue directory must be an absolute path".to_string()));
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn build_mqtt_args(config: &KVS, default_queue_dir: &str, target_type: TargetType) -> Result<MQTTArgs, TargetError> {
|
||||||
|
let broker = config
|
||||||
|
.lookup(MQTT_BROKER)
|
||||||
|
.ok_or_else(|| TargetError::Configuration("Missing MQTT broker".to_string()))?;
|
||||||
|
let broker_url = parse_url(&broker, "broker URL")?;
|
||||||
|
|
||||||
|
let topic = config
|
||||||
|
.lookup(MQTT_TOPIC)
|
||||||
|
.ok_or_else(|| TargetError::Configuration("Missing MQTT topic".to_string()))?;
|
||||||
|
|
||||||
|
Ok(MQTTArgs {
|
||||||
|
enable: true,
|
||||||
|
broker: broker_url,
|
||||||
|
topic,
|
||||||
|
qos: config
|
||||||
|
.lookup(MQTT_QOS)
|
||||||
|
.and_then(|v| v.parse::<u8>().ok())
|
||||||
|
.map(|q| match q {
|
||||||
|
0 => QoS::AtMostOnce,
|
||||||
|
1 => QoS::AtLeastOnce,
|
||||||
|
2 => QoS::ExactlyOnce,
|
||||||
|
_ => QoS::AtLeastOnce,
|
||||||
|
})
|
||||||
|
.unwrap_or(QoS::AtLeastOnce),
|
||||||
|
username: config.lookup(MQTT_USERNAME).unwrap_or_default(),
|
||||||
|
password: config.lookup(MQTT_PASSWORD).unwrap_or_default(),
|
||||||
|
max_reconnect_interval: config
|
||||||
|
.lookup(MQTT_RECONNECT_INTERVAL)
|
||||||
|
.and_then(|v| v.parse::<u64>().ok())
|
||||||
|
.map(Duration::from_secs)
|
||||||
|
.unwrap_or_else(|| Duration::from_secs(5)),
|
||||||
|
keep_alive: config
|
||||||
|
.lookup(MQTT_KEEP_ALIVE_INTERVAL)
|
||||||
|
.and_then(|v| v.parse::<u64>().ok())
|
||||||
|
.map(Duration::from_secs)
|
||||||
|
.unwrap_or_else(|| Duration::from_secs(30)),
|
||||||
|
tls: MQTTTlsConfig::from_values(
|
||||||
|
config.lookup(MQTT_TLS_POLICY).as_deref(),
|
||||||
|
config.lookup(MQTT_TLS_CA).as_deref(),
|
||||||
|
config.lookup(MQTT_TLS_CLIENT_CERT).as_deref(),
|
||||||
|
config.lookup(MQTT_TLS_CLIENT_KEY).as_deref(),
|
||||||
|
config.lookup(MQTT_TLS_TRUST_LEAF_AS_CA).as_deref(),
|
||||||
|
config.lookup(MQTT_WS_PATH_ALLOWLIST).as_deref(),
|
||||||
|
)?,
|
||||||
|
queue_dir: config.lookup(MQTT_QUEUE_DIR).unwrap_or_else(|| default_queue_dir.to_string()),
|
||||||
|
queue_limit: config
|
||||||
|
.lookup(MQTT_QUEUE_LIMIT)
|
||||||
|
.and_then(|v| v.parse::<u64>().ok())
|
||||||
|
.unwrap_or(DEFAULT_LIMIT),
|
||||||
|
target_type,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn validate_mqtt_config(config: &KVS) -> Result<(), TargetError> {
|
||||||
|
let broker = config
|
||||||
|
.lookup(MQTT_BROKER)
|
||||||
|
.ok_or_else(|| TargetError::Configuration("Missing MQTT broker".to_string()))?;
|
||||||
|
let url = parse_url(&broker, "broker URL")?;
|
||||||
|
|
||||||
|
let tls = MQTTTlsConfig::from_values(
|
||||||
|
config.lookup(MQTT_TLS_POLICY).as_deref(),
|
||||||
|
config.lookup(MQTT_TLS_CA).as_deref(),
|
||||||
|
config.lookup(MQTT_TLS_CLIENT_CERT).as_deref(),
|
||||||
|
config.lookup(MQTT_TLS_CLIENT_KEY).as_deref(),
|
||||||
|
config.lookup(MQTT_TLS_TRUST_LEAF_AS_CA).as_deref(),
|
||||||
|
config.lookup(MQTT_WS_PATH_ALLOWLIST).as_deref(),
|
||||||
|
)?;
|
||||||
|
validate_mqtt_broker_url(&url, &tls)?;
|
||||||
|
|
||||||
|
if config.lookup(MQTT_TOPIC).is_none() {
|
||||||
|
return Err(TargetError::Configuration("Missing MQTT topic".to_string()));
|
||||||
|
}
|
||||||
|
|
||||||
|
if let Some(qos_str) = config.lookup(MQTT_QOS) {
|
||||||
|
let qos = qos_str
|
||||||
|
.parse::<u8>()
|
||||||
|
.map_err(|_| TargetError::Configuration("Invalid QoS value".to_string()))?;
|
||||||
|
if qos > 2 {
|
||||||
|
return Err(TargetError::Configuration("QoS must be 0, 1, or 2".to_string()));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
let queue_dir = config.lookup(MQTT_QUEUE_DIR).unwrap_or_default();
|
||||||
|
if !queue_dir.is_empty() {
|
||||||
|
if !Path::new(&queue_dir).is_absolute() {
|
||||||
|
return Err(TargetError::Configuration("MQTT queue directory must be an absolute path".to_string()));
|
||||||
|
}
|
||||||
|
if let Some(qos_str) = config.lookup(MQTT_QOS)
|
||||||
|
&& qos_str == "0"
|
||||||
|
{
|
||||||
|
return Err(TargetError::Configuration(
|
||||||
|
"QoS should be AtLeastOnce (1) or ExactlyOnce (2) if queue_dir is set".to_string(),
|
||||||
|
));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn build_nats_args(config: &KVS, default_queue_dir: &str, target_type: TargetType) -> Result<NATSArgs, TargetError> {
|
||||||
|
let address = config
|
||||||
|
.lookup(NATS_ADDRESS)
|
||||||
|
.ok_or_else(|| TargetError::Configuration("Missing NATS address".to_string()))?;
|
||||||
|
validate_nats_address(&address)?;
|
||||||
|
|
||||||
|
let subject = config
|
||||||
|
.lookup(NATS_SUBJECT)
|
||||||
|
.ok_or_else(|| TargetError::Configuration("Missing NATS subject".to_string()))?;
|
||||||
|
|
||||||
|
Ok(NATSArgs {
|
||||||
|
enable: true,
|
||||||
|
address,
|
||||||
|
subject,
|
||||||
|
username: config.lookup(NATS_USERNAME).unwrap_or_default(),
|
||||||
|
password: config.lookup(NATS_PASSWORD).unwrap_or_default(),
|
||||||
|
token: config.lookup(NATS_TOKEN).unwrap_or_default(),
|
||||||
|
credentials_file: config.lookup(NATS_CREDENTIALS_FILE).unwrap_or_default(),
|
||||||
|
tls_ca: config.lookup(NATS_TLS_CA).unwrap_or_default(),
|
||||||
|
tls_client_cert: config.lookup(NATS_TLS_CLIENT_CERT).unwrap_or_default(),
|
||||||
|
tls_client_key: config.lookup(NATS_TLS_CLIENT_KEY).unwrap_or_default(),
|
||||||
|
tls_required: parse_target_bool(config.lookup(NATS_TLS_REQUIRED).as_deref()).unwrap_or(false),
|
||||||
|
queue_dir: config.lookup(NATS_QUEUE_DIR).unwrap_or_else(|| default_queue_dir.to_string()),
|
||||||
|
queue_limit: config
|
||||||
|
.lookup(NATS_QUEUE_LIMIT)
|
||||||
|
.and_then(|v| v.parse::<u64>().ok())
|
||||||
|
.unwrap_or(DEFAULT_LIMIT),
|
||||||
|
target_type,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn validate_nats_config(config: &KVS, default_queue_dir: &str) -> Result<(), TargetError> {
|
||||||
|
let address = config
|
||||||
|
.lookup(NATS_ADDRESS)
|
||||||
|
.ok_or_else(|| TargetError::Configuration("Missing NATS address".to_string()))?;
|
||||||
|
let server = validate_nats_address(&address)?;
|
||||||
|
validate_nats_server_config(&server, config, default_queue_dir)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn build_pulsar_args(config: &KVS, default_queue_dir: &str, target_type: TargetType) -> Result<PulsarArgs, TargetError> {
|
||||||
|
let broker = config
|
||||||
|
.lookup(PULSAR_BROKER)
|
||||||
|
.ok_or_else(|| TargetError::Configuration("Missing Pulsar broker".to_string()))?;
|
||||||
|
validate_pulsar_broker(&broker)?;
|
||||||
|
|
||||||
|
let topic = config
|
||||||
|
.lookup(PULSAR_TOPIC)
|
||||||
|
.ok_or_else(|| TargetError::Configuration("Missing Pulsar topic".to_string()))?;
|
||||||
|
|
||||||
|
Ok(PulsarArgs {
|
||||||
|
enable: true,
|
||||||
|
broker,
|
||||||
|
topic,
|
||||||
|
auth_token: config.lookup(PULSAR_AUTH_TOKEN).unwrap_or_default(),
|
||||||
|
username: config.lookup(PULSAR_USERNAME).unwrap_or_default(),
|
||||||
|
password: config.lookup(PULSAR_PASSWORD).unwrap_or_default(),
|
||||||
|
tls_ca: config.lookup(PULSAR_TLS_CA).unwrap_or_default(),
|
||||||
|
tls_allow_insecure: parse_target_bool(config.lookup(PULSAR_TLS_ALLOW_INSECURE).as_deref()).unwrap_or(false),
|
||||||
|
tls_hostname_verification: parse_target_bool(config.lookup(PULSAR_TLS_HOSTNAME_VERIFICATION).as_deref()).unwrap_or(true),
|
||||||
|
queue_dir: config
|
||||||
|
.lookup(PULSAR_QUEUE_DIR)
|
||||||
|
.unwrap_or_else(|| default_queue_dir.to_string()),
|
||||||
|
queue_limit: config
|
||||||
|
.lookup(PULSAR_QUEUE_LIMIT)
|
||||||
|
.and_then(|v| v.parse::<u64>().ok())
|
||||||
|
.unwrap_or(DEFAULT_LIMIT),
|
||||||
|
target_type,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn validate_pulsar_config(config: &KVS, default_queue_dir: &str) -> Result<(), TargetError> {
|
||||||
|
let broker = config
|
||||||
|
.lookup(PULSAR_BROKER)
|
||||||
|
.ok_or_else(|| TargetError::Configuration("Missing Pulsar broker".to_string()))?;
|
||||||
|
validate_pulsar_broker_config(&broker, config, default_queue_dir)
|
||||||
|
}
|
||||||
@@ -14,12 +14,15 @@
|
|||||||
|
|
||||||
pub mod arn;
|
pub mod arn;
|
||||||
mod check;
|
mod check;
|
||||||
|
pub mod config;
|
||||||
pub mod error;
|
pub mod error;
|
||||||
pub mod store;
|
pub mod store;
|
||||||
pub mod sys;
|
pub mod sys;
|
||||||
pub mod target;
|
pub mod target;
|
||||||
|
|
||||||
pub use check::{check_mqtt_broker_available, check_mqtt_broker_available_with_tls};
|
pub use check::{
|
||||||
|
check_mqtt_broker_available, check_mqtt_broker_available_with_tls, check_nats_server_available, check_pulsar_broker_available,
|
||||||
|
};
|
||||||
pub use error::{StoreError, TargetError};
|
pub use error::{StoreError, TargetError};
|
||||||
pub use rustfs_s3_common::EventName;
|
pub use rustfs_s3_common::EventName;
|
||||||
use serde::{Deserialize, Serialize};
|
use serde::{Deserialize, Serialize};
|
||||||
|
|||||||
@@ -26,6 +26,8 @@ use std::time::{SystemTime, UNIX_EPOCH};
|
|||||||
use tracing::warn;
|
use tracing::warn;
|
||||||
|
|
||||||
pub mod mqtt;
|
pub mod mqtt;
|
||||||
|
pub mod nats;
|
||||||
|
pub mod pulsar;
|
||||||
pub mod webhook;
|
pub mod webhook;
|
||||||
|
|
||||||
/// A read-only snapshot of delivery counters for a target.
|
/// A read-only snapshot of delivery counters for a target.
|
||||||
@@ -286,6 +288,8 @@ pub enum ChannelTargetType {
|
|||||||
Webhook,
|
Webhook,
|
||||||
Kafka,
|
Kafka,
|
||||||
Mqtt,
|
Mqtt,
|
||||||
|
Nats,
|
||||||
|
Pulsar,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl ChannelTargetType {
|
impl ChannelTargetType {
|
||||||
@@ -294,6 +298,8 @@ impl ChannelTargetType {
|
|||||||
ChannelTargetType::Webhook => "webhook",
|
ChannelTargetType::Webhook => "webhook",
|
||||||
ChannelTargetType::Kafka => "kafka",
|
ChannelTargetType::Kafka => "kafka",
|
||||||
ChannelTargetType::Mqtt => "mqtt",
|
ChannelTargetType::Mqtt => "mqtt",
|
||||||
|
ChannelTargetType::Nats => "nats",
|
||||||
|
ChannelTargetType::Pulsar => "pulsar",
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -304,6 +310,8 @@ impl std::fmt::Display for ChannelTargetType {
|
|||||||
ChannelTargetType::Webhook => write!(f, "webhook"),
|
ChannelTargetType::Webhook => write!(f, "webhook"),
|
||||||
ChannelTargetType::Kafka => write!(f, "kafka"),
|
ChannelTargetType::Kafka => write!(f, "kafka"),
|
||||||
ChannelTargetType::Mqtt => write!(f, "mqtt"),
|
ChannelTargetType::Mqtt => write!(f, "mqtt"),
|
||||||
|
ChannelTargetType::Nats => write!(f, "nats"),
|
||||||
|
ChannelTargetType::Pulsar => write!(f, "pulsar"),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,367 @@
|
|||||||
|
// 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.
|
||||||
|
|
||||||
|
use crate::{
|
||||||
|
StoreError, Target, TargetLog,
|
||||||
|
arn::TargetID,
|
||||||
|
error::TargetError,
|
||||||
|
store::{Key, QueueStore, Store},
|
||||||
|
target::{
|
||||||
|
ChannelTargetType, EntityTarget, QueuedPayload, QueuedPayloadMeta, TargetDeliveryCounters, TargetDeliverySnapshot,
|
||||||
|
TargetType,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
use async_trait::async_trait;
|
||||||
|
use rustfs_config::{NATS_CREDENTIALS_FILE, NATS_TLS_CA, NATS_TLS_CLIENT_CERT, NATS_TLS_CLIENT_KEY};
|
||||||
|
use serde::Serialize;
|
||||||
|
use serde::de::DeserializeOwned;
|
||||||
|
use std::path::{Path, PathBuf};
|
||||||
|
use std::str::FromStr;
|
||||||
|
use std::sync::atomic::{AtomicBool, Ordering};
|
||||||
|
use std::sync::{Arc, Mutex};
|
||||||
|
use tracing::{error, info, instrument};
|
||||||
|
|
||||||
|
#[derive(Debug, Clone)]
|
||||||
|
pub struct NATSArgs {
|
||||||
|
pub enable: bool,
|
||||||
|
pub address: String,
|
||||||
|
pub subject: String,
|
||||||
|
pub username: String,
|
||||||
|
pub password: String,
|
||||||
|
pub token: String,
|
||||||
|
pub credentials_file: String,
|
||||||
|
pub tls_ca: String,
|
||||||
|
pub tls_client_cert: String,
|
||||||
|
pub tls_client_key: String,
|
||||||
|
pub tls_required: bool,
|
||||||
|
pub queue_dir: String,
|
||||||
|
pub queue_limit: u64,
|
||||||
|
pub target_type: TargetType,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl NATSArgs {
|
||||||
|
pub fn validate(&self) -> Result<(), TargetError> {
|
||||||
|
if !self.enable {
|
||||||
|
return Ok(());
|
||||||
|
}
|
||||||
|
|
||||||
|
validate_nats_address(&self.address)?;
|
||||||
|
validate_nats_auth(self)?;
|
||||||
|
|
||||||
|
if self.subject.trim().is_empty() || self.subject.chars().any(char::is_whitespace) {
|
||||||
|
return Err(TargetError::Configuration(
|
||||||
|
"NATS subject cannot be empty or contain whitespace".to_string(),
|
||||||
|
));
|
||||||
|
}
|
||||||
|
|
||||||
|
if !self.credentials_file.is_empty() && !Path::new(&self.credentials_file).is_absolute() {
|
||||||
|
return Err(TargetError::Configuration(format!("{NATS_CREDENTIALS_FILE} must be an absolute path")));
|
||||||
|
}
|
||||||
|
if !self.tls_ca.is_empty() && !Path::new(&self.tls_ca).is_absolute() {
|
||||||
|
return Err(TargetError::Configuration(format!("{NATS_TLS_CA} must be an absolute path")));
|
||||||
|
}
|
||||||
|
if !self.tls_client_cert.is_empty() && !Path::new(&self.tls_client_cert).is_absolute() {
|
||||||
|
return Err(TargetError::Configuration(format!("{NATS_TLS_CLIENT_CERT} must be an absolute path")));
|
||||||
|
}
|
||||||
|
if !self.tls_client_key.is_empty() && !Path::new(&self.tls_client_key).is_absolute() {
|
||||||
|
return Err(TargetError::Configuration(format!("{NATS_TLS_CLIENT_KEY} must be an absolute path")));
|
||||||
|
}
|
||||||
|
if self.tls_client_cert.is_empty() != self.tls_client_key.is_empty() {
|
||||||
|
return Err(TargetError::Configuration(
|
||||||
|
"NATS tls_client_cert and tls_client_key must be specified together".to_string(),
|
||||||
|
));
|
||||||
|
}
|
||||||
|
|
||||||
|
if !self.queue_dir.is_empty() && !Path::new(&self.queue_dir).is_absolute() {
|
||||||
|
return Err(TargetError::Configuration("NATS queue directory must be an absolute path".to_string()));
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn validate_nats_address(address: &str) -> Result<async_nats::ServerAddr, TargetError> {
|
||||||
|
let server = async_nats::ServerAddr::from_str(address)
|
||||||
|
.map_err(|e| TargetError::Configuration(format!("Invalid NATS address: {e}")))?;
|
||||||
|
|
||||||
|
if server.has_user_pass() {
|
||||||
|
return Err(TargetError::Configuration("NATS address must not embed username or password".to_string()));
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(server)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn validate_nats_auth(args: &NATSArgs) -> Result<(), TargetError> {
|
||||||
|
let mut auth_methods = 0usize;
|
||||||
|
|
||||||
|
if !args.token.is_empty() {
|
||||||
|
auth_methods += 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
if !args.credentials_file.is_empty() {
|
||||||
|
auth_methods += 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
let has_user = !args.username.is_empty();
|
||||||
|
let has_password = !args.password.is_empty();
|
||||||
|
if has_user || has_password {
|
||||||
|
if has_user != has_password {
|
||||||
|
return Err(TargetError::Configuration(
|
||||||
|
"NATS username and password must be specified together".to_string(),
|
||||||
|
));
|
||||||
|
}
|
||||||
|
auth_methods += 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
if auth_methods > 1 {
|
||||||
|
return Err(TargetError::Configuration(
|
||||||
|
"NATS supports only one auth method at a time: token, username/password, or credentials_file".to_string(),
|
||||||
|
));
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
pub async fn connect_nats(args: &NATSArgs) -> Result<async_nats::Client, TargetError> {
|
||||||
|
args.validate()?;
|
||||||
|
|
||||||
|
let mut options = async_nats::ConnectOptions::new().require_tls(args.tls_required);
|
||||||
|
|
||||||
|
if !args.token.is_empty() {
|
||||||
|
options = options.token(args.token.clone());
|
||||||
|
} else if !args.username.is_empty() {
|
||||||
|
options = options.user_and_password(args.username.clone(), args.password.clone());
|
||||||
|
} else if !args.credentials_file.is_empty() {
|
||||||
|
options = options
|
||||||
|
.credentials_file(&args.credentials_file)
|
||||||
|
.await
|
||||||
|
.map_err(|e| TargetError::Configuration(format!("Failed to load NATS credentials file: {e}")))?;
|
||||||
|
}
|
||||||
|
|
||||||
|
if !args.tls_ca.is_empty() {
|
||||||
|
options = options.add_root_certificates(PathBuf::from(&args.tls_ca));
|
||||||
|
}
|
||||||
|
if !args.tls_client_cert.is_empty() {
|
||||||
|
options = options.add_client_certificate(PathBuf::from(&args.tls_client_cert), PathBuf::from(&args.tls_client_key));
|
||||||
|
}
|
||||||
|
|
||||||
|
options
|
||||||
|
.connect(args.address.clone())
|
||||||
|
.await
|
||||||
|
.map_err(|e| TargetError::Network(format!("Failed to connect to NATS server: {e}")))
|
||||||
|
}
|
||||||
|
|
||||||
|
pub struct NATSTarget<E>
|
||||||
|
where
|
||||||
|
E: Send + Sync + 'static + Clone + Serialize + DeserializeOwned,
|
||||||
|
{
|
||||||
|
id: TargetID,
|
||||||
|
args: NATSArgs,
|
||||||
|
client: Mutex<Option<async_nats::Client>>,
|
||||||
|
store: Option<Box<dyn Store<QueuedPayload, Error = StoreError, Key = Key> + Send + Sync>>,
|
||||||
|
connected: AtomicBool,
|
||||||
|
delivery_counters: Arc<TargetDeliveryCounters>,
|
||||||
|
_phantom: std::marker::PhantomData<E>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<E> NATSTarget<E>
|
||||||
|
where
|
||||||
|
E: Send + Sync + 'static + Clone + Serialize + DeserializeOwned,
|
||||||
|
{
|
||||||
|
pub fn clone_box(&self) -> Box<dyn Target<E> + Send + Sync> {
|
||||||
|
Box::new(NATSTarget::<E> {
|
||||||
|
id: self.id.clone(),
|
||||||
|
args: self.args.clone(),
|
||||||
|
client: Mutex::new(self.client.lock().unwrap().clone()),
|
||||||
|
store: self.store.as_ref().map(|s| s.boxed_clone()),
|
||||||
|
connected: AtomicBool::new(self.connected.load(Ordering::SeqCst)),
|
||||||
|
delivery_counters: Arc::clone(&self.delivery_counters),
|
||||||
|
_phantom: std::marker::PhantomData,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
#[instrument(skip(args), fields(target_id_as_string = %id))]
|
||||||
|
pub fn new(id: String, args: NATSArgs) -> Result<Self, TargetError> {
|
||||||
|
args.validate()?;
|
||||||
|
let target_id = TargetID::new(id, ChannelTargetType::Nats.as_str().to_string());
|
||||||
|
let queue_store = if !args.queue_dir.is_empty() {
|
||||||
|
let base_path = PathBuf::from(&args.queue_dir);
|
||||||
|
let specific_queue_path = base_path.join(format!("rustfs-{}-{}", ChannelTargetType::Nats.as_str(), target_id.id));
|
||||||
|
let extension = match args.target_type {
|
||||||
|
TargetType::AuditLog => rustfs_config::audit::AUDIT_STORE_EXTENSION,
|
||||||
|
TargetType::NotifyEvent => rustfs_config::notify::NOTIFY_STORE_EXTENSION,
|
||||||
|
};
|
||||||
|
let store = QueueStore::<QueuedPayload>::new(specific_queue_path, args.queue_limit, extension);
|
||||||
|
if let Err(e) = store.open() {
|
||||||
|
error!(target_id = %target_id, error = %e, "Failed to open store for NATS target");
|
||||||
|
return Err(TargetError::Storage(format!("{e}")));
|
||||||
|
}
|
||||||
|
Some(Box::new(store) as Box<dyn Store<QueuedPayload, Error = StoreError, Key = Key> + Send + Sync>)
|
||||||
|
} else {
|
||||||
|
None
|
||||||
|
};
|
||||||
|
|
||||||
|
Ok(Self {
|
||||||
|
id: target_id,
|
||||||
|
args,
|
||||||
|
client: Mutex::new(None),
|
||||||
|
store: queue_store,
|
||||||
|
connected: AtomicBool::new(false),
|
||||||
|
delivery_counters: Arc::new(TargetDeliveryCounters::default()),
|
||||||
|
_phantom: std::marker::PhantomData,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn get_or_connect(&self) -> Result<async_nats::Client, TargetError> {
|
||||||
|
if let Some(client) = self.client.lock().unwrap().clone() {
|
||||||
|
return Ok(client);
|
||||||
|
}
|
||||||
|
|
||||||
|
let client = connect_nats(&self.args).await?;
|
||||||
|
client
|
||||||
|
.flush()
|
||||||
|
.await
|
||||||
|
.map_err(|e| TargetError::Network(format!("Failed to flush NATS connection: {e}")))?;
|
||||||
|
self.connected.store(true, Ordering::SeqCst);
|
||||||
|
|
||||||
|
let mut guard = self.client.lock().unwrap();
|
||||||
|
let shared = guard.get_or_insert_with(|| client.clone()).clone();
|
||||||
|
Ok(shared)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn build_queued_payload(&self, event: &EntityTarget<E>) -> Result<QueuedPayload, TargetError> {
|
||||||
|
let object_name = crate::target::decode_object_name(&event.object_name)?;
|
||||||
|
let key = format!("{}/{}", event.bucket_name, object_name);
|
||||||
|
let log = TargetLog {
|
||||||
|
event_name: event.event_name,
|
||||||
|
key,
|
||||||
|
records: vec![event.clone()],
|
||||||
|
};
|
||||||
|
let body = serde_json::to_vec(&log).map_err(|e| TargetError::Serialization(format!("Failed to serialize event: {e}")))?;
|
||||||
|
let meta = QueuedPayloadMeta::new(
|
||||||
|
event.event_name,
|
||||||
|
event.bucket_name.clone(),
|
||||||
|
event.object_name.clone(),
|
||||||
|
"application/json",
|
||||||
|
body.len(),
|
||||||
|
);
|
||||||
|
Ok(QueuedPayload::new(meta, body))
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn send_body(&self, body: Vec<u8>) -> Result<(), TargetError> {
|
||||||
|
let client = self.get_or_connect().await?;
|
||||||
|
client
|
||||||
|
.publish(self.args.subject.clone(), body.into())
|
||||||
|
.await
|
||||||
|
.map_err(|e| TargetError::Request(format!("Failed to publish NATS message: {e}")))?;
|
||||||
|
self.delivery_counters.record_success();
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[async_trait]
|
||||||
|
impl<E> Target<E> for NATSTarget<E>
|
||||||
|
where
|
||||||
|
E: Send + Sync + 'static + Clone + Serialize + DeserializeOwned,
|
||||||
|
{
|
||||||
|
fn id(&self) -> TargetID {
|
||||||
|
self.id.clone()
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn is_active(&self) -> Result<bool, TargetError> {
|
||||||
|
let client = self.get_or_connect().await?;
|
||||||
|
client
|
||||||
|
.flush()
|
||||||
|
.await
|
||||||
|
.map_err(|e| TargetError::Network(format!("NATS health check failed: {e}")))?;
|
||||||
|
Ok(true)
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn save(&self, event: Arc<EntityTarget<E>>) -> Result<(), TargetError> {
|
||||||
|
let queued = match self.build_queued_payload(&event) {
|
||||||
|
Ok(queued) => queued,
|
||||||
|
Err(err) => {
|
||||||
|
self.delivery_counters.record_final_failure();
|
||||||
|
return Err(err);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
if let Some(store) = &self.store {
|
||||||
|
let encoded = match queued.encode() {
|
||||||
|
Ok(encoded) => encoded,
|
||||||
|
Err(err) => {
|
||||||
|
self.delivery_counters.record_final_failure();
|
||||||
|
return Err(TargetError::Storage(format!("Failed to encode queued payload: {err}")));
|
||||||
|
}
|
||||||
|
};
|
||||||
|
if let Err(e) = store.put_raw(&encoded) {
|
||||||
|
self.delivery_counters.record_final_failure();
|
||||||
|
return Err(TargetError::Storage(format!("Failed to save event to store: {e}")));
|
||||||
|
}
|
||||||
|
Ok(())
|
||||||
|
} else {
|
||||||
|
if let Err(err) = self.send_body(queued.body).await {
|
||||||
|
self.delivery_counters.record_final_failure();
|
||||||
|
return Err(err);
|
||||||
|
}
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn send_raw_from_store(&self, _key: Key, body: Vec<u8>, _meta: QueuedPayloadMeta) -> Result<(), TargetError> {
|
||||||
|
self.send_body(body).await
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn close(&self) -> Result<(), TargetError> {
|
||||||
|
let client = self.client.lock().unwrap().take();
|
||||||
|
self.connected.store(false, Ordering::SeqCst);
|
||||||
|
if let Some(client) = client {
|
||||||
|
client
|
||||||
|
.drain()
|
||||||
|
.await
|
||||||
|
.map_err(|e| TargetError::Network(format!("Failed to drain NATS client: {e}")))?;
|
||||||
|
}
|
||||||
|
info!(target_id = %self.id, "NATS target closed");
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
fn store(&self) -> Option<&(dyn Store<QueuedPayload, Error = StoreError, Key = Key> + Send + Sync)> {
|
||||||
|
self.store.as_deref()
|
||||||
|
}
|
||||||
|
|
||||||
|
fn clone_dyn(&self) -> Box<dyn Target<E> + Send + Sync> {
|
||||||
|
self.clone_box()
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn init(&self) -> Result<(), TargetError> {
|
||||||
|
if !self.is_enabled() {
|
||||||
|
return Ok(());
|
||||||
|
}
|
||||||
|
let _ = self.get_or_connect().await?;
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
fn is_enabled(&self) -> bool {
|
||||||
|
self.args.enable
|
||||||
|
}
|
||||||
|
|
||||||
|
fn delivery_snapshot(&self) -> TargetDeliverySnapshot {
|
||||||
|
self.delivery_counters
|
||||||
|
.snapshot(self.store.as_deref().map_or(0, |store| store.len() as u64))
|
||||||
|
}
|
||||||
|
|
||||||
|
fn record_final_failure(&self) {
|
||||||
|
self.delivery_counters.record_final_failure();
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,387 @@
|
|||||||
|
// 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.
|
||||||
|
|
||||||
|
use crate::{
|
||||||
|
StoreError, Target, TargetLog,
|
||||||
|
arn::TargetID,
|
||||||
|
error::TargetError,
|
||||||
|
store::{Key, QueueStore, Store},
|
||||||
|
target::{
|
||||||
|
ChannelTargetType, EntityTarget, QueuedPayload, QueuedPayloadMeta, TargetDeliveryCounters, TargetDeliverySnapshot,
|
||||||
|
TargetType,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
use async_trait::async_trait;
|
||||||
|
use pulsar::{Authentication, Producer, Pulsar, TokioExecutor};
|
||||||
|
use serde::Serialize;
|
||||||
|
use serde::de::DeserializeOwned;
|
||||||
|
use std::path::{Path, PathBuf};
|
||||||
|
use std::sync::atomic::{AtomicBool, Ordering};
|
||||||
|
use std::sync::{Arc, Mutex};
|
||||||
|
use tokio::sync::Mutex as AsyncMutex;
|
||||||
|
use tracing::{error, info, instrument};
|
||||||
|
use url::Url;
|
||||||
|
|
||||||
|
#[derive(Debug, Clone)]
|
||||||
|
pub struct PulsarArgs {
|
||||||
|
pub enable: bool,
|
||||||
|
pub broker: String,
|
||||||
|
pub topic: String,
|
||||||
|
pub auth_token: String,
|
||||||
|
pub username: String,
|
||||||
|
pub password: String,
|
||||||
|
pub tls_ca: String,
|
||||||
|
pub tls_allow_insecure: bool,
|
||||||
|
pub tls_hostname_verification: bool,
|
||||||
|
pub queue_dir: String,
|
||||||
|
pub queue_limit: u64,
|
||||||
|
pub target_type: TargetType,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl PulsarArgs {
|
||||||
|
pub fn validate(&self) -> Result<(), TargetError> {
|
||||||
|
if !self.enable {
|
||||||
|
return Ok(());
|
||||||
|
}
|
||||||
|
|
||||||
|
validate_pulsar_broker(&self.broker)?;
|
||||||
|
|
||||||
|
if self.topic.trim().is_empty() {
|
||||||
|
return Err(TargetError::Configuration("Pulsar topic cannot be empty".to_string()));
|
||||||
|
}
|
||||||
|
|
||||||
|
if !self.auth_token.is_empty() && (!self.username.is_empty() || !self.password.is_empty()) {
|
||||||
|
return Err(TargetError::Configuration(
|
||||||
|
"Pulsar supports either auth_token or username/password auth, not both".to_string(),
|
||||||
|
));
|
||||||
|
}
|
||||||
|
|
||||||
|
if self.username.is_empty() != self.password.is_empty() {
|
||||||
|
return Err(TargetError::Configuration(
|
||||||
|
"Pulsar username and password must be specified together".to_string(),
|
||||||
|
));
|
||||||
|
}
|
||||||
|
|
||||||
|
if !self.tls_ca.is_empty() && !Path::new(&self.tls_ca).is_absolute() {
|
||||||
|
return Err(TargetError::Configuration("Pulsar tls_ca must be an absolute path".to_string()));
|
||||||
|
}
|
||||||
|
|
||||||
|
if !self.queue_dir.is_empty() && !Path::new(&self.queue_dir).is_absolute() {
|
||||||
|
return Err(TargetError::Configuration("Pulsar queue directory must be an absolute path".to_string()));
|
||||||
|
}
|
||||||
|
|
||||||
|
let parsed = Url::parse(&self.broker)
|
||||||
|
.map_err(|e| TargetError::Configuration(format!("Invalid Pulsar broker URL: {e} (value: '{}')", self.broker)))?;
|
||||||
|
let tls_enabled = parsed.scheme() == "pulsar+ssl";
|
||||||
|
if !tls_enabled && (!self.tls_ca.is_empty() || self.tls_allow_insecure || !self.tls_hostname_verification) {
|
||||||
|
return Err(TargetError::Configuration(
|
||||||
|
"Pulsar TLS settings are only allowed with pulsar+ssl brokers".to_string(),
|
||||||
|
));
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn validate_pulsar_broker(broker: &str) -> Result<Url, TargetError> {
|
||||||
|
let url = Url::parse(broker)
|
||||||
|
.map_err(|e| TargetError::Configuration(format!("Invalid Pulsar broker URL: {e} (value: '{broker}')")))?;
|
||||||
|
|
||||||
|
match url.scheme() {
|
||||||
|
"pulsar" | "pulsar+ssl" => {}
|
||||||
|
_ => {
|
||||||
|
return Err(TargetError::Configuration(
|
||||||
|
"Pulsar broker must use pulsar:// or pulsar+ssl://".to_string(),
|
||||||
|
));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if !url.username().is_empty() || url.password().is_some() {
|
||||||
|
return Err(TargetError::Configuration(
|
||||||
|
"Pulsar broker URL must not embed username or password".to_string(),
|
||||||
|
));
|
||||||
|
}
|
||||||
|
|
||||||
|
if url.host_str().is_none() {
|
||||||
|
return Err(TargetError::Configuration("Pulsar broker is missing host".to_string()));
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(url)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub async fn connect_pulsar(args: &PulsarArgs) -> Result<Pulsar<TokioExecutor>, TargetError> {
|
||||||
|
args.validate()?;
|
||||||
|
|
||||||
|
let mut builder = Pulsar::builder(args.broker.clone(), TokioExecutor);
|
||||||
|
|
||||||
|
if !args.auth_token.is_empty() {
|
||||||
|
builder = builder.with_auth(Authentication {
|
||||||
|
name: "token".to_string(),
|
||||||
|
data: args.auth_token.clone().into_bytes(),
|
||||||
|
});
|
||||||
|
} else if !args.username.is_empty() {
|
||||||
|
builder =
|
||||||
|
builder.with_auth_provider(pulsar::authentication::basic::BasicAuthentication::new(&args.username, &args.password));
|
||||||
|
}
|
||||||
|
|
||||||
|
if !args.tls_ca.is_empty() {
|
||||||
|
builder = builder
|
||||||
|
.with_certificate_chain_file(&args.tls_ca)
|
||||||
|
.map_err(|e| TargetError::Configuration(format!("Failed to load Pulsar tls_ca: {e}")))?;
|
||||||
|
}
|
||||||
|
|
||||||
|
builder = builder
|
||||||
|
.with_allow_insecure_connection(args.tls_allow_insecure)
|
||||||
|
.with_tls_hostname_verification_enabled(args.tls_hostname_verification);
|
||||||
|
|
||||||
|
builder
|
||||||
|
.build()
|
||||||
|
.await
|
||||||
|
.map_err(|e| TargetError::Network(format!("Failed to connect to Pulsar broker: {e}")))
|
||||||
|
}
|
||||||
|
|
||||||
|
pub struct PulsarTarget<E>
|
||||||
|
where
|
||||||
|
E: Send + Sync + 'static + Clone + Serialize + DeserializeOwned,
|
||||||
|
{
|
||||||
|
id: TargetID,
|
||||||
|
args: PulsarArgs,
|
||||||
|
client: Mutex<Option<Pulsar<TokioExecutor>>>,
|
||||||
|
producer: AsyncMutex<Option<Producer<TokioExecutor>>>,
|
||||||
|
store: Option<Box<dyn Store<QueuedPayload, Error = StoreError, Key = Key> + Send + Sync>>,
|
||||||
|
connected: AtomicBool,
|
||||||
|
delivery_counters: Arc<TargetDeliveryCounters>,
|
||||||
|
_phantom: std::marker::PhantomData<E>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<E> PulsarTarget<E>
|
||||||
|
where
|
||||||
|
E: Send + Sync + 'static + Clone + Serialize + DeserializeOwned,
|
||||||
|
{
|
||||||
|
pub fn clone_box(&self) -> Box<dyn Target<E> + Send + Sync> {
|
||||||
|
Box::new(PulsarTarget::<E> {
|
||||||
|
id: self.id.clone(),
|
||||||
|
args: self.args.clone(),
|
||||||
|
client: Mutex::new(self.client.lock().unwrap().clone()),
|
||||||
|
producer: AsyncMutex::new(None),
|
||||||
|
store: self.store.as_ref().map(|s| s.boxed_clone()),
|
||||||
|
connected: AtomicBool::new(self.connected.load(Ordering::SeqCst)),
|
||||||
|
delivery_counters: Arc::clone(&self.delivery_counters),
|
||||||
|
_phantom: std::marker::PhantomData,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
#[instrument(skip(args), fields(target_id_as_string = %id))]
|
||||||
|
pub fn new(id: String, args: PulsarArgs) -> Result<Self, TargetError> {
|
||||||
|
args.validate()?;
|
||||||
|
let target_id = TargetID::new(id, ChannelTargetType::Pulsar.as_str().to_string());
|
||||||
|
let queue_store = if !args.queue_dir.is_empty() {
|
||||||
|
let base_path = PathBuf::from(&args.queue_dir);
|
||||||
|
let specific_queue_path = base_path.join(format!("rustfs-{}-{}", ChannelTargetType::Pulsar.as_str(), target_id.id));
|
||||||
|
let extension = match args.target_type {
|
||||||
|
TargetType::AuditLog => rustfs_config::audit::AUDIT_STORE_EXTENSION,
|
||||||
|
TargetType::NotifyEvent => rustfs_config::notify::NOTIFY_STORE_EXTENSION,
|
||||||
|
};
|
||||||
|
let store = QueueStore::<QueuedPayload>::new(specific_queue_path, args.queue_limit, extension);
|
||||||
|
if let Err(e) = store.open() {
|
||||||
|
error!(target_id = %target_id, error = %e, "Failed to open store for Pulsar target");
|
||||||
|
return Err(TargetError::Storage(format!("{e}")));
|
||||||
|
}
|
||||||
|
Some(Box::new(store) as Box<dyn Store<QueuedPayload, Error = StoreError, Key = Key> + Send + Sync>)
|
||||||
|
} else {
|
||||||
|
None
|
||||||
|
};
|
||||||
|
|
||||||
|
Ok(Self {
|
||||||
|
id: target_id,
|
||||||
|
args,
|
||||||
|
client: Mutex::new(None),
|
||||||
|
producer: AsyncMutex::new(None),
|
||||||
|
store: queue_store,
|
||||||
|
connected: AtomicBool::new(false),
|
||||||
|
delivery_counters: Arc::new(TargetDeliveryCounters::default()),
|
||||||
|
_phantom: std::marker::PhantomData,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn get_or_connect_client(&self) -> Result<Pulsar<TokioExecutor>, TargetError> {
|
||||||
|
if let Some(client) = self.client.lock().unwrap().clone() {
|
||||||
|
return Ok(client);
|
||||||
|
}
|
||||||
|
|
||||||
|
let client = connect_pulsar(&self.args).await?;
|
||||||
|
self.connected.store(true, Ordering::SeqCst);
|
||||||
|
let mut guard = self.client.lock().unwrap();
|
||||||
|
let shared = guard.get_or_insert_with(|| client.clone()).clone();
|
||||||
|
Ok(shared)
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn init_producer(&self) -> Result<(), TargetError> {
|
||||||
|
if self.producer.lock().await.is_some() {
|
||||||
|
return Ok(());
|
||||||
|
}
|
||||||
|
|
||||||
|
let client = self.get_or_connect_client().await?;
|
||||||
|
let producer = client
|
||||||
|
.producer()
|
||||||
|
.with_topic(self.args.topic.clone())
|
||||||
|
.with_name(self.id.id.clone())
|
||||||
|
.build()
|
||||||
|
.await
|
||||||
|
.map_err(|e| TargetError::Network(format!("Failed to create Pulsar producer: {e}")))?;
|
||||||
|
|
||||||
|
let mut guard = self.producer.lock().await;
|
||||||
|
if guard.is_none() {
|
||||||
|
*guard = Some(producer);
|
||||||
|
}
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
fn build_queued_payload(&self, event: &EntityTarget<E>) -> Result<QueuedPayload, TargetError> {
|
||||||
|
let object_name = crate::target::decode_object_name(&event.object_name)?;
|
||||||
|
let key = format!("{}/{}", event.bucket_name, object_name);
|
||||||
|
let log = TargetLog {
|
||||||
|
event_name: event.event_name,
|
||||||
|
key,
|
||||||
|
records: vec![event.clone()],
|
||||||
|
};
|
||||||
|
let body = serde_json::to_vec(&log).map_err(|e| TargetError::Serialization(format!("Failed to serialize event: {e}")))?;
|
||||||
|
let meta = QueuedPayloadMeta::new(
|
||||||
|
event.event_name,
|
||||||
|
event.bucket_name.clone(),
|
||||||
|
event.object_name.clone(),
|
||||||
|
"application/json",
|
||||||
|
body.len(),
|
||||||
|
);
|
||||||
|
Ok(QueuedPayload::new(meta, body))
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn send_body(&self, body: Vec<u8>) -> Result<(), TargetError> {
|
||||||
|
self.init_producer().await?;
|
||||||
|
let mut guard = self.producer.lock().await;
|
||||||
|
let producer = guard
|
||||||
|
.as_mut()
|
||||||
|
.ok_or_else(|| TargetError::Configuration("Pulsar producer not initialized".to_string()))?;
|
||||||
|
let receipt = producer
|
||||||
|
.send_non_blocking(body)
|
||||||
|
.await
|
||||||
|
.map_err(|e| TargetError::Request(format!("Failed to send Pulsar message: {e}")))?;
|
||||||
|
receipt
|
||||||
|
.await
|
||||||
|
.map_err(|e| TargetError::Request(format!("Failed to receive Pulsar receipt: {e}")))?;
|
||||||
|
self.delivery_counters.record_success();
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[async_trait]
|
||||||
|
impl<E> Target<E> for PulsarTarget<E>
|
||||||
|
where
|
||||||
|
E: Send + Sync + 'static + Clone + Serialize + DeserializeOwned,
|
||||||
|
{
|
||||||
|
fn id(&self) -> TargetID {
|
||||||
|
self.id.clone()
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn is_active(&self) -> Result<bool, TargetError> {
|
||||||
|
self.init_producer().await?;
|
||||||
|
let guard = self.producer.lock().await;
|
||||||
|
let producer = guard
|
||||||
|
.as_ref()
|
||||||
|
.ok_or_else(|| TargetError::Configuration("Pulsar producer not initialized".to_string()))?;
|
||||||
|
producer
|
||||||
|
.check_connection()
|
||||||
|
.await
|
||||||
|
.map_err(|e| TargetError::Network(format!("Pulsar health check failed: {e}")))?;
|
||||||
|
Ok(true)
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn save(&self, event: Arc<EntityTarget<E>>) -> Result<(), TargetError> {
|
||||||
|
let queued = match self.build_queued_payload(&event) {
|
||||||
|
Ok(queued) => queued,
|
||||||
|
Err(err) => {
|
||||||
|
self.delivery_counters.record_final_failure();
|
||||||
|
return Err(err);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
if let Some(store) = &self.store {
|
||||||
|
let encoded = match queued.encode() {
|
||||||
|
Ok(encoded) => encoded,
|
||||||
|
Err(err) => {
|
||||||
|
self.delivery_counters.record_final_failure();
|
||||||
|
return Err(TargetError::Storage(format!("Failed to encode queued payload: {err}")));
|
||||||
|
}
|
||||||
|
};
|
||||||
|
if let Err(e) = store.put_raw(&encoded) {
|
||||||
|
self.delivery_counters.record_final_failure();
|
||||||
|
return Err(TargetError::Storage(format!("Failed to save event to store: {e}")));
|
||||||
|
}
|
||||||
|
Ok(())
|
||||||
|
} else {
|
||||||
|
if let Err(err) = self.send_body(queued.body).await {
|
||||||
|
self.delivery_counters.record_final_failure();
|
||||||
|
return Err(err);
|
||||||
|
}
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn send_raw_from_store(&self, _key: Key, body: Vec<u8>, _meta: QueuedPayloadMeta) -> Result<(), TargetError> {
|
||||||
|
self.send_body(body).await
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn close(&self) -> Result<(), TargetError> {
|
||||||
|
let mut producer = self.producer.lock().await;
|
||||||
|
if let Some(producer) = producer.as_mut() {
|
||||||
|
producer
|
||||||
|
.close()
|
||||||
|
.await
|
||||||
|
.map_err(|e| TargetError::Network(format!("Failed to close Pulsar producer: {e}")))?;
|
||||||
|
}
|
||||||
|
*producer = None;
|
||||||
|
self.client.lock().unwrap().take();
|
||||||
|
self.connected.store(false, Ordering::SeqCst);
|
||||||
|
info!(target_id = %self.id, "Pulsar target closed");
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
fn store(&self) -> Option<&(dyn Store<QueuedPayload, Error = StoreError, Key = Key> + Send + Sync)> {
|
||||||
|
self.store.as_deref()
|
||||||
|
}
|
||||||
|
|
||||||
|
fn clone_dyn(&self) -> Box<dyn Target<E> + Send + Sync> {
|
||||||
|
self.clone_box()
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn init(&self) -> Result<(), TargetError> {
|
||||||
|
if !self.is_enabled() {
|
||||||
|
return Ok(());
|
||||||
|
}
|
||||||
|
self.init_producer().await
|
||||||
|
}
|
||||||
|
|
||||||
|
fn is_enabled(&self) -> bool {
|
||||||
|
self.args.enable
|
||||||
|
}
|
||||||
|
|
||||||
|
fn delivery_snapshot(&self) -> TargetDeliverySnapshot {
|
||||||
|
self.delivery_counters
|
||||||
|
.snapshot(self.store.as_deref().map_or(0, |store| store.len() as u64))
|
||||||
|
}
|
||||||
|
|
||||||
|
fn record_final_failure(&self) {
|
||||||
|
self.delivery_counters.record_final_failure();
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -14,6 +14,13 @@
|
|||||||
|
|
||||||
use crate::admin::{
|
use crate::admin::{
|
||||||
auth::validate_admin_request,
|
auth::validate_admin_request,
|
||||||
|
handlers::target_descriptor::{
|
||||||
|
AdminTargetSpec, AdminTargetValidator, EndpointKey, TargetDomain, allowed_target_keys,
|
||||||
|
collect_config_entry_keys as shared_collect_config_entry_keys,
|
||||||
|
collect_configured_endpoint_keys as shared_collect_configured_endpoint_keys,
|
||||||
|
collect_env_endpoint_keys as shared_collect_env_endpoint_keys, normalized_endpoint_key, target_service_name, target_spec,
|
||||||
|
validate_target_request,
|
||||||
|
},
|
||||||
router::{AdminOperation, Operation, S3Router},
|
router::{AdminOperation, Operation, S3Router},
|
||||||
};
|
};
|
||||||
use crate::auth::{check_key_valid, get_session_token};
|
use crate::auth::{check_key_valid, get_session_token};
|
||||||
@@ -24,22 +31,20 @@ use http::{HeaderMap, StatusCode};
|
|||||||
use hyper::Method;
|
use hyper::Method;
|
||||||
use matchit::Params;
|
use matchit::Params;
|
||||||
use rustfs_audit::{audit_system, start_audit_system as start_global_audit_system, system::AuditSystemState};
|
use rustfs_audit::{audit_system, start_audit_system as start_global_audit_system, system::AuditSystemState};
|
||||||
use rustfs_config::audit::{AUDIT_MQTT_KEYS, AUDIT_MQTT_SUB_SYS, AUDIT_ROUTE_PREFIX, AUDIT_WEBHOOK_KEYS, AUDIT_WEBHOOK_SUB_SYS};
|
use rustfs_config::audit::{
|
||||||
use rustfs_config::{DEFAULT_DELIMITER, ENABLE_KEY, ENV_PREFIX, EnableState, MAX_ADMIN_REQUEST_BODY_SIZE};
|
AUDIT_MQTT_KEYS, AUDIT_MQTT_SUB_SYS, AUDIT_NATS_KEYS, AUDIT_NATS_SUB_SYS, AUDIT_PULSAR_KEYS, AUDIT_PULSAR_SUB_SYS,
|
||||||
|
AUDIT_ROUTE_PREFIX, AUDIT_WEBHOOK_KEYS, AUDIT_WEBHOOK_SUB_SYS,
|
||||||
|
};
|
||||||
|
use rustfs_config::{AUDIT_DEFAULT_DIR, DEFAULT_DELIMITER, ENABLE_KEY, EnableState, MAX_ADMIN_REQUEST_BODY_SIZE};
|
||||||
use rustfs_ecstore::config::Config;
|
use rustfs_ecstore::config::Config;
|
||||||
use rustfs_policy::policy::action::{Action, AdminAction};
|
use rustfs_policy::policy::action::{Action, AdminAction};
|
||||||
use rustfs_targets::{TargetError, check_mqtt_broker_available_with_tls, target::mqtt::MQTTTlsConfig};
|
|
||||||
use s3s::{Body, S3Request, S3Response, S3Result, header::CONTENT_TYPE, s3_error};
|
use s3s::{Body, S3Request, S3Response, S3Result, header::CONTENT_TYPE, s3_error};
|
||||||
use serde::{Deserialize, Serialize};
|
use serde::{Deserialize, Serialize};
|
||||||
use std::collections::{HashMap, HashSet};
|
use std::collections::{HashMap, HashSet};
|
||||||
use std::future::Future;
|
|
||||||
use std::io::{Error, ErrorKind};
|
|
||||||
use std::path::Path;
|
|
||||||
use std::sync::Arc;
|
use std::sync::Arc;
|
||||||
use tokio::sync::Semaphore;
|
use tokio::sync::Semaphore;
|
||||||
use tokio::time::{Duration, sleep, timeout};
|
use tokio::time::{Duration, timeout};
|
||||||
use tracing::{Span, warn};
|
use tracing::{Span, warn};
|
||||||
use url::Url;
|
|
||||||
|
|
||||||
pub fn register_audit_target_route(r: &mut S3Router<AdminOperation>) -> std::io::Result<()> {
|
pub fn register_audit_target_route(r: &mut S3Router<AdminOperation>) -> std::io::Result<()> {
|
||||||
r.insert(
|
r.insert(
|
||||||
@@ -87,8 +92,6 @@ struct AuditEndpointsResponse {
|
|||||||
audit_endpoints: Vec<AuditEndpoint>,
|
audit_endpoints: Vec<AuditEndpoint>,
|
||||||
}
|
}
|
||||||
|
|
||||||
type EndpointKey = (String, String);
|
|
||||||
|
|
||||||
#[derive(Clone, Copy, Debug, PartialEq, Eq, Serialize)]
|
#[derive(Clone, Copy, Debug, PartialEq, Eq, Serialize)]
|
||||||
#[serde(rename_all = "lowercase")]
|
#[serde(rename_all = "lowercase")]
|
||||||
enum AuditEndpointSource {
|
enum AuditEndpointSource {
|
||||||
@@ -98,8 +101,33 @@ enum AuditEndpointSource {
|
|||||||
Runtime,
|
Runtime,
|
||||||
}
|
}
|
||||||
|
|
||||||
fn normalized_endpoint_key(account_id: &str, service: &str) -> EndpointKey {
|
fn audit_target_specs() -> [AdminTargetSpec; 4] {
|
||||||
(account_id.to_lowercase(), service.to_string())
|
[
|
||||||
|
AdminTargetSpec {
|
||||||
|
subsystem: AUDIT_WEBHOOK_SUB_SYS,
|
||||||
|
service: "webhook",
|
||||||
|
valid_keys: AUDIT_WEBHOOK_KEYS,
|
||||||
|
validator: AdminTargetValidator::Webhook,
|
||||||
|
},
|
||||||
|
AdminTargetSpec {
|
||||||
|
subsystem: AUDIT_MQTT_SUB_SYS,
|
||||||
|
service: "mqtt",
|
||||||
|
valid_keys: AUDIT_MQTT_KEYS,
|
||||||
|
validator: AdminTargetValidator::Mqtt,
|
||||||
|
},
|
||||||
|
AdminTargetSpec {
|
||||||
|
subsystem: AUDIT_NATS_SUB_SYS,
|
||||||
|
service: "nats",
|
||||||
|
valid_keys: AUDIT_NATS_KEYS,
|
||||||
|
validator: AdminTargetValidator::Nats(TargetDomain::Audit),
|
||||||
|
},
|
||||||
|
AdminTargetSpec {
|
||||||
|
subsystem: AUDIT_PULSAR_SUB_SYS,
|
||||||
|
service: "pulsar",
|
||||||
|
valid_keys: AUDIT_PULSAR_KEYS,
|
||||||
|
validator: AdminTargetValidator::Pulsar(TargetDomain::Audit),
|
||||||
|
},
|
||||||
|
]
|
||||||
}
|
}
|
||||||
|
|
||||||
async fn authorize_audit_admin_request(req: &S3Request<Body>, action: AdminAction) -> S3Result<()> {
|
async fn authorize_audit_admin_request(req: &S3Request<Body>, action: AdminAction) -> S3Result<()> {
|
||||||
@@ -121,58 +149,9 @@ fn build_response(status: StatusCode, body: Body, request_id: Option<&http::Head
|
|||||||
S3Response::with_headers((status, body), header)
|
S3Response::with_headers((status, body), header)
|
||||||
}
|
}
|
||||||
|
|
||||||
async fn retry_with_backoff<F, Fut, T>(mut operation: F, max_attempts: usize, base_delay: Duration) -> Result<T, Error>
|
|
||||||
where
|
|
||||||
F: FnMut() -> Fut,
|
|
||||||
Fut: Future<Output = Result<T, Error>>,
|
|
||||||
{
|
|
||||||
let mut attempts = 0;
|
|
||||||
let mut delay = base_delay;
|
|
||||||
let mut last_err = None;
|
|
||||||
|
|
||||||
while attempts < max_attempts {
|
|
||||||
match operation().await {
|
|
||||||
Ok(result) => return Ok(result),
|
|
||||||
Err(e) => {
|
|
||||||
last_err = Some(e);
|
|
||||||
attempts += 1;
|
|
||||||
if attempts < max_attempts {
|
|
||||||
sleep(delay).await;
|
|
||||||
delay = delay.saturating_mul(2);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
Err(last_err.unwrap_or_else(|| Error::other("retry_with_backoff: unknown error")))
|
|
||||||
}
|
|
||||||
|
|
||||||
async fn validate_queue_dir(queue_dir: &str) -> S3Result<()> {
|
|
||||||
if !queue_dir.is_empty() {
|
|
||||||
if !Path::new(queue_dir).is_absolute() {
|
|
||||||
return Err(s3_error!(InvalidArgument, "queue_dir must be absolute path"));
|
|
||||||
}
|
|
||||||
retry_with_backoff(
|
|
||||||
|| async { tokio::fs::metadata(queue_dir).await.map(|_| ()) },
|
|
||||||
3,
|
|
||||||
Duration::from_millis(100),
|
|
||||||
)
|
|
||||||
.await
|
|
||||||
.map_err(|e| match e.kind() {
|
|
||||||
ErrorKind::NotFound => s3_error!(InvalidArgument, "queue_dir does not exist"),
|
|
||||||
ErrorKind::PermissionDenied => s3_error!(InvalidArgument, "queue_dir exists but permission denied"),
|
|
||||||
_ => s3_error!(InvalidArgument, "failed to access queue_dir: {}", e),
|
|
||||||
})?;
|
|
||||||
}
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
|
|
||||||
fn config_enable_is_on(value: &str) -> bool {
|
|
||||||
matches!(value.trim().to_ascii_lowercase().as_str(), "on" | "true" | "yes" | "1")
|
|
||||||
}
|
|
||||||
|
|
||||||
fn has_any_audit_targets(config: &Config) -> bool {
|
fn has_any_audit_targets(config: &Config) -> bool {
|
||||||
for subsystem in [AUDIT_WEBHOOK_SUB_SYS, AUDIT_MQTT_SUB_SYS] {
|
for spec in audit_target_specs() {
|
||||||
let Some(targets) = config.0.get(subsystem) else {
|
let Some(targets) = config.0.get(spec.subsystem) else {
|
||||||
continue;
|
continue;
|
||||||
};
|
};
|
||||||
if targets.keys().any(|key| key != DEFAULT_DELIMITER) {
|
if targets.keys().any(|key| key != DEFAULT_DELIMITER) {
|
||||||
@@ -183,73 +162,15 @@ fn has_any_audit_targets(config: &Config) -> bool {
|
|||||||
}
|
}
|
||||||
|
|
||||||
fn collect_configured_audit_endpoint_keys(config: &Config) -> Vec<EndpointKey> {
|
fn collect_configured_audit_endpoint_keys(config: &Config) -> Vec<EndpointKey> {
|
||||||
let mut endpoints = Vec::new();
|
shared_collect_configured_endpoint_keys(&audit_target_specs(), config)
|
||||||
for (subsystem, service) in [(AUDIT_WEBHOOK_SUB_SYS, "webhook"), (AUDIT_MQTT_SUB_SYS, "mqtt")] {
|
|
||||||
let Some(targets) = config.0.get(subsystem) else {
|
|
||||||
continue;
|
|
||||||
};
|
|
||||||
|
|
||||||
for (target_name, kvs) in targets {
|
|
||||||
if target_name == DEFAULT_DELIMITER {
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
let enabled = kvs.lookup(ENABLE_KEY).as_deref().map(config_enable_is_on).unwrap_or(false);
|
|
||||||
if enabled {
|
|
||||||
endpoints.push((target_name.clone(), service.to_string()));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
endpoints
|
|
||||||
}
|
}
|
||||||
|
|
||||||
fn collect_config_entry_keys(config: &Config) -> HbHashSet<EndpointKey> {
|
fn collect_config_entry_keys(config: &Config) -> HbHashSet<EndpointKey> {
|
||||||
let mut endpoints = HbHashSet::new();
|
shared_collect_config_entry_keys(&audit_target_specs(), config)
|
||||||
for (subsystem, service) in [(AUDIT_WEBHOOK_SUB_SYS, "webhook"), (AUDIT_MQTT_SUB_SYS, "mqtt")] {
|
|
||||||
let Some(targets) = config.0.get(subsystem) else {
|
|
||||||
continue;
|
|
||||||
};
|
|
||||||
|
|
||||||
for target_name in targets.keys() {
|
|
||||||
if target_name == DEFAULT_DELIMITER {
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
endpoints.insert(normalized_endpoint_key(target_name, service));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
endpoints
|
|
||||||
}
|
}
|
||||||
|
|
||||||
fn collect_env_endpoint_keys() -> HbHashSet<EndpointKey> {
|
fn collect_env_endpoint_keys() -> HbHashSet<EndpointKey> {
|
||||||
let mut endpoints = HbHashSet::new();
|
shared_collect_env_endpoint_keys(&audit_target_specs(), AUDIT_ROUTE_PREFIX)
|
||||||
|
|
||||||
for (service, valid_keys) in [("webhook", AUDIT_WEBHOOK_KEYS), ("mqtt", AUDIT_MQTT_KEYS)] {
|
|
||||||
let env_prefix = format!("{ENV_PREFIX}{AUDIT_ROUTE_PREFIX}{service}{DEFAULT_DELIMITER}").to_uppercase();
|
|
||||||
|
|
||||||
for (key, _value) in std::env::vars() {
|
|
||||||
let Some(rest) = key.strip_prefix(&env_prefix) else {
|
|
||||||
continue;
|
|
||||||
};
|
|
||||||
|
|
||||||
let mut parts = rest.rsplitn(2, DEFAULT_DELIMITER);
|
|
||||||
let instance_id_part = parts.next().unwrap_or(DEFAULT_DELIMITER);
|
|
||||||
let field_name_part = parts.next();
|
|
||||||
|
|
||||||
let (field_name, instance_id) = match field_name_part {
|
|
||||||
Some(field) => (field.to_lowercase(), instance_id_part.to_lowercase()),
|
|
||||||
None => (instance_id_part.to_lowercase(), DEFAULT_DELIMITER.to_string()),
|
|
||||||
};
|
|
||||||
|
|
||||||
if instance_id == DEFAULT_DELIMITER || instance_id.is_empty() {
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
if valid_keys.contains(&field_name.as_str()) {
|
|
||||||
endpoints.insert(normalized_endpoint_key(&instance_id, service));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
endpoints
|
|
||||||
}
|
}
|
||||||
|
|
||||||
fn classify_audit_endpoint_source(
|
fn classify_audit_endpoint_source(
|
||||||
@@ -268,11 +189,7 @@ fn classify_audit_endpoint_source(
|
|||||||
fn audit_endpoint_source(config: &Config, target_type: &str, target_name: &str) -> AuditEndpointSource {
|
fn audit_endpoint_source(config: &Config, target_type: &str, target_name: &str) -> AuditEndpointSource {
|
||||||
let config_targets = collect_config_entry_keys(config);
|
let config_targets = collect_config_entry_keys(config);
|
||||||
let env_targets = collect_env_endpoint_keys();
|
let env_targets = collect_env_endpoint_keys();
|
||||||
let service = match target_type {
|
let service = target_service_name(&audit_target_specs(), target_type).unwrap_or_default();
|
||||||
AUDIT_WEBHOOK_SUB_SYS => "webhook",
|
|
||||||
AUDIT_MQTT_SUB_SYS => "mqtt",
|
|
||||||
_ => "",
|
|
||||||
};
|
|
||||||
|
|
||||||
let key = normalized_endpoint_key(target_name, service);
|
let key = normalized_endpoint_key(target_name, service);
|
||||||
classify_audit_endpoint_source(&config_targets, &env_targets, &key)
|
classify_audit_endpoint_source(&config_targets, &env_targets, &key)
|
||||||
@@ -384,7 +301,7 @@ fn extract_target_params<'a>(params: &'a Params<'_, '_>) -> S3Result<(&'a str, &
|
|||||||
let target_type = params
|
let target_type = params
|
||||||
.get("target_type")
|
.get("target_type")
|
||||||
.ok_or_else(|| s3_error!(InvalidArgument, "missing required parameter: 'target_type'"))?;
|
.ok_or_else(|| s3_error!(InvalidArgument, "missing required parameter: 'target_type'"))?;
|
||||||
if target_type != AUDIT_WEBHOOK_SUB_SYS && target_type != AUDIT_MQTT_SUB_SYS {
|
if target_service_name(&audit_target_specs(), target_type).is_none() {
|
||||||
return Err(s3_error!(InvalidArgument, "unsupported audit target type: '{}'", target_type));
|
return Err(s3_error!(InvalidArgument, "unsupported audit target type: '{}'", target_type));
|
||||||
}
|
}
|
||||||
let target_name = params
|
let target_name = params
|
||||||
@@ -486,77 +403,16 @@ impl Operation for AuditTargetConfig {
|
|||||||
let audit_body: AuditTargetBody = serde_json::from_slice(&body_bytes)
|
let audit_body: AuditTargetBody = serde_json::from_slice(&body_bytes)
|
||||||
.map_err(|e| s3_error!(InvalidArgument, "invalid json body for audit target config: {}", e))?;
|
.map_err(|e| s3_error!(InvalidArgument, "invalid json body for audit target config: {}", e))?;
|
||||||
|
|
||||||
let allowed_keys: HashSet<&str> = match target_type {
|
let specs = audit_target_specs();
|
||||||
AUDIT_WEBHOOK_SUB_SYS => AUDIT_WEBHOOK_KEYS.iter().cloned().collect(),
|
let allowed_keys: HashSet<&str> = allowed_target_keys(&specs, target_type);
|
||||||
AUDIT_MQTT_SUB_SYS => AUDIT_MQTT_KEYS.iter().cloned().collect(),
|
|
||||||
_ => unreachable!(),
|
|
||||||
};
|
|
||||||
|
|
||||||
let kv_map = collect_validated_key_values(&audit_body.key_values, &allowed_keys, target_type)?;
|
let kv_map = collect_validated_key_values(&audit_body.key_values, &allowed_keys, target_type)?;
|
||||||
|
|
||||||
if target_type == AUDIT_WEBHOOK_SUB_SYS {
|
let spec = target_spec(&specs, target_type)
|
||||||
let endpoint = kv_map
|
.ok_or_else(|| s3_error!(InvalidArgument, "unsupported audit target type: '{}'", target_type))?;
|
||||||
.get("endpoint")
|
timeout(Duration::from_secs(10), validate_target_request(spec, &kv_map, AUDIT_DEFAULT_DIR))
|
||||||
.map(String::as_str)
|
.await
|
||||||
.ok_or_else(|| s3_error!(InvalidArgument, "endpoint is required"))?;
|
.map_err(|_| s3_error!(InvalidArgument, "audit target validation timed out"))??;
|
||||||
let parsed_endpoint = Url::parse(endpoint).map_err(|e| s3_error!(InvalidArgument, "invalid endpoint url: {}", e))?;
|
|
||||||
match parsed_endpoint.scheme() {
|
|
||||||
"http" | "https" => {}
|
|
||||||
other => {
|
|
||||||
return Err(s3_error!(
|
|
||||||
InvalidArgument,
|
|
||||||
"unsupported endpoint scheme: {} (only http and https are allowed)",
|
|
||||||
other
|
|
||||||
));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if let Some(queue_dir) = kv_map.get("queue_dir") {
|
|
||||||
validate_queue_dir(queue_dir.as_str()).await?;
|
|
||||||
}
|
|
||||||
if kv_map.contains_key("client_cert") != kv_map.contains_key("client_key") {
|
|
||||||
return Err(s3_error!(InvalidArgument, "client_cert and client_key must be specified as a pair"));
|
|
||||||
}
|
|
||||||
} else if target_type == AUDIT_MQTT_SUB_SYS {
|
|
||||||
let endpoint = kv_map
|
|
||||||
.get(rustfs_config::MQTT_BROKER)
|
|
||||||
.map(String::as_str)
|
|
||||||
.ok_or_else(|| s3_error!(InvalidArgument, "broker endpoint is required"))?;
|
|
||||||
let topic = kv_map
|
|
||||||
.get(rustfs_config::MQTT_TOPIC)
|
|
||||||
.map(String::as_str)
|
|
||||||
.ok_or_else(|| s3_error!(InvalidArgument, "topic is required"))?;
|
|
||||||
let username = kv_map.get(rustfs_config::MQTT_USERNAME).map(String::as_str);
|
|
||||||
let password = kv_map.get(rustfs_config::MQTT_PASSWORD).map(String::as_str);
|
|
||||||
let tls = MQTTTlsConfig::from_values(
|
|
||||||
kv_map.get(rustfs_config::MQTT_TLS_POLICY).map(String::as_str),
|
|
||||||
kv_map.get(rustfs_config::MQTT_TLS_CA).map(String::as_str),
|
|
||||||
kv_map.get(rustfs_config::MQTT_TLS_CLIENT_CERT).map(String::as_str),
|
|
||||||
kv_map.get(rustfs_config::MQTT_TLS_CLIENT_KEY).map(String::as_str),
|
|
||||||
kv_map.get(rustfs_config::MQTT_TLS_TRUST_LEAF_AS_CA).map(String::as_str),
|
|
||||||
kv_map.get(rustfs_config::MQTT_WS_PATH_ALLOWLIST).map(String::as_str),
|
|
||||||
)
|
|
||||||
.map_err(|e| s3_error!(InvalidArgument, "invalid MQTT TLS settings: {}", e))?;
|
|
||||||
let parsed_broker = Url::parse(endpoint).map_err(|e| s3_error!(InvalidArgument, "invalid broker URL: {}", e))?;
|
|
||||||
rustfs_targets::target::mqtt::validate_mqtt_broker_url(&parsed_broker, &tls)
|
|
||||||
.map_err(|e| s3_error!(InvalidArgument, "{}", e))?;
|
|
||||||
check_mqtt_broker_available_with_tls(parsed_broker.as_str(), topic, username, password, &tls)
|
|
||||||
.await
|
|
||||||
.map_err(|e| match e {
|
|
||||||
TargetError::Configuration(_) => s3_error!(InvalidArgument, "{}", e),
|
|
||||||
_ => s3_error!(InvalidArgument, "MQTT broker check failed: {}", e),
|
|
||||||
})?;
|
|
||||||
|
|
||||||
if let Some(queue_dir) = kv_map.get("queue_dir") {
|
|
||||||
validate_queue_dir(queue_dir.as_str()).await?;
|
|
||||||
if let Some(qos) = kv_map.get("qos") {
|
|
||||||
match qos.parse::<u8>() {
|
|
||||||
Ok(1) | Ok(2) => {}
|
|
||||||
Ok(0) => return Err(s3_error!(InvalidArgument, "qos should be 1 or 2 if queue_dir is set")),
|
|
||||||
_ => return Err(s3_error!(InvalidArgument, "qos must be an integer 0, 1, or 2")),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
let mut kvs = rustfs_ecstore::config::KVS::new();
|
let mut kvs = rustfs_ecstore::config::KVS::new();
|
||||||
for (key, value) in kv_map {
|
for (key, value) in kv_map {
|
||||||
@@ -656,6 +512,7 @@ impl Operation for RemoveAuditTarget {
|
|||||||
mod tests {
|
mod tests {
|
||||||
use super::*;
|
use super::*;
|
||||||
use matchit::Router;
|
use matchit::Router;
|
||||||
|
use rustfs_config::ENV_PREFIX;
|
||||||
use rustfs_ecstore::config::{KV, KVS};
|
use rustfs_ecstore::config::{KV, KVS};
|
||||||
use std::collections::{HashMap, HashSet};
|
use std::collections::{HashMap, HashSet};
|
||||||
use temp_env::{with_var, with_vars, with_vars_unset};
|
use temp_env::{with_var, with_vars, with_vars_unset};
|
||||||
|
|||||||
@@ -14,6 +14,13 @@
|
|||||||
|
|
||||||
use crate::admin::{
|
use crate::admin::{
|
||||||
auth::validate_admin_request,
|
auth::validate_admin_request,
|
||||||
|
handlers::target_descriptor::{
|
||||||
|
AdminTargetSpec, AdminTargetValidator, EndpointKey, TargetDomain, allowed_target_keys,
|
||||||
|
collect_config_entry_keys as shared_collect_config_entry_keys,
|
||||||
|
collect_configured_endpoint_keys as shared_collect_configured_endpoint_keys,
|
||||||
|
collect_env_endpoint_keys as shared_collect_env_endpoint_keys, normalized_endpoint_key, target_service_name, target_spec,
|
||||||
|
validate_target_request,
|
||||||
|
},
|
||||||
router::{AdminOperation, Operation, S3Router},
|
router::{AdminOperation, Operation, S3Router},
|
||||||
};
|
};
|
||||||
use crate::auth::{check_key_valid, get_session_token};
|
use crate::auth::{check_key_valid, get_session_token};
|
||||||
@@ -24,23 +31,19 @@ use http::{HeaderMap, StatusCode};
|
|||||||
use hyper::Method;
|
use hyper::Method;
|
||||||
use matchit::Params;
|
use matchit::Params;
|
||||||
use rustfs_config::notify::{
|
use rustfs_config::notify::{
|
||||||
NOTIFY_MQTT_KEYS, NOTIFY_MQTT_SUB_SYS, NOTIFY_ROUTE_PREFIX, NOTIFY_WEBHOOK_KEYS, NOTIFY_WEBHOOK_SUB_SYS,
|
NOTIFY_MQTT_KEYS, NOTIFY_MQTT_SUB_SYS, NOTIFY_NATS_KEYS, NOTIFY_NATS_SUB_SYS, NOTIFY_PULSAR_KEYS, NOTIFY_PULSAR_SUB_SYS,
|
||||||
|
NOTIFY_ROUTE_PREFIX, NOTIFY_WEBHOOK_KEYS, NOTIFY_WEBHOOK_SUB_SYS,
|
||||||
};
|
};
|
||||||
use rustfs_config::{DEFAULT_DELIMITER, ENABLE_KEY, ENV_PREFIX, EnableState, MAX_ADMIN_REQUEST_BODY_SIZE};
|
use rustfs_config::{ENABLE_KEY, EVENT_DEFAULT_DIR, EnableState, MAX_ADMIN_REQUEST_BODY_SIZE};
|
||||||
use rustfs_ecstore::config::Config;
|
use rustfs_ecstore::config::Config;
|
||||||
use rustfs_policy::policy::action::{Action, AdminAction};
|
use rustfs_policy::policy::action::{Action, AdminAction};
|
||||||
use rustfs_targets::{TargetError, check_mqtt_broker_available_with_tls, target::mqtt::MQTTTlsConfig};
|
|
||||||
use s3s::{Body, S3Request, S3Response, S3Result, header::CONTENT_TYPE, s3_error};
|
use s3s::{Body, S3Request, S3Response, S3Result, header::CONTENT_TYPE, s3_error};
|
||||||
use serde::{Deserialize, Serialize};
|
use serde::{Deserialize, Serialize};
|
||||||
use std::collections::{HashMap, HashSet};
|
use std::collections::{HashMap, HashSet};
|
||||||
use std::future::Future;
|
|
||||||
use std::io::{Error, ErrorKind};
|
|
||||||
use std::path::Path;
|
|
||||||
use std::sync::Arc;
|
use std::sync::Arc;
|
||||||
use tokio::sync::Semaphore;
|
use tokio::sync::Semaphore;
|
||||||
use tokio::time::{Duration, sleep, timeout};
|
use tokio::time::{Duration, timeout};
|
||||||
use tracing::{Span, info, warn};
|
use tracing::{Span, info, warn};
|
||||||
use url::Url;
|
|
||||||
|
|
||||||
pub fn register_notification_target_route(r: &mut S3Router<AdminOperation>) -> std::io::Result<()> {
|
pub fn register_notification_target_route(r: &mut S3Router<AdminOperation>) -> std::io::Result<()> {
|
||||||
r.insert(
|
r.insert(
|
||||||
@@ -94,8 +97,6 @@ struct NotificationEndpointsResponse {
|
|||||||
notification_endpoints: Vec<NotificationEndpoint>,
|
notification_endpoints: Vec<NotificationEndpoint>,
|
||||||
}
|
}
|
||||||
|
|
||||||
type EndpointKey = (String, String);
|
|
||||||
|
|
||||||
#[derive(Clone, Copy, Debug, PartialEq, Eq, Serialize)]
|
#[derive(Clone, Copy, Debug, PartialEq, Eq, Serialize)]
|
||||||
#[serde(rename_all = "lowercase")]
|
#[serde(rename_all = "lowercase")]
|
||||||
enum NotificationEndpointSource {
|
enum NotificationEndpointSource {
|
||||||
@@ -105,8 +106,33 @@ enum NotificationEndpointSource {
|
|||||||
Runtime,
|
Runtime,
|
||||||
}
|
}
|
||||||
|
|
||||||
fn normalized_endpoint_key(account_id: &str, service: &str) -> EndpointKey {
|
fn notification_target_specs() -> [AdminTargetSpec; 4] {
|
||||||
(account_id.to_lowercase(), service.to_string())
|
[
|
||||||
|
AdminTargetSpec {
|
||||||
|
subsystem: NOTIFY_WEBHOOK_SUB_SYS,
|
||||||
|
service: "webhook",
|
||||||
|
valid_keys: NOTIFY_WEBHOOK_KEYS,
|
||||||
|
validator: AdminTargetValidator::Webhook,
|
||||||
|
},
|
||||||
|
AdminTargetSpec {
|
||||||
|
subsystem: NOTIFY_MQTT_SUB_SYS,
|
||||||
|
service: "mqtt",
|
||||||
|
valid_keys: NOTIFY_MQTT_KEYS,
|
||||||
|
validator: AdminTargetValidator::Mqtt,
|
||||||
|
},
|
||||||
|
AdminTargetSpec {
|
||||||
|
subsystem: NOTIFY_NATS_SUB_SYS,
|
||||||
|
service: "nats",
|
||||||
|
valid_keys: NOTIFY_NATS_KEYS,
|
||||||
|
validator: AdminTargetValidator::Nats(TargetDomain::Notify),
|
||||||
|
},
|
||||||
|
AdminTargetSpec {
|
||||||
|
subsystem: NOTIFY_PULSAR_SUB_SYS,
|
||||||
|
service: "pulsar",
|
||||||
|
valid_keys: NOTIFY_PULSAR_KEYS,
|
||||||
|
validator: AdminTargetValidator::Pulsar(TargetDomain::Notify),
|
||||||
|
},
|
||||||
|
]
|
||||||
}
|
}
|
||||||
|
|
||||||
// --- Helper Functions ---
|
// --- Helper Functions ---
|
||||||
@@ -134,123 +160,16 @@ fn build_response(status: StatusCode, body: Body, request_id: Option<&http::Head
|
|||||||
S3Response::with_headers((status, body), header)
|
S3Response::with_headers((status, body), header)
|
||||||
}
|
}
|
||||||
|
|
||||||
async fn retry_with_backoff<F, Fut, T>(mut operation: F, max_attempts: usize, base_delay: Duration) -> Result<T, Error>
|
|
||||||
where
|
|
||||||
F: FnMut() -> Fut,
|
|
||||||
Fut: Future<Output = Result<T, Error>>,
|
|
||||||
{
|
|
||||||
let mut attempts = 0;
|
|
||||||
let mut delay = base_delay;
|
|
||||||
let mut last_err = None;
|
|
||||||
|
|
||||||
while attempts < max_attempts {
|
|
||||||
match operation().await {
|
|
||||||
Ok(result) => return Ok(result),
|
|
||||||
Err(e) => {
|
|
||||||
last_err = Some(e);
|
|
||||||
attempts += 1;
|
|
||||||
if attempts < max_attempts {
|
|
||||||
sleep(delay).await;
|
|
||||||
delay = delay.saturating_mul(2);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
Err(last_err.unwrap_or_else(|| Error::other("retry_with_backoff: unknown error")))
|
|
||||||
}
|
|
||||||
|
|
||||||
async fn validate_queue_dir(queue_dir: &str) -> S3Result<()> {
|
|
||||||
if !queue_dir.is_empty() {
|
|
||||||
if !Path::new(queue_dir).is_absolute() {
|
|
||||||
return Err(s3_error!(InvalidArgument, "queue_dir must be absolute path"));
|
|
||||||
}
|
|
||||||
retry_with_backoff(
|
|
||||||
|| async { tokio::fs::metadata(queue_dir).await.map(|_| ()) },
|
|
||||||
3,
|
|
||||||
Duration::from_millis(100),
|
|
||||||
)
|
|
||||||
.await
|
|
||||||
.map_err(|e| match e.kind() {
|
|
||||||
ErrorKind::NotFound => s3_error!(InvalidArgument, "queue_dir does not exist"),
|
|
||||||
ErrorKind::PermissionDenied => s3_error!(InvalidArgument, "queue_dir exists but permission denied"),
|
|
||||||
_ => s3_error!(InvalidArgument, "failed to access queue_dir: {}", e),
|
|
||||||
})?;
|
|
||||||
}
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
|
|
||||||
fn config_enable_is_on(value: &str) -> bool {
|
|
||||||
matches!(value.trim().to_ascii_lowercase().as_str(), "on" | "true" | "yes" | "1")
|
|
||||||
}
|
|
||||||
|
|
||||||
fn collect_configured_endpoint_keys(config: &Config) -> Vec<EndpointKey> {
|
fn collect_configured_endpoint_keys(config: &Config) -> Vec<EndpointKey> {
|
||||||
let mut endpoints = Vec::new();
|
shared_collect_configured_endpoint_keys(¬ification_target_specs(), config)
|
||||||
for (subsystem, service) in [(NOTIFY_WEBHOOK_SUB_SYS, "webhook"), (NOTIFY_MQTT_SUB_SYS, "mqtt")] {
|
|
||||||
let Some(targets) = config.0.get(subsystem) else {
|
|
||||||
continue;
|
|
||||||
};
|
|
||||||
|
|
||||||
for (target_name, kvs) in targets {
|
|
||||||
if target_name == DEFAULT_DELIMITER {
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
let enabled = kvs.lookup(ENABLE_KEY).as_deref().map(config_enable_is_on).unwrap_or(false);
|
|
||||||
if enabled {
|
|
||||||
endpoints.push((target_name.clone(), service.to_string()));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
endpoints
|
|
||||||
}
|
}
|
||||||
|
|
||||||
fn collect_config_entry_keys(config: &Config) -> HbHashSet<EndpointKey> {
|
fn collect_config_entry_keys(config: &Config) -> HbHashSet<EndpointKey> {
|
||||||
let mut endpoints = HbHashSet::new();
|
shared_collect_config_entry_keys(¬ification_target_specs(), config)
|
||||||
for (subsystem, service) in [(NOTIFY_WEBHOOK_SUB_SYS, "webhook"), (NOTIFY_MQTT_SUB_SYS, "mqtt")] {
|
|
||||||
let Some(targets) = config.0.get(subsystem) else {
|
|
||||||
continue;
|
|
||||||
};
|
|
||||||
|
|
||||||
for target_name in targets.keys() {
|
|
||||||
if target_name == DEFAULT_DELIMITER {
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
endpoints.insert(normalized_endpoint_key(target_name, service));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
endpoints
|
|
||||||
}
|
}
|
||||||
|
|
||||||
fn collect_env_endpoint_keys() -> HbHashSet<EndpointKey> {
|
fn collect_env_endpoint_keys() -> HbHashSet<EndpointKey> {
|
||||||
let mut endpoints = HbHashSet::new();
|
shared_collect_env_endpoint_keys(¬ification_target_specs(), NOTIFY_ROUTE_PREFIX)
|
||||||
|
|
||||||
for (service, valid_keys) in [("webhook", NOTIFY_WEBHOOK_KEYS), ("mqtt", NOTIFY_MQTT_KEYS)] {
|
|
||||||
let env_prefix = format!("{ENV_PREFIX}{NOTIFY_ROUTE_PREFIX}{service}{DEFAULT_DELIMITER}").to_uppercase();
|
|
||||||
|
|
||||||
for (key, _value) in std::env::vars() {
|
|
||||||
let Some(rest) = key.strip_prefix(&env_prefix) else {
|
|
||||||
continue;
|
|
||||||
};
|
|
||||||
|
|
||||||
let mut parts = rest.rsplitn(2, DEFAULT_DELIMITER);
|
|
||||||
let instance_id_part = parts.next().unwrap_or(DEFAULT_DELIMITER);
|
|
||||||
let field_name_part = parts.next();
|
|
||||||
|
|
||||||
let (field_name, instance_id) = match field_name_part {
|
|
||||||
Some(field) => (field.to_lowercase(), instance_id_part.to_lowercase()),
|
|
||||||
None => (instance_id_part.to_lowercase(), DEFAULT_DELIMITER.to_string()),
|
|
||||||
};
|
|
||||||
|
|
||||||
if instance_id == DEFAULT_DELIMITER || instance_id.is_empty() {
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
if valid_keys.contains(&field_name.as_str()) {
|
|
||||||
endpoints.insert(normalized_endpoint_key(&instance_id, service));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
endpoints
|
|
||||||
}
|
}
|
||||||
|
|
||||||
fn classify_notification_endpoint_source(
|
fn classify_notification_endpoint_source(
|
||||||
@@ -269,11 +188,7 @@ fn classify_notification_endpoint_source(
|
|||||||
fn notification_endpoint_source(config: &Config, target_type: &str, target_name: &str) -> NotificationEndpointSource {
|
fn notification_endpoint_source(config: &Config, target_type: &str, target_name: &str) -> NotificationEndpointSource {
|
||||||
let config_targets = collect_config_entry_keys(config);
|
let config_targets = collect_config_entry_keys(config);
|
||||||
let env_targets = collect_env_endpoint_keys();
|
let env_targets = collect_env_endpoint_keys();
|
||||||
let service = match target_type {
|
let service = target_service_name(¬ification_target_specs(), target_type).unwrap_or_default();
|
||||||
NOTIFY_WEBHOOK_SUB_SYS => "webhook",
|
|
||||||
NOTIFY_MQTT_SUB_SYS => "mqtt",
|
|
||||||
_ => "",
|
|
||||||
};
|
|
||||||
|
|
||||||
let key = normalized_endpoint_key(target_name, service);
|
let key = normalized_endpoint_key(target_name, service);
|
||||||
classify_notification_endpoint_source(&config_targets, &env_targets, &key)
|
classify_notification_endpoint_source(&config_targets, &env_targets, &key)
|
||||||
@@ -414,78 +329,15 @@ impl Operation for NotificationTarget {
|
|||||||
let notification_body: NotificationTargetBody = serde_json::from_slice(&body_bytes)
|
let notification_body: NotificationTargetBody = serde_json::from_slice(&body_bytes)
|
||||||
.map_err(|e| s3_error!(InvalidArgument, "invalid json body for target config: {}", e))?;
|
.map_err(|e| s3_error!(InvalidArgument, "invalid json body for target config: {}", e))?;
|
||||||
|
|
||||||
let allowed_keys: HashSet<&str> = match target_type {
|
let specs = notification_target_specs();
|
||||||
NOTIFY_WEBHOOK_SUB_SYS => rustfs_config::notify::NOTIFY_WEBHOOK_KEYS.iter().cloned().collect(),
|
let allowed_keys: HashSet<&str> = allowed_target_keys(&specs, target_type);
|
||||||
NOTIFY_MQTT_SUB_SYS => rustfs_config::notify::NOTIFY_MQTT_KEYS.iter().cloned().collect(),
|
|
||||||
_ => unreachable!(),
|
|
||||||
};
|
|
||||||
|
|
||||||
let kv_map = collect_validated_key_values(¬ification_body.key_values, &allowed_keys, target_type)?;
|
let kv_map = collect_validated_key_values(¬ification_body.key_values, &allowed_keys, target_type)?;
|
||||||
|
let spec = target_spec(&specs, target_type)
|
||||||
// Type-specific validation
|
.ok_or_else(|| s3_error!(InvalidArgument, "unsupported target type: '{}'", target_type))?;
|
||||||
if target_type == NOTIFY_WEBHOOK_SUB_SYS {
|
timeout(Duration::from_secs(10), validate_target_request(spec, &kv_map, EVENT_DEFAULT_DIR))
|
||||||
let endpoint = kv_map
|
.await
|
||||||
.get("endpoint")
|
.map_err(|_| s3_error!(InvalidArgument, "target validation timed out"))??;
|
||||||
.map(String::as_str)
|
|
||||||
.ok_or_else(|| s3_error!(InvalidArgument, "endpoint is required"))?;
|
|
||||||
let parsed_endpoint = Url::parse(endpoint).map_err(|e| s3_error!(InvalidArgument, "invalid endpoint url: {}", e))?;
|
|
||||||
match parsed_endpoint.scheme() {
|
|
||||||
"http" | "https" => {}
|
|
||||||
other => {
|
|
||||||
return Err(s3_error!(
|
|
||||||
InvalidArgument,
|
|
||||||
"unsupported endpoint scheme: {} (only http and https are allowed)",
|
|
||||||
other
|
|
||||||
));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if let Some(queue_dir) = kv_map.get("queue_dir") {
|
|
||||||
validate_queue_dir(queue_dir.as_str()).await?;
|
|
||||||
}
|
|
||||||
if kv_map.contains_key("client_cert") != kv_map.contains_key("client_key") {
|
|
||||||
return Err(s3_error!(InvalidArgument, "client_cert and client_key must be specified as a pair"));
|
|
||||||
}
|
|
||||||
} else if target_type == NOTIFY_MQTT_SUB_SYS {
|
|
||||||
let endpoint = kv_map
|
|
||||||
.get(rustfs_config::MQTT_BROKER)
|
|
||||||
.map(String::as_str)
|
|
||||||
.ok_or_else(|| s3_error!(InvalidArgument, "broker endpoint is required"))?;
|
|
||||||
let topic = kv_map
|
|
||||||
.get(rustfs_config::MQTT_TOPIC)
|
|
||||||
.map(String::as_str)
|
|
||||||
.ok_or_else(|| s3_error!(InvalidArgument, "topic is required"))?;
|
|
||||||
let username = kv_map.get(rustfs_config::MQTT_USERNAME).map(String::as_str);
|
|
||||||
let password = kv_map.get(rustfs_config::MQTT_PASSWORD).map(String::as_str);
|
|
||||||
let tls = MQTTTlsConfig::from_values(
|
|
||||||
kv_map.get(rustfs_config::MQTT_TLS_POLICY).map(String::as_str),
|
|
||||||
kv_map.get(rustfs_config::MQTT_TLS_CA).map(String::as_str),
|
|
||||||
kv_map.get(rustfs_config::MQTT_TLS_CLIENT_CERT).map(String::as_str),
|
|
||||||
kv_map.get(rustfs_config::MQTT_TLS_CLIENT_KEY).map(String::as_str),
|
|
||||||
kv_map.get(rustfs_config::MQTT_TLS_TRUST_LEAF_AS_CA).map(String::as_str),
|
|
||||||
kv_map.get(rustfs_config::MQTT_WS_PATH_ALLOWLIST).map(String::as_str),
|
|
||||||
)
|
|
||||||
.map_err(|e| s3_error!(InvalidArgument, "invalid MQTT TLS settings: {}", e))?;
|
|
||||||
let parsed_broker = Url::parse(endpoint).map_err(|e| s3_error!(InvalidArgument, "invalid broker URL: {}", e))?;
|
|
||||||
rustfs_targets::target::mqtt::validate_mqtt_broker_url(&parsed_broker, &tls)
|
|
||||||
.map_err(|e| s3_error!(InvalidArgument, "{}", e))?;
|
|
||||||
check_mqtt_broker_available_with_tls(parsed_broker.as_str(), topic, username, password, &tls)
|
|
||||||
.await
|
|
||||||
.map_err(|e| match e {
|
|
||||||
TargetError::Configuration(_) => s3_error!(InvalidArgument, "{}", e),
|
|
||||||
_ => s3_error!(InvalidArgument, "MQTT broker check failed: {}", e),
|
|
||||||
})?;
|
|
||||||
|
|
||||||
if let Some(queue_dir) = kv_map.get("queue_dir") {
|
|
||||||
validate_queue_dir(queue_dir.as_str()).await?;
|
|
||||||
if let Some(qos) = kv_map.get("qos") {
|
|
||||||
match qos.parse::<u8>() {
|
|
||||||
Ok(1) | Ok(2) => {}
|
|
||||||
Ok(0) => return Err(s3_error!(InvalidArgument, "qos should be 1 or 2 if queue_dir is set")),
|
|
||||||
_ => return Err(s3_error!(InvalidArgument, "qos must be an integer 0, 1, or 2")),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
let mut kvs = rustfs_ecstore::config::KVS::new();
|
let mut kvs = rustfs_ecstore::config::KVS::new();
|
||||||
for (key, value) in kv_map {
|
for (key, value) in kv_map {
|
||||||
@@ -616,7 +468,7 @@ fn extract_param<'a>(params: &'a Params<'_, '_>, key: &str) -> S3Result<&'a str>
|
|||||||
|
|
||||||
fn extract_target_params<'a>(params: &'a Params<'_, '_>) -> S3Result<(&'a str, &'a str)> {
|
fn extract_target_params<'a>(params: &'a Params<'_, '_>) -> S3Result<(&'a str, &'a str)> {
|
||||||
let target_type = extract_param(params, "target_type")?;
|
let target_type = extract_param(params, "target_type")?;
|
||||||
if target_type != NOTIFY_WEBHOOK_SUB_SYS && target_type != NOTIFY_MQTT_SUB_SYS {
|
if target_service_name(¬ification_target_specs(), target_type).is_none() {
|
||||||
return Err(s3_error!(InvalidArgument, "unsupported target type: '{}'", target_type));
|
return Err(s3_error!(InvalidArgument, "unsupported target type: '{}'", target_type));
|
||||||
}
|
}
|
||||||
let target_name = extract_param(params, "target_name")?;
|
let target_name = extract_param(params, "target_name")?;
|
||||||
@@ -626,6 +478,7 @@ fn extract_target_params<'a>(params: &'a Params<'_, '_>) -> S3Result<(&'a str, &
|
|||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
mod tests {
|
mod tests {
|
||||||
use super::*;
|
use super::*;
|
||||||
|
use rustfs_config::DEFAULT_DELIMITER;
|
||||||
use rustfs_ecstore::config::{KV, KVS};
|
use rustfs_ecstore::config::{KV, KVS};
|
||||||
use rustfs_targets::arn::TargetID;
|
use rustfs_targets::arn::TargetID;
|
||||||
use std::collections::{HashMap, HashSet};
|
use std::collections::{HashMap, HashSet};
|
||||||
|
|||||||
@@ -37,6 +37,7 @@ pub mod service_account;
|
|||||||
pub mod site_replication;
|
pub mod site_replication;
|
||||||
pub mod sts;
|
pub mod sts;
|
||||||
pub mod system;
|
pub mod system;
|
||||||
|
mod target_descriptor;
|
||||||
pub mod tier;
|
pub mod tier;
|
||||||
pub mod trace;
|
pub mod trace;
|
||||||
pub mod user;
|
pub mod user;
|
||||||
|
|||||||
@@ -0,0 +1,299 @@
|
|||||||
|
// 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.
|
||||||
|
|
||||||
|
use hashbrown::HashSet as HbHashSet;
|
||||||
|
use rustfs_config::{
|
||||||
|
ENABLE_KEY, MQTT_BROKER, MQTT_PASSWORD, MQTT_QOS, MQTT_TLS_CA, MQTT_TLS_CLIENT_CERT, MQTT_TLS_CLIENT_KEY, MQTT_TLS_POLICY,
|
||||||
|
MQTT_TLS_TRUST_LEAF_AS_CA, MQTT_TOPIC, MQTT_USERNAME, MQTT_WS_PATH_ALLOWLIST,
|
||||||
|
};
|
||||||
|
use rustfs_ecstore::config::Config;
|
||||||
|
use rustfs_targets::{
|
||||||
|
TargetError, check_mqtt_broker_available_with_tls, check_nats_server_available, check_pulsar_broker_available,
|
||||||
|
config::{build_nats_args, build_pulsar_args, collect_env_target_instance_ids},
|
||||||
|
target::{TargetType, mqtt::MQTTTlsConfig},
|
||||||
|
};
|
||||||
|
use s3s::{S3Result, s3_error};
|
||||||
|
use std::collections::{HashMap, HashSet};
|
||||||
|
use std::io::{Error, ErrorKind};
|
||||||
|
use std::path::Path;
|
||||||
|
use tokio::time::{Duration, sleep};
|
||||||
|
use url::Url;
|
||||||
|
|
||||||
|
pub(crate) type EndpointKey = (String, String);
|
||||||
|
|
||||||
|
#[derive(Clone, Copy)]
|
||||||
|
pub(crate) enum TargetDomain {
|
||||||
|
Notify,
|
||||||
|
Audit,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl TargetDomain {
|
||||||
|
fn runtime_target_type(self) -> TargetType {
|
||||||
|
match self {
|
||||||
|
TargetDomain::Notify => TargetType::NotifyEvent,
|
||||||
|
TargetDomain::Audit => TargetType::AuditLog,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Clone, Copy)]
|
||||||
|
pub(crate) enum AdminTargetValidator {
|
||||||
|
Webhook,
|
||||||
|
Mqtt,
|
||||||
|
Nats(TargetDomain),
|
||||||
|
Pulsar(TargetDomain),
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Clone, Copy)]
|
||||||
|
pub(crate) struct AdminTargetSpec {
|
||||||
|
pub subsystem: &'static str,
|
||||||
|
pub service: &'static str,
|
||||||
|
pub valid_keys: &'static [&'static str],
|
||||||
|
pub validator: AdminTargetValidator,
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(crate) fn normalized_endpoint_key(account_id: &str, service: &str) -> EndpointKey {
|
||||||
|
(account_id.to_lowercase(), service.to_string())
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(crate) fn target_spec<'a>(specs: &'a [AdminTargetSpec], target_type: &str) -> Option<&'a AdminTargetSpec> {
|
||||||
|
specs.iter().find(|spec| spec.subsystem == target_type)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(crate) fn target_service_name(specs: &[AdminTargetSpec], target_type: &str) -> Option<&'static str> {
|
||||||
|
target_spec(specs, target_type).map(|spec| spec.service)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(crate) fn collect_configured_endpoint_keys(specs: &[AdminTargetSpec], config: &Config) -> Vec<EndpointKey> {
|
||||||
|
let mut endpoints = Vec::new();
|
||||||
|
for spec in specs {
|
||||||
|
let Some(targets) = config.0.get(spec.subsystem) else {
|
||||||
|
continue;
|
||||||
|
};
|
||||||
|
|
||||||
|
for (target_name, kvs) in targets {
|
||||||
|
if target_name == rustfs_config::DEFAULT_DELIMITER {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
let enabled = kvs.lookup(ENABLE_KEY).as_deref().map(config_enable_is_on).unwrap_or(false);
|
||||||
|
if enabled {
|
||||||
|
endpoints.push((target_name.clone(), spec.service.to_string()));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
endpoints
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(crate) fn collect_config_entry_keys(specs: &[AdminTargetSpec], config: &Config) -> HbHashSet<EndpointKey> {
|
||||||
|
let mut endpoints = HbHashSet::new();
|
||||||
|
for spec in specs {
|
||||||
|
let Some(targets) = config.0.get(spec.subsystem) else {
|
||||||
|
continue;
|
||||||
|
};
|
||||||
|
|
||||||
|
for target_name in targets.keys() {
|
||||||
|
if target_name == rustfs_config::DEFAULT_DELIMITER {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
endpoints.insert(normalized_endpoint_key(target_name, spec.service));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
endpoints
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(crate) fn collect_env_endpoint_keys(specs: &[AdminTargetSpec], route_prefix: &str) -> HbHashSet<EndpointKey> {
|
||||||
|
let mut endpoints = HbHashSet::new();
|
||||||
|
for spec in specs {
|
||||||
|
let valid_keys = spec.valid_keys.iter().map(|key| (*key).to_string()).collect::<HashSet<_>>();
|
||||||
|
for instance_id in collect_env_target_instance_ids(route_prefix, spec.service, &valid_keys) {
|
||||||
|
if instance_id != rustfs_config::DEFAULT_DELIMITER && !instance_id.is_empty() {
|
||||||
|
endpoints.insert(normalized_endpoint_key(&instance_id, spec.service));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
endpoints
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(crate) fn allowed_target_keys(specs: &[AdminTargetSpec], target_type: &str) -> HashSet<&'static str> {
|
||||||
|
target_spec(specs, target_type)
|
||||||
|
.map(|spec| spec.valid_keys.iter().copied().collect())
|
||||||
|
.unwrap_or_default()
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(crate) async fn validate_queue_dir(queue_dir: &str) -> S3Result<()> {
|
||||||
|
if !queue_dir.is_empty() {
|
||||||
|
if !Path::new(queue_dir).is_absolute() {
|
||||||
|
return Err(s3_error!(InvalidArgument, "queue_dir must be absolute path"));
|
||||||
|
}
|
||||||
|
retry_with_backoff(
|
||||||
|
|| async { tokio::fs::metadata(queue_dir).await.map(|_| ()) },
|
||||||
|
3,
|
||||||
|
Duration::from_millis(100),
|
||||||
|
)
|
||||||
|
.await
|
||||||
|
.map_err(|e| match e.kind() {
|
||||||
|
ErrorKind::NotFound => s3_error!(InvalidArgument, "queue_dir does not exist"),
|
||||||
|
ErrorKind::PermissionDenied => s3_error!(InvalidArgument, "queue_dir exists but permission denied"),
|
||||||
|
_ => s3_error!(InvalidArgument, "failed to access queue_dir: {}", e),
|
||||||
|
})?;
|
||||||
|
}
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(crate) async fn validate_target_request(
|
||||||
|
spec: &AdminTargetSpec,
|
||||||
|
kv_map: &HashMap<String, String>,
|
||||||
|
default_queue_dir: &str,
|
||||||
|
) -> S3Result<()> {
|
||||||
|
match spec.validator {
|
||||||
|
AdminTargetValidator::Webhook => validate_webhook_request(kv_map).await,
|
||||||
|
AdminTargetValidator::Mqtt => validate_mqtt_request(kv_map).await,
|
||||||
|
AdminTargetValidator::Nats(domain) => validate_nats_request(kv_map, default_queue_dir, domain).await,
|
||||||
|
AdminTargetValidator::Pulsar(domain) => validate_pulsar_request(kv_map, default_queue_dir, domain).await,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn config_enable_is_on(value: &str) -> bool {
|
||||||
|
matches!(value.trim().to_ascii_lowercase().as_str(), "on" | "true" | "yes" | "1")
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn retry_with_backoff<F, Fut, T>(mut operation: F, max_attempts: usize, base_delay: Duration) -> Result<T, Error>
|
||||||
|
where
|
||||||
|
F: FnMut() -> Fut,
|
||||||
|
Fut: std::future::Future<Output = Result<T, Error>>,
|
||||||
|
{
|
||||||
|
let mut attempts = 0;
|
||||||
|
let mut delay = base_delay;
|
||||||
|
let mut last_err = None;
|
||||||
|
|
||||||
|
while attempts < max_attempts {
|
||||||
|
match operation().await {
|
||||||
|
Ok(result) => return Ok(result),
|
||||||
|
Err(e) => {
|
||||||
|
last_err = Some(e);
|
||||||
|
attempts += 1;
|
||||||
|
if attempts < max_attempts {
|
||||||
|
sleep(delay).await;
|
||||||
|
delay = delay.saturating_mul(2);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Err(last_err.unwrap_or_else(|| Error::other("retry_with_backoff: unknown error")))
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn validate_webhook_request(kv_map: &HashMap<String, String>) -> S3Result<()> {
|
||||||
|
let endpoint = kv_map
|
||||||
|
.get("endpoint")
|
||||||
|
.map(String::as_str)
|
||||||
|
.ok_or_else(|| s3_error!(InvalidArgument, "endpoint is required"))?;
|
||||||
|
let parsed_endpoint = Url::parse(endpoint).map_err(|e| s3_error!(InvalidArgument, "invalid endpoint url: {}", e))?;
|
||||||
|
match parsed_endpoint.scheme() {
|
||||||
|
"http" | "https" => {}
|
||||||
|
other => {
|
||||||
|
return Err(s3_error!(
|
||||||
|
InvalidArgument,
|
||||||
|
"unsupported endpoint scheme: {} (only http and https are allowed)",
|
||||||
|
other
|
||||||
|
));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if let Some(queue_dir) = kv_map.get("queue_dir") {
|
||||||
|
validate_queue_dir(queue_dir.as_str()).await?;
|
||||||
|
}
|
||||||
|
if kv_map.contains_key("client_cert") != kv_map.contains_key("client_key") {
|
||||||
|
return Err(s3_error!(InvalidArgument, "client_cert and client_key must be specified as a pair"));
|
||||||
|
}
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn validate_mqtt_request(kv_map: &HashMap<String, String>) -> S3Result<()> {
|
||||||
|
let endpoint = kv_map
|
||||||
|
.get(MQTT_BROKER)
|
||||||
|
.map(String::as_str)
|
||||||
|
.ok_or_else(|| s3_error!(InvalidArgument, "broker endpoint is required"))?;
|
||||||
|
let topic = kv_map
|
||||||
|
.get(MQTT_TOPIC)
|
||||||
|
.map(String::as_str)
|
||||||
|
.ok_or_else(|| s3_error!(InvalidArgument, "topic is required"))?;
|
||||||
|
let username = kv_map.get(MQTT_USERNAME).map(String::as_str);
|
||||||
|
let password = kv_map.get(MQTT_PASSWORD).map(String::as_str);
|
||||||
|
let tls = MQTTTlsConfig::from_values(
|
||||||
|
kv_map.get(MQTT_TLS_POLICY).map(String::as_str),
|
||||||
|
kv_map.get(MQTT_TLS_CA).map(String::as_str),
|
||||||
|
kv_map.get(MQTT_TLS_CLIENT_CERT).map(String::as_str),
|
||||||
|
kv_map.get(MQTT_TLS_CLIENT_KEY).map(String::as_str),
|
||||||
|
kv_map.get(MQTT_TLS_TRUST_LEAF_AS_CA).map(String::as_str),
|
||||||
|
kv_map.get(MQTT_WS_PATH_ALLOWLIST).map(String::as_str),
|
||||||
|
)
|
||||||
|
.map_err(|e| s3_error!(InvalidArgument, "invalid MQTT TLS settings: {}", e))?;
|
||||||
|
let parsed_broker = Url::parse(endpoint).map_err(|e| s3_error!(InvalidArgument, "invalid broker URL: {}", e))?;
|
||||||
|
rustfs_targets::target::mqtt::validate_mqtt_broker_url(&parsed_broker, &tls)
|
||||||
|
.map_err(|e| s3_error!(InvalidArgument, "{}", e))?;
|
||||||
|
check_mqtt_broker_available_with_tls(parsed_broker.as_str(), topic, username, password, &tls)
|
||||||
|
.await
|
||||||
|
.map_err(|e| match e {
|
||||||
|
TargetError::Configuration(_) => s3_error!(InvalidArgument, "{}", e),
|
||||||
|
_ => s3_error!(InvalidArgument, "MQTT broker check failed: {}", e),
|
||||||
|
})?;
|
||||||
|
|
||||||
|
if let Some(queue_dir) = kv_map.get("queue_dir") {
|
||||||
|
validate_queue_dir(queue_dir.as_str()).await?;
|
||||||
|
if let Some(qos) = kv_map.get(MQTT_QOS) {
|
||||||
|
match qos.parse::<u8>() {
|
||||||
|
Ok(1) | Ok(2) => {}
|
||||||
|
Ok(0) => return Err(s3_error!(InvalidArgument, "qos should be 1 or 2 if queue_dir is set")),
|
||||||
|
_ => return Err(s3_error!(InvalidArgument, "qos must be an integer 0, 1, or 2")),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn validate_nats_request(kv_map: &HashMap<String, String>, default_queue_dir: &str, domain: TargetDomain) -> S3Result<()> {
|
||||||
|
if let Some(queue_dir) = kv_map.get("queue_dir") {
|
||||||
|
validate_queue_dir(queue_dir.as_str()).await?;
|
||||||
|
}
|
||||||
|
let args = build_nats_args(&to_kvs(kv_map), default_queue_dir, domain.runtime_target_type())
|
||||||
|
.map_err(|e| s3_error!(InvalidArgument, "{}", e))?;
|
||||||
|
check_nats_server_available(&args).await.map_err(|e| match e {
|
||||||
|
TargetError::Configuration(_) => s3_error!(InvalidArgument, "{}", e),
|
||||||
|
_ => s3_error!(InvalidArgument, "NATS server check failed: {}", e),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn validate_pulsar_request(
|
||||||
|
kv_map: &HashMap<String, String>,
|
||||||
|
default_queue_dir: &str,
|
||||||
|
domain: TargetDomain,
|
||||||
|
) -> S3Result<()> {
|
||||||
|
if let Some(queue_dir) = kv_map.get("queue_dir") {
|
||||||
|
validate_queue_dir(queue_dir.as_str()).await?;
|
||||||
|
}
|
||||||
|
let args = build_pulsar_args(&to_kvs(kv_map), default_queue_dir, domain.runtime_target_type())
|
||||||
|
.map_err(|e| s3_error!(InvalidArgument, "{}", e))?;
|
||||||
|
check_pulsar_broker_available(&args).await.map_err(|e| match e {
|
||||||
|
TargetError::Configuration(_) => s3_error!(InvalidArgument, "{}", e),
|
||||||
|
_ => s3_error!(InvalidArgument, "Pulsar broker check failed: {}", e),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
fn to_kvs(kv_map: &HashMap<String, String>) -> rustfs_ecstore::config::KVS {
|
||||||
|
let mut kvs = rustfs_ecstore::config::KVS::new();
|
||||||
|
for (key, value) in kv_map {
|
||||||
|
kvs.insert(key.clone(), value.clone());
|
||||||
|
}
|
||||||
|
kvs
|
||||||
|
}
|
||||||
@@ -23,6 +23,8 @@ fn server_config_from_context() -> Option<rustfs_ecstore::config::Config> {
|
|||||||
fn has_any_audit_targets(config: &rustfs_ecstore::config::Config) -> bool {
|
fn has_any_audit_targets(config: &rustfs_ecstore::config::Config) -> bool {
|
||||||
for subsystem in [
|
for subsystem in [
|
||||||
rustfs_config::audit::AUDIT_MQTT_SUB_SYS,
|
rustfs_config::audit::AUDIT_MQTT_SUB_SYS,
|
||||||
|
rustfs_config::audit::AUDIT_NATS_SUB_SYS,
|
||||||
|
rustfs_config::audit::AUDIT_PULSAR_SUB_SYS,
|
||||||
rustfs_config::audit::AUDIT_WEBHOOK_SUB_SYS,
|
rustfs_config::audit::AUDIT_WEBHOOK_SUB_SYS,
|
||||||
] {
|
] {
|
||||||
let Some(targets) = config.0.get(subsystem) else {
|
let Some(targets) = config.0.get(subsystem) else {
|
||||||
@@ -73,7 +75,7 @@ pub async fn start_audit_system() -> AuditResult<()> {
|
|||||||
if !has_targets {
|
if !has_targets {
|
||||||
info!(
|
info!(
|
||||||
target: "rustfs::main::start_audit_system",
|
target: "rustfs::main::start_audit_system",
|
||||||
"Audit subsystem (MQTT/Webhook) is not configured, and audit system initialization is skipped."
|
"Audit subsystem (Webhook/MQTT/NATS/Pulsar) is not configured, and audit system initialization is skipped."
|
||||||
);
|
);
|
||||||
return Ok(());
|
return Ok(());
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user