1pub async fn check_mqtt_broker_available(
47 broker_url: &str,
48 topic: &str,
49 username: Option<&str>,
50 password: Option<&str>,
51) -> Result<(), crate::TargetError> {
52 use crate::target::mqtt::MQTTTlsConfig;
53
54 check_mqtt_broker_available_with_tls(broker_url, topic, username, password, &MQTTTlsConfig::default()).await
55}
56
57pub async fn check_mqtt_broker_available_with_tls(
58 broker_url: &str,
59 topic: &str,
60 username: Option<&str>,
61 password: Option<&str>,
62 tls: &crate::target::mqtt::MQTTTlsConfig,
63) -> Result<(), crate::TargetError> {
64 use crate::target::mqtt::build_mqtt_options;
65 use rumqttc::{AsyncClient, QoS};
66
67 let url =
68 crate::parse_url(broker_url).map_err(|e| crate::TargetError::Configuration(format!("Broker URL parsing failed: {e}")))?;
69 let url = url.url();
70
71 let mqtt_options = build_mqtt_options(
73 "rustfs_check".to_string(),
74 url,
75 username,
76 password,
77 tls,
78 std::time::Duration::from_secs(5),
79 None,
80 )?;
81 let (client, mut eventloop) = AsyncClient::builder(mqtt_options).capacity(1).build();
82
83 client
85 .subscribe(topic, QoS::AtLeastOnce)
86 .await
87 .map_err(|e| crate::TargetError::Network(format!("MQTT subscription failed: {e}")))?;
88 match tokio::time::timeout(std::time::Duration::from_secs(3), eventloop.poll()).await {
90 Ok(Ok(_)) => Ok(()),
91 Ok(Err(e)) => Err(crate::TargetError::Network(format!("MQTT connection failed: {e}"))),
92 Err(_) => Err(crate::TargetError::Timeout("MQTT connection timed out".to_string())),
93 }
94}
95
96pub async fn check_nats_server_available(args: &crate::target::nats::NATSArgs) -> Result<(), crate::TargetError> {
97 tokio::time::timeout(std::time::Duration::from_secs(5), async {
98 let client = crate::target::nats::connect_nats(args).await?;
99 client
100 .flush()
101 .await
102 .map_err(|e| crate::TargetError::Network(format!("NATS connection check failed: {e}")))?;
103 let validation = if args.jetstream_enable.unwrap_or(false) {
108 let mut context = async_nats::jetstream::new(client.clone());
109 context.set_timeout(std::time::Duration::from_secs(
112 args.jetstream_ack_timeout_secs
113 .unwrap_or(rustfs_config::NATS_JETSTREAM_ACK_TIMEOUT_DEFAULT_SECS),
114 ));
115 crate::target::nats::validate_jetstream_stream(&context, args, "nats-check", None).await
116 } else {
117 Ok(())
118 };
119 client
120 .drain()
121 .await
122 .map_err(|e| crate::TargetError::Network(format!("Failed to close NATS check connection: {e}")))?;
123 validation
124 })
125 .await
126 .unwrap_or_else(|_| Err(crate::TargetError::Timeout("NATS connection timed out".to_string())))
127}
128
129pub async fn check_pulsar_broker_available(args: &crate::target::pulsar::PulsarArgs) -> Result<(), crate::TargetError> {
130 tokio::time::timeout(std::time::Duration::from_secs(5), async {
131 let client = crate::target::pulsar::connect_pulsar(args).await?;
132 client
133 .lookup_partitioned_topic(args.topic.clone())
134 .await
135 .map_err(|e| crate::TargetError::Network(format!("Pulsar topic lookup failed: {e}")))?;
136 Ok(())
137 })
138 .await
139 .unwrap_or_else(|_| Err(crate::TargetError::Timeout("Pulsar connection timed out".to_string())))
140}
141
142pub async fn check_mysql_server_available(args: &crate::target::mysql::MySqlArgs) -> Result<(), crate::TargetError> {
148 use crate::target::ensure_rustls_provider_installed;
149 use crate::target::mysql::{MySqlDsn, map_mysql_error};
150 use mysql_async::{Opts, OptsBuilder, Pool, SslOpts, prelude::Queryable};
151 use std::path::PathBuf;
152
153 args.validate()?;
154
155 let dsn = MySqlDsn::parse(&args.dsn_string)?;
156
157 let mut builder = OptsBuilder::default()
158 .user(Some(dsn.user.clone()))
159 .pass(Some(dsn.password.clone()))
160 .ip_or_hostname(dsn.host.clone())
161 .tcp_port(dsn.port)
162 .db_name(Some(dsn.database.clone()));
163
164 if dsn.tls {
165 ensure_rustls_provider_installed();
166 let mut ssl_opts = SslOpts::default();
167 if !args.tls_ca.is_empty() {
168 ssl_opts = ssl_opts.with_root_certs(vec![PathBuf::from(args.tls_ca.clone()).into()]);
169 }
170 if !args.tls_client_cert.is_empty() && !args.tls_client_key.is_empty() {
171 let identity = mysql_async::ClientIdentity::new(
172 PathBuf::from(args.tls_client_cert.clone()).into(),
173 PathBuf::from(args.tls_client_key.clone()).into(),
174 );
175 ssl_opts = ssl_opts.with_client_identity(Some(identity));
176 }
177 builder = builder.ssl_opts(Some(ssl_opts));
178 }
179
180 let pool = Pool::new(Opts::from(builder));
181 let timeout = std::time::Duration::from_secs(8);
186 tokio::time::timeout(timeout, async {
187 let mut conn = pool
188 .get_conn()
189 .await
190 .map_err(|err| map_mysql_error(err, "MySQL connectivity probe failed to acquire connection"))?;
191 conn.query_drop("SELECT 1")
192 .await
193 .map_err(|err| map_mysql_error(err, "MySQL connectivity probe failed"))?;
194 Ok::<(), crate::TargetError>(())
195 })
196 .await
197 .unwrap_or_else(|_| Err(crate::TargetError::Timeout("MySQL connectivity probe timed out".to_string())))
198}
199
200pub async fn check_postgres_server_available(args: &crate::target::postgres::PostgresArgs) -> Result<(), crate::TargetError> {
218 use crate::target::postgres::{build_pool, map_pg_error, map_pool_error, table_probe_sql};
219
220 args.validate()?;
221
222 let timeout = std::time::Duration::from_secs(8);
223 tokio::time::timeout(timeout, async {
224 let pool = build_pool(args)?;
225 let client = pool
226 .get()
227 .await
228 .map_err(|e| map_pool_error(e, "PostgreSQL connectivity probe failed to acquire connection"))?;
229 client
230 .execute("SELECT 1", &[])
231 .await
232 .map_err(|e| map_pg_error(&e, "PostgreSQL liveness probe failed"))?;
233 let probe_sql = table_probe_sql(&args.schema, &args.table);
234 client
235 .execute(probe_sql.as_str(), &[])
236 .await
237 .map_err(|e| map_pg_error(&e, "PostgreSQL table probe failed"))?;
238 pool.close();
239 Ok::<(), crate::TargetError>(())
240 })
241 .await
242 .unwrap_or_else(|_| Err(crate::TargetError::Timeout("PostgreSQL connectivity probe timed out".to_string())))
243}
244
245pub async fn check_kafka_broker_available(args: &crate::target::kafka::KafkaArgs) -> Result<(), crate::TargetError> {
246 use rustfs_kafka_async::error::{ConnectionError, Error as KafkaError};
247 use rustfs_kafka_async::{AsyncProducer, AsyncProducerConfig, RequiredAcks};
248 use std::time::Duration;
249
250 args.validate()?;
251
252 let map_kafka_error = |err: KafkaError, context: &str| match err {
253 KafkaError::Connection(ConnectionError::NoHostReachable) => crate::TargetError::NotConnected,
254 KafkaError::Connection(ConnectionError::Timeout(_)) => crate::TargetError::Timeout(format!("{context}: {err}")),
255 KafkaError::Connection(_) => crate::TargetError::Network(format!("{context}: {err}")),
256 KafkaError::Config(_) => crate::TargetError::Configuration(format!("{context}: {err}")),
257 _ => crate::TargetError::Request(format!("{context}: {err}")),
258 };
259
260 let acks = match args.acks {
261 0 => RequiredAcks::None,
262 1 => RequiredAcks::One,
263 _ => RequiredAcks::All,
264 };
265
266 let mut config = AsyncProducerConfig::new()
267 .with_ack_timeout(Duration::from_secs(5))
268 .with_required_acks(acks);
269
270 if let Some(security) = args.security_config(false)? {
271 config = config.with_security(security);
272 }
273
274 tokio::time::timeout(Duration::from_secs(5), async {
275 let _ = AsyncProducer::from_hosts_with_config(args.brokers.clone(), config)
276 .await
277 .map_err(|err| map_kafka_error(err, "Kafka broker check failed to create producer"))?;
278 Ok(())
279 })
280 .await
281 .unwrap_or_else(|_| Err(crate::TargetError::Timeout("Kafka connection timed out".to_string())))
282}
283
284pub async fn check_redis_server_available(args: &crate::target::redis::RedisArgs) -> Result<(), crate::TargetError> {
285 tokio::time::timeout(std::time::Duration::from_secs(5), async {
286 let client = crate::target::redis::build_redis_client(args)?;
287 crate::target::redis::ping_redis_server(&client, args).await
288 })
289 .await
290 .unwrap_or_else(|_| Err(crate::TargetError::Timeout("Redis connection timed out".to_string())))
291}
292
293pub async fn check_amqp_broker_available(args: &crate::target::amqp::AMQPArgs) -> Result<(), crate::TargetError> {
294 match tokio::time::timeout(std::time::Duration::from_secs(5), async {
295 let connection = crate::target::amqp::connect_amqp(args).await?;
296 if !connection.connection.status().connected() || !connection.channel.status().connected() {
297 return Err(crate::TargetError::NotConnected);
298 }
299 connection
300 .connection
301 .close(200, "OK".into())
302 .await
303 .map_err(|e| crate::TargetError::Network(format!("Failed to close AMQP check connection: {e}")))?;
304 Ok(())
305 })
306 .await
307 {
308 Ok(result) => result,
309 Err(_) => Err(crate::TargetError::Timeout("AMQP connection timed out".to_string())),
310 }
311}
312
313#[cfg(test)]
314mod tests {
315 use super::*;
316 use crate::{
317 TargetError,
318 target::{TargetType, kafka::KafkaArgs, mysql::MySqlArgs},
319 };
320
321 fn kafka_args() -> KafkaArgs {
322 KafkaArgs {
323 enable: true,
324 brokers: vec!["127.0.0.1:9092".to_string()],
325 topic: "rustfs-events".to_string(),
326 acks: 1,
327 tls_enable: false,
328 tls_ca: String::new(),
329 tls_client_cert: String::new(),
330 tls_client_key: String::new(),
331 sasl_enable: false,
332 sasl_mechanism: String::new(),
333 sasl_username: String::new(),
334 sasl_password: String::new(),
335 queue_dir: String::new(),
336 queue_limit: 100,
337 target_type: TargetType::NotifyEvent,
338 }
339 }
340
341 fn mysql_args() -> MySqlArgs {
342 MySqlArgs {
343 enable: true,
344 dsn_string: "rustfs:password@tcp(127.0.0.1:3306)/rustfs_events".to_string(),
345 table: "rustfs_events".to_string(),
346 format: "access".to_string(),
347 tls_ca: String::new(),
348 tls_client_cert: String::new(),
349 tls_client_key: String::new(),
350 queue_dir: String::new(),
351 queue_limit: 100,
352 max_open_connections: 2,
353 target_type: TargetType::NotifyEvent,
354 }
355 }
356
357 #[test]
358 fn check_kafka_broker_available_rejects_sasl_without_tls_before_connecting() {
359 let mut args = kafka_args();
360 args.sasl_enable = true;
361 args.sasl_username = "user".to_string();
362 args.sasl_password = "secret".to_string();
363
364 let err = tokio::runtime::Runtime::new()
365 .expect("runtime")
366 .block_on(check_kafka_broker_available(&args))
367 .expect_err("SASL without TLS should fail before opening a network connection");
368
369 match err {
370 TargetError::Configuration(msg) => assert!(msg.contains("requires tls_enable")),
371 other => panic!("expected configuration error, got {other:?}"),
372 }
373 }
374
375 #[test]
376 fn check_mysql_server_available_rejects_invalid_table_before_connecting() {
377 let mut args = mysql_args();
378 args.table = "rustfs-events".to_string();
379
380 let err = tokio::runtime::Runtime::new()
381 .expect("runtime")
382 .block_on(check_mysql_server_available(&args))
383 .expect_err("invalid table should fail before opening a network connection");
384
385 match err {
386 TargetError::Configuration(msg) => assert!(msg.contains("not a valid identifier")),
387 other => panic!("expected configuration error, got {other:?}"),
388 }
389 }
390
391 #[test]
392 fn check_mysql_server_available_rejects_unpaired_tls_client_fields_before_connecting() {
393 let mut args = mysql_args();
394 args.dsn_string = "rustfs:password@tcp(127.0.0.1:3306)/rustfs_events?tls=true".to_string();
395 args.tls_client_cert = "/etc/ssl/mysql/client.pem".to_string();
396
397 let err = tokio::runtime::Runtime::new()
398 .expect("runtime")
399 .block_on(check_mysql_server_available(&args))
400 .expect_err("unpaired TLS client fields should fail before opening a network connection");
401
402 match err {
403 TargetError::Configuration(msg) => assert!(msg.contains("must be specified together")),
404 other => panic!("expected configuration error, got {other:?}"),
405 }
406 }
407}