mirror of
https://github.com/rustfs/rustfs.git
synced 2026-08-13 08:36:54 +00:00
feat(targets): add NATS and Pulsar target support (#2618)
This commit is contained in:
@@ -14,6 +14,13 @@
|
||||
|
||||
use crate::admin::{
|
||||
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},
|
||||
};
|
||||
use crate::auth::{check_key_valid, get_session_token};
|
||||
@@ -24,22 +31,20 @@ use http::{HeaderMap, StatusCode};
|
||||
use hyper::Method;
|
||||
use matchit::Params;
|
||||
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::{DEFAULT_DELIMITER, ENABLE_KEY, ENV_PREFIX, EnableState, MAX_ADMIN_REQUEST_BODY_SIZE};
|
||||
use rustfs_config::audit::{
|
||||
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_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 serde::{Deserialize, Serialize};
|
||||
use std::collections::{HashMap, HashSet};
|
||||
use std::future::Future;
|
||||
use std::io::{Error, ErrorKind};
|
||||
use std::path::Path;
|
||||
use std::sync::Arc;
|
||||
use tokio::sync::Semaphore;
|
||||
use tokio::time::{Duration, sleep, timeout};
|
||||
use tokio::time::{Duration, timeout};
|
||||
use tracing::{Span, warn};
|
||||
use url::Url;
|
||||
|
||||
pub fn register_audit_target_route(r: &mut S3Router<AdminOperation>) -> std::io::Result<()> {
|
||||
r.insert(
|
||||
@@ -87,8 +92,6 @@ struct AuditEndpointsResponse {
|
||||
audit_endpoints: Vec<AuditEndpoint>,
|
||||
}
|
||||
|
||||
type EndpointKey = (String, String);
|
||||
|
||||
#[derive(Clone, Copy, Debug, PartialEq, Eq, Serialize)]
|
||||
#[serde(rename_all = "lowercase")]
|
||||
enum AuditEndpointSource {
|
||||
@@ -98,8 +101,33 @@ enum AuditEndpointSource {
|
||||
Runtime,
|
||||
}
|
||||
|
||||
fn normalized_endpoint_key(account_id: &str, service: &str) -> EndpointKey {
|
||||
(account_id.to_lowercase(), service.to_string())
|
||||
fn audit_target_specs() -> [AdminTargetSpec; 4] {
|
||||
[
|
||||
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<()> {
|
||||
@@ -121,58 +149,9 @@ fn build_response(status: StatusCode, body: Body, request_id: Option<&http::Head
|
||||
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 {
|
||||
for subsystem in [AUDIT_WEBHOOK_SUB_SYS, AUDIT_MQTT_SUB_SYS] {
|
||||
let Some(targets) = config.0.get(subsystem) else {
|
||||
for spec in audit_target_specs() {
|
||||
let Some(targets) = config.0.get(spec.subsystem) else {
|
||||
continue;
|
||||
};
|
||||
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> {
|
||||
let mut endpoints = Vec::new();
|
||||
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
|
||||
shared_collect_configured_endpoint_keys(&audit_target_specs(), config)
|
||||
}
|
||||
|
||||
fn collect_config_entry_keys(config: &Config) -> HbHashSet<EndpointKey> {
|
||||
let mut endpoints = HbHashSet::new();
|
||||
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
|
||||
shared_collect_config_entry_keys(&audit_target_specs(), config)
|
||||
}
|
||||
|
||||
fn collect_env_endpoint_keys() -> HbHashSet<EndpointKey> {
|
||||
let mut endpoints = HbHashSet::new();
|
||||
|
||||
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
|
||||
shared_collect_env_endpoint_keys(&audit_target_specs(), AUDIT_ROUTE_PREFIX)
|
||||
}
|
||||
|
||||
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 {
|
||||
let config_targets = collect_config_entry_keys(config);
|
||||
let env_targets = collect_env_endpoint_keys();
|
||||
let service = match target_type {
|
||||
AUDIT_WEBHOOK_SUB_SYS => "webhook",
|
||||
AUDIT_MQTT_SUB_SYS => "mqtt",
|
||||
_ => "",
|
||||
};
|
||||
let service = target_service_name(&audit_target_specs(), target_type).unwrap_or_default();
|
||||
|
||||
let key = normalized_endpoint_key(target_name, service);
|
||||
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
|
||||
.get("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));
|
||||
}
|
||||
let target_name = params
|
||||
@@ -486,77 +403,16 @@ impl Operation for AuditTargetConfig {
|
||||
let audit_body: AuditTargetBody = serde_json::from_slice(&body_bytes)
|
||||
.map_err(|e| s3_error!(InvalidArgument, "invalid json body for audit target config: {}", e))?;
|
||||
|
||||
let allowed_keys: HashSet<&str> = match target_type {
|
||||
AUDIT_WEBHOOK_SUB_SYS => AUDIT_WEBHOOK_KEYS.iter().cloned().collect(),
|
||||
AUDIT_MQTT_SUB_SYS => AUDIT_MQTT_KEYS.iter().cloned().collect(),
|
||||
_ => unreachable!(),
|
||||
};
|
||||
let specs = audit_target_specs();
|
||||
let allowed_keys: HashSet<&str> = allowed_target_keys(&specs, 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 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"));
|
||||
}
|
||||
} 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 spec = target_spec(&specs, target_type)
|
||||
.ok_or_else(|| s3_error!(InvalidArgument, "unsupported audit target type: '{}'", target_type))?;
|
||||
timeout(Duration::from_secs(10), validate_target_request(spec, &kv_map, AUDIT_DEFAULT_DIR))
|
||||
.await
|
||||
.map_err(|_| s3_error!(InvalidArgument, "audit target validation timed out"))??;
|
||||
|
||||
let mut kvs = rustfs_ecstore::config::KVS::new();
|
||||
for (key, value) in kv_map {
|
||||
@@ -656,6 +512,7 @@ impl Operation for RemoveAuditTarget {
|
||||
mod tests {
|
||||
use super::*;
|
||||
use matchit::Router;
|
||||
use rustfs_config::ENV_PREFIX;
|
||||
use rustfs_ecstore::config::{KV, KVS};
|
||||
use std::collections::{HashMap, HashSet};
|
||||
use temp_env::{with_var, with_vars, with_vars_unset};
|
||||
|
||||
@@ -14,6 +14,13 @@
|
||||
|
||||
use crate::admin::{
|
||||
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},
|
||||
};
|
||||
use crate::auth::{check_key_valid, get_session_token};
|
||||
@@ -24,23 +31,19 @@ use http::{HeaderMap, StatusCode};
|
||||
use hyper::Method;
|
||||
use matchit::Params;
|
||||
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_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 serde::{Deserialize, Serialize};
|
||||
use std::collections::{HashMap, HashSet};
|
||||
use std::future::Future;
|
||||
use std::io::{Error, ErrorKind};
|
||||
use std::path::Path;
|
||||
use std::sync::Arc;
|
||||
use tokio::sync::Semaphore;
|
||||
use tokio::time::{Duration, sleep, timeout};
|
||||
use tokio::time::{Duration, timeout};
|
||||
use tracing::{Span, info, warn};
|
||||
use url::Url;
|
||||
|
||||
pub fn register_notification_target_route(r: &mut S3Router<AdminOperation>) -> std::io::Result<()> {
|
||||
r.insert(
|
||||
@@ -94,8 +97,6 @@ struct NotificationEndpointsResponse {
|
||||
notification_endpoints: Vec<NotificationEndpoint>,
|
||||
}
|
||||
|
||||
type EndpointKey = (String, String);
|
||||
|
||||
#[derive(Clone, Copy, Debug, PartialEq, Eq, Serialize)]
|
||||
#[serde(rename_all = "lowercase")]
|
||||
enum NotificationEndpointSource {
|
||||
@@ -105,8 +106,33 @@ enum NotificationEndpointSource {
|
||||
Runtime,
|
||||
}
|
||||
|
||||
fn normalized_endpoint_key(account_id: &str, service: &str) -> EndpointKey {
|
||||
(account_id.to_lowercase(), service.to_string())
|
||||
fn notification_target_specs() -> [AdminTargetSpec; 4] {
|
||||
[
|
||||
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 ---
|
||||
@@ -134,123 +160,16 @@ fn build_response(status: StatusCode, body: Body, request_id: Option<&http::Head
|
||||
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> {
|
||||
let mut endpoints = Vec::new();
|
||||
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
|
||||
shared_collect_configured_endpoint_keys(¬ification_target_specs(), config)
|
||||
}
|
||||
|
||||
fn collect_config_entry_keys(config: &Config) -> HbHashSet<EndpointKey> {
|
||||
let mut endpoints = HbHashSet::new();
|
||||
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
|
||||
shared_collect_config_entry_keys(¬ification_target_specs(), config)
|
||||
}
|
||||
|
||||
fn collect_env_endpoint_keys() -> HbHashSet<EndpointKey> {
|
||||
let mut endpoints = HbHashSet::new();
|
||||
|
||||
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
|
||||
shared_collect_env_endpoint_keys(¬ification_target_specs(), NOTIFY_ROUTE_PREFIX)
|
||||
}
|
||||
|
||||
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 {
|
||||
let config_targets = collect_config_entry_keys(config);
|
||||
let env_targets = collect_env_endpoint_keys();
|
||||
let service = match target_type {
|
||||
NOTIFY_WEBHOOK_SUB_SYS => "webhook",
|
||||
NOTIFY_MQTT_SUB_SYS => "mqtt",
|
||||
_ => "",
|
||||
};
|
||||
let service = target_service_name(¬ification_target_specs(), target_type).unwrap_or_default();
|
||||
|
||||
let key = normalized_endpoint_key(target_name, service);
|
||||
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)
|
||||
.map_err(|e| s3_error!(InvalidArgument, "invalid json body for target config: {}", e))?;
|
||||
|
||||
let allowed_keys: HashSet<&str> = match target_type {
|
||||
NOTIFY_WEBHOOK_SUB_SYS => rustfs_config::notify::NOTIFY_WEBHOOK_KEYS.iter().cloned().collect(),
|
||||
NOTIFY_MQTT_SUB_SYS => rustfs_config::notify::NOTIFY_MQTT_KEYS.iter().cloned().collect(),
|
||||
_ => unreachable!(),
|
||||
};
|
||||
let specs = notification_target_specs();
|
||||
let allowed_keys: HashSet<&str> = allowed_target_keys(&specs, target_type);
|
||||
|
||||
let kv_map = collect_validated_key_values(¬ification_body.key_values, &allowed_keys, target_type)?;
|
||||
|
||||
// Type-specific validation
|
||||
if target_type == NOTIFY_WEBHOOK_SUB_SYS {
|
||||
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"));
|
||||
}
|
||||
} 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 spec = target_spec(&specs, target_type)
|
||||
.ok_or_else(|| s3_error!(InvalidArgument, "unsupported target type: '{}'", target_type))?;
|
||||
timeout(Duration::from_secs(10), validate_target_request(spec, &kv_map, EVENT_DEFAULT_DIR))
|
||||
.await
|
||||
.map_err(|_| s3_error!(InvalidArgument, "target validation timed out"))??;
|
||||
|
||||
let mut kvs = rustfs_ecstore::config::KVS::new();
|
||||
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)> {
|
||||
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));
|
||||
}
|
||||
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)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use rustfs_config::DEFAULT_DELIMITER;
|
||||
use rustfs_ecstore::config::{KV, KVS};
|
||||
use rustfs_targets::arn::TargetID;
|
||||
use std::collections::{HashMap, HashSet};
|
||||
|
||||
@@ -37,6 +37,7 @@ pub mod service_account;
|
||||
pub mod site_replication;
|
||||
pub mod sts;
|
||||
pub mod system;
|
||||
mod target_descriptor;
|
||||
pub mod tier;
|
||||
pub mod trace;
|
||||
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 {
|
||||
for subsystem in [
|
||||
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,
|
||||
] {
|
||||
let Some(targets) = config.0.get(subsystem) else {
|
||||
@@ -73,7 +75,7 @@ pub async fn start_audit_system() -> AuditResult<()> {
|
||||
if !has_targets {
|
||||
info!(
|
||||
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(());
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user