feat(targets): add AMQP support for notify and audit (#2879)

Co-authored-by: Hyesook Yun <74169420+suk13574@users.noreply.github.com>
This commit is contained in:
houseme
2026-05-09 09:56:26 +08:00
committed by GitHub
parent 1582f216fe
commit 81ad48dac2
29 changed files with 3280 additions and 830 deletions
+4 -3
View File
@@ -21,7 +21,8 @@ pub use loader::{
collect_target_configs_from_env,
};
pub use target_args::{
build_kafka_args, build_mqtt_args, build_mysql_args, build_nats_args, build_postgres_args, build_pulsar_args,
build_redis_args, build_webhook_args, validate_kafka_config, validate_mqtt_config, validate_mysql_config,
validate_nats_config, validate_postgres_config, validate_pulsar_config, validate_redis_config, validate_webhook_config,
build_amqp_args, build_kafka_args, build_mqtt_args, build_mysql_args, build_nats_args, build_postgres_args,
build_pulsar_args, build_redis_args, build_webhook_args, validate_amqp_config, validate_kafka_config, validate_mqtt_config,
validate_mysql_config, validate_nats_config, validate_postgres_config, validate_pulsar_config, validate_redis_config,
validate_webhook_config,
};
+200 -23
View File
@@ -16,6 +16,7 @@ use super::common::{parse_target_bool, parse_url, validate_nats_server_config, v
use crate::error::TargetError;
use crate::target::{
TargetType,
amqp::AMQPArgs,
kafka::KafkaArgs,
mqtt::{MQTTArgs, MQTTTlsConfig, validate_mqtt_broker_url},
mysql::MySqlArgs,
@@ -27,22 +28,23 @@ use crate::target::{
};
use rumqttc::QoS;
use rustfs_config::{
DEFAULT_LIMIT, KAFKA_ACKS, KAFKA_BROKERS, KAFKA_QUEUE_DIR, KAFKA_QUEUE_LIMIT, KAFKA_TLS_CA, KAFKA_TLS_CLIENT_CERT,
KAFKA_TLS_CLIENT_KEY, KAFKA_TLS_ENABLE, KAFKA_TOPIC, 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, MYSQL_DSN_STRING,
MYSQL_FORMAT, MYSQL_MAX_OPEN_CONNECTIONS, MYSQL_QUEUE_DIR, MYSQL_QUEUE_LIMIT, MYSQL_TABLE, MYSQL_TLS_CA,
MYSQL_TLS_CLIENT_CERT, MYSQL_TLS_CLIENT_KEY, 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, POSTGRES_DSN_STRING, POSTGRES_FORMAT, POSTGRES_QUEUE_DIR, POSTGRES_QUEUE_LIMIT, POSTGRES_TABLE,
POSTGRES_TLS_CA, POSTGRES_TLS_CLIENT_CERT, POSTGRES_TLS_CLIENT_KEY, POSTGRES_TLS_REQUIRED, 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, REDIS_CHANNEL, REDIS_CONNECTION_TIMEOUT,
REDIS_KEEP_ALIVE_INTERVAL, REDIS_MAX_RETRY_ATTEMPTS, REDIS_MAX_RETRY_DELAY, REDIS_MIN_RETRY_DELAY, REDIS_PASSWORD,
REDIS_PIPELINE_BUFFER_SIZE, REDIS_QUEUE_DIR, REDIS_QUEUE_LIMIT, REDIS_RECONNECT_RETRY_ATTEMPTS, REDIS_RESPONSE_TIMEOUT,
REDIS_TLS_ALLOW_INSECURE, REDIS_TLS_CA, REDIS_TLS_CLIENT_CERT, REDIS_TLS_CLIENT_KEY, REDIS_TLS_POLICY, REDIS_URL,
REDIS_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,
AMQP_EXCHANGE, AMQP_MANDATORY, AMQP_PASSWORD, AMQP_PERSISTENT, AMQP_QUEUE_DIR, AMQP_QUEUE_LIMIT, AMQP_ROUTING_KEY,
AMQP_TLS_CA, AMQP_TLS_CLIENT_CERT, AMQP_TLS_CLIENT_KEY, AMQP_URL, AMQP_USERNAME, DEFAULT_LIMIT, KAFKA_ACKS, KAFKA_BROKERS,
KAFKA_QUEUE_DIR, KAFKA_QUEUE_LIMIT, KAFKA_TLS_CA, KAFKA_TLS_CLIENT_CERT, KAFKA_TLS_CLIENT_KEY, KAFKA_TLS_ENABLE, KAFKA_TOPIC,
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, MYSQL_DSN_STRING, MYSQL_FORMAT, MYSQL_MAX_OPEN_CONNECTIONS, MYSQL_QUEUE_DIR,
MYSQL_QUEUE_LIMIT, MYSQL_TABLE, MYSQL_TLS_CA, MYSQL_TLS_CLIENT_CERT, MYSQL_TLS_CLIENT_KEY, 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, POSTGRES_DSN_STRING, POSTGRES_FORMAT, POSTGRES_QUEUE_DIR,
POSTGRES_QUEUE_LIMIT, POSTGRES_TABLE, POSTGRES_TLS_CA, POSTGRES_TLS_CLIENT_CERT, POSTGRES_TLS_CLIENT_KEY,
POSTGRES_TLS_REQUIRED, 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, REDIS_CHANNEL,
REDIS_CONNECTION_TIMEOUT, REDIS_KEEP_ALIVE_INTERVAL, REDIS_MAX_RETRY_ATTEMPTS, REDIS_MAX_RETRY_DELAY, REDIS_MIN_RETRY_DELAY,
REDIS_PASSWORD, REDIS_PIPELINE_BUFFER_SIZE, REDIS_QUEUE_DIR, REDIS_QUEUE_LIMIT, REDIS_RECONNECT_RETRY_ATTEMPTS,
REDIS_RESPONSE_TIMEOUT, REDIS_TLS_ALLOW_INSECURE, REDIS_TLS_CA, REDIS_TLS_CLIENT_CERT, REDIS_TLS_CLIENT_KEY,
REDIS_TLS_POLICY, REDIS_URL, REDIS_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;
@@ -66,6 +68,55 @@ fn parse_kafka_acks_value(value: Option<&str>) -> Result<i16, TargetError> {
}
}
fn parse_amqp_bool_value(field: &str, config: &KVS, default: bool) -> Result<bool, TargetError> {
match config.lookup(field) {
Some(value) => parse_target_bool(Some(value.as_str()))
.ok_or_else(|| TargetError::Configuration(format!("Invalid AMQP {field} boolean value: {value}"))),
None => Ok(default),
}
}
pub fn build_amqp_args(config: &KVS, default_queue_dir: &str, target_type: TargetType) -> Result<AMQPArgs, TargetError> {
let url = config
.lookup(AMQP_URL)
.ok_or_else(|| TargetError::Configuration("Missing AMQP url".to_string()))?;
let url = parse_url(url.trim(), "AMQP URL")?;
let exchange = config
.lookup(AMQP_EXCHANGE)
.ok_or_else(|| TargetError::Configuration("Missing AMQP exchange".to_string()))?;
let routing_key = config
.lookup(AMQP_ROUTING_KEY)
.ok_or_else(|| TargetError::Configuration("Missing AMQP routing_key".to_string()))?;
let args = AMQPArgs {
enable: true,
url,
exchange,
routing_key,
mandatory: parse_amqp_bool_value(AMQP_MANDATORY, config, false)?,
persistent: parse_amqp_bool_value(AMQP_PERSISTENT, config, true)?,
username: config.lookup(AMQP_USERNAME).unwrap_or_default(),
password: config.lookup(AMQP_PASSWORD).unwrap_or_default(),
tls_ca: config.lookup(AMQP_TLS_CA).unwrap_or_default(),
tls_client_cert: config.lookup(AMQP_TLS_CLIENT_CERT).unwrap_or_default(),
tls_client_key: config.lookup(AMQP_TLS_CLIENT_KEY).unwrap_or_default(),
queue_dir: config.lookup(AMQP_QUEUE_DIR).unwrap_or_else(|| default_queue_dir.to_string()),
queue_limit: config
.lookup(AMQP_QUEUE_LIMIT)
.and_then(|v| v.parse::<u64>().ok())
.unwrap_or(DEFAULT_LIMIT),
target_type,
};
args.validate()?;
Ok(args)
}
pub fn validate_amqp_config(config: &KVS, default_queue_dir: &str) -> Result<(), TargetError> {
let _ = build_amqp_args(config, default_queue_dir, TargetType::NotifyEvent)?;
Ok(())
}
pub fn build_webhook_args(config: &KVS, default_queue_dir: &str, target_type: TargetType) -> Result<WebhookArgs, TargetError> {
let endpoint = config
.lookup(WEBHOOK_ENDPOINT)
@@ -540,19 +591,28 @@ pub fn validate_mysql_config(config: &KVS, default_queue_dir: &str) -> Result<()
#[cfg(test)]
mod tests {
use super::{
build_kafka_args, build_mysql_args, build_postgres_args, build_redis_args, validate_kafka_config, validate_mysql_config,
validate_postgres_config, validate_redis_config,
build_amqp_args, build_kafka_args, build_mysql_args, build_postgres_args, build_redis_args, validate_amqp_config,
validate_kafka_config, validate_mysql_config, validate_postgres_config, validate_redis_config,
};
use crate::target::{TargetType, postgres::PostgresFormat};
use rustfs_config::{
KAFKA_ACKS, KAFKA_BROKERS, KAFKA_TOPIC, MYSQL_DSN_STRING, MYSQL_MAX_OPEN_CONNECTIONS, MYSQL_QUEUE_DIR, MYSQL_TABLE,
MYSQL_TLS_CA, MYSQL_TLS_CLIENT_CERT, MYSQL_TLS_CLIENT_KEY, POSTGRES_DSN_STRING, POSTGRES_FORMAT, POSTGRES_QUEUE_DIR,
POSTGRES_TABLE, POSTGRES_TLS_CA, POSTGRES_TLS_CLIENT_CERT, POSTGRES_TLS_CLIENT_KEY, REDIS_CHANNEL,
REDIS_CONNECTION_TIMEOUT, REDIS_MAX_RETRY_DELAY, REDIS_MIN_RETRY_DELAY, REDIS_PIPELINE_BUFFER_SIZE,
REDIS_RECONNECT_RETRY_ATTEMPTS, REDIS_RESPONSE_TIMEOUT, REDIS_TLS_ALLOW_INSECURE, REDIS_URL,
AMQP_EXCHANGE, AMQP_MANDATORY, AMQP_PASSWORD, AMQP_PERSISTENT, AMQP_QUEUE_DIR, AMQP_ROUTING_KEY, AMQP_TLS_CLIENT_CERT,
AMQP_TLS_CLIENT_KEY, AMQP_URL, AMQP_USERNAME, KAFKA_ACKS, KAFKA_BROKERS, KAFKA_TOPIC, MYSQL_DSN_STRING,
MYSQL_MAX_OPEN_CONNECTIONS, MYSQL_QUEUE_DIR, MYSQL_TABLE, MYSQL_TLS_CA, MYSQL_TLS_CLIENT_CERT, MYSQL_TLS_CLIENT_KEY,
POSTGRES_DSN_STRING, POSTGRES_FORMAT, POSTGRES_QUEUE_DIR, POSTGRES_TABLE, POSTGRES_TLS_CA, POSTGRES_TLS_CLIENT_CERT,
POSTGRES_TLS_CLIENT_KEY, REDIS_CHANNEL, REDIS_CONNECTION_TIMEOUT, REDIS_MAX_RETRY_DELAY, REDIS_MIN_RETRY_DELAY,
REDIS_PIPELINE_BUFFER_SIZE, REDIS_RECONNECT_RETRY_ATTEMPTS, REDIS_RESPONSE_TIMEOUT, REDIS_TLS_ALLOW_INSECURE, REDIS_URL,
};
use rustfs_ecstore::config::KVS;
fn amqp_base_config() -> KVS {
let mut config = KVS::new();
config.insert(AMQP_URL.to_string(), "amqp://127.0.0.1:5672/%2f".to_string());
config.insert(AMQP_EXCHANGE.to_string(), "rustfs.events".to_string());
config.insert(AMQP_ROUTING_KEY.to_string(), "objects".to_string());
config
}
fn kafka_base_config() -> KVS {
let mut config = KVS::new();
config.insert(KAFKA_BROKERS.to_string(), "127.0.0.1:9092".to_string());
@@ -570,6 +630,123 @@ mod tests {
config
}
#[test]
fn build_amqp_args_accepts_valid_config() {
let args = build_amqp_args(&amqp_base_config(), "", TargetType::NotifyEvent).expect("valid AMQP args");
assert_eq!(args.url.as_str(), "amqp://127.0.0.1:5672/%2f");
assert_eq!(args.exchange, "rustfs.events");
assert_eq!(args.routing_key, "objects");
assert!(!args.mandatory);
assert!(args.persistent);
}
#[test]
fn build_amqp_args_accepts_bool_aliases() {
let mut config = amqp_base_config();
config.insert(AMQP_MANDATORY.to_string(), "on".to_string());
config.insert(AMQP_PERSISTENT.to_string(), "no".to_string());
let args = build_amqp_args(&config, "", TargetType::NotifyEvent).expect("valid AMQP bool aliases");
assert!(args.mandatory);
assert!(!args.persistent);
}
#[test]
fn validate_amqp_config_rejects_invalid_bool() {
let mut config = amqp_base_config();
config.insert(AMQP_MANDATORY.to_string(), "sometimes".to_string());
let err = validate_amqp_config(&config, "").expect_err("invalid AMQP bool should fail");
assert!(err.to_string().contains("Invalid AMQP mandatory boolean"));
}
#[test]
fn validate_amqp_config_rejects_invalid_scheme() {
let mut config = amqp_base_config();
config.insert(AMQP_URL.to_string(), "http://127.0.0.1:5672".to_string());
let err = validate_amqp_config(&config, "").expect_err("invalid AMQP scheme should fail");
assert!(err.to_string().contains("only amqp and amqps"));
}
#[test]
fn validate_amqp_config_rejects_missing_url_host() {
let mut config = amqp_base_config();
config.insert(AMQP_URL.to_string(), "amqp:///objects".to_string());
let err = validate_amqp_config(&config, "").expect_err("missing AMQP host should fail");
assert!(err.to_string().contains("missing host"));
}
#[test]
fn validate_amqp_config_rejects_missing_exchange() {
let mut config = amqp_base_config();
config.0.retain(|kv| kv.key != AMQP_EXCHANGE);
let err = validate_amqp_config(&config, "").expect_err("missing AMQP exchange should fail");
assert!(err.to_string().contains("Missing AMQP exchange"));
}
#[test]
fn validate_amqp_config_rejects_missing_routing_key() {
let mut config = amqp_base_config();
config.0.retain(|kv| kv.key != AMQP_ROUTING_KEY);
let err = validate_amqp_config(&config, "").expect_err("missing AMQP routing_key should fail");
assert!(err.to_string().contains("Missing AMQP routing_key"));
}
#[test]
fn validate_amqp_config_rejects_relative_queue_dir() {
let mut config = amqp_base_config();
config.insert(AMQP_QUEUE_DIR.to_string(), "relative-queue".to_string());
let err = validate_amqp_config(&config, "").expect_err("relative queue_dir should fail");
assert!(err.to_string().contains("absolute path"));
}
#[test]
fn validate_amqp_config_rejects_unpaired_tls_client_cert_key() {
let mut config = amqp_base_config();
config.insert(AMQP_URL.to_string(), "amqps://127.0.0.1:5671/%2f".to_string());
config.insert(AMQP_TLS_CLIENT_CERT.to_string(), "/tmp/client.crt".to_string());
let err = validate_amqp_config(&config, "").expect_err("unpaired TLS cert should fail");
assert!(err.to_string().contains("tls_client_cert and tls_client_key"));
}
#[test]
fn validate_amqp_config_rejects_tls_paths_without_amqps() {
let mut config = amqp_base_config();
config.insert(AMQP_TLS_CLIENT_CERT.to_string(), "/tmp/client.crt".to_string());
config.insert(AMQP_TLS_CLIENT_KEY.to_string(), "/tmp/client.key".to_string());
let err = validate_amqp_config(&config, "").expect_err("TLS paths without amqps should fail");
assert!(err.to_string().contains("only allowed with amqps"));
}
#[test]
fn validate_amqp_config_rejects_ambiguous_credentials() {
let mut config = amqp_base_config();
config.insert(AMQP_URL.to_string(), "amqp://guest:guest@127.0.0.1:5672/%2f".to_string());
config.insert(AMQP_USERNAME.to_string(), "user".to_string());
config.insert(AMQP_PASSWORD.to_string(), "password".to_string());
let err = validate_amqp_config(&config, "").expect_err("ambiguous credentials should fail");
assert!(err.to_string().contains("either in url or username/password"));
}
#[test]
fn build_kafka_args_accepts_all_ack_alias() {
let mut config = kafka_base_config();