Skip to main content

risingwave_connector/connector_common/
common.rs

1// Copyright 2024 RisingWave Labs
2//
3// Licensed under the Apache License, Version 2.0 (the "License");
4// you may not use this file except in compliance with the License.
5// You may obtain a copy of the License at
6//
7//     http://www.apache.org/licenses/LICENSE-2.0
8//
9// Unless required by applicable law or agreed to in writing, software
10// distributed under the License is distributed on an "AS IS" BASIS,
11// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12// See the License for the specific language governing permissions and
13// limitations under the License.
14
15use std::collections::BTreeMap;
16use std::hash::Hash;
17use std::io::Write;
18use std::path::Path;
19use std::sync::{Arc, LazyLock, Weak};
20use std::time::Duration;
21
22use anyhow::{Context, anyhow};
23use async_nats::jetstream::consumer::DeliverPolicy;
24use async_nats::jetstream::{self};
25use aws_sdk_kinesis::Client as KinesisClient;
26use aws_sdk_kinesis::config::{AsyncSleep, SharedAsyncSleep, Sleep};
27use moka::future::Cache as MokaCache;
28use moka::ops::compute::Op;
29use phf::{Set, phf_set};
30use pulsar::authentication::oauth2::{OAuth2Authentication, OAuth2Params};
31use pulsar::{Authentication, OperationRetryOptions, Pulsar, TokioExecutor};
32use rdkafka::ClientConfig;
33use risingwave_common::bail;
34use rustls_pki_types::pem::PemObject;
35use rustls_pki_types::{CertificateDer, PrivatePkcs8KeyDer};
36use serde::Deserialize;
37use serde_with::json::JsonString;
38use serde_with::{DisplayFromStr, serde_as};
39use tempfile::NamedTempFile;
40use time::OffsetDateTime;
41use url::Url;
42use with_options::WithOptions;
43
44use crate::aws_utils::load_file_descriptor_from_s3;
45use crate::deserialize_duration_from_string;
46use crate::enforce_secret::EnforceSecret;
47use crate::error::ConnectorResult;
48use crate::sink::SinkError;
49use crate::source::nats::source::NatsOffset;
50
51pub const PRIVATE_LINK_BROKER_REWRITE_MAP_KEY: &str = "broker.rewrite.endpoints";
52pub const PRIVATE_LINK_TARGETS_KEY: &str = "privatelink.targets";
53
54const AWS_MSK_IAM_AUTH: &str = "AWS_MSK_IAM";
55
56/// The environment variable to disable using default credential from environment.
57/// It's recommended to set this variable to `true` in cloud hosting environment.
58pub const DISABLE_DEFAULT_CREDENTIAL: &str = "DISABLE_DEFAULT_CREDENTIAL";
59
60#[derive(Debug, Clone, Deserialize)]
61pub struct AwsPrivateLinkItem {
62    pub az_id: Option<String>,
63    pub port: u16,
64}
65
66use aws_config::default_provider::region::DefaultRegionChain;
67use aws_config::sts::AssumeRoleProvider;
68use aws_credential_types::provider::SharedCredentialsProvider;
69use aws_types::SdkConfig;
70use aws_types::region::Region;
71use risingwave_common::util::env_var::env_var_is_true;
72
73/// A flatten config map for aws auth.
74#[derive(Deserialize, Debug, Clone, WithOptions, PartialEq)]
75pub struct AwsAuthProps {
76    #[serde(rename = "aws.region", alias = "region", alias = "s3.region")]
77    pub region: Option<String>,
78
79    #[serde(
80        rename = "aws.endpoint_url",
81        alias = "endpoint_url",
82        alias = "endpoint",
83        alias = "s3.endpoint"
84    )]
85    pub endpoint: Option<String>,
86    #[serde(
87        rename = "aws.credentials.access_key_id",
88        alias = "access_key",
89        alias = "s3.access.key"
90    )]
91    pub access_key: Option<String>,
92    #[serde(
93        rename = "aws.credentials.secret_access_key",
94        alias = "secret_key",
95        alias = "s3.secret.key"
96    )]
97    pub secret_key: Option<String>,
98    #[serde(rename = "aws.credentials.session_token", alias = "session_token")]
99    pub session_token: Option<String>,
100    /// IAM role
101    #[serde(rename = "aws.credentials.role.arn", alias = "arn")]
102    pub arn: Option<String>,
103    /// external ID in IAM role trust policy
104    #[serde(rename = "aws.credentials.role.external_id", alias = "external_id")]
105    pub external_id: Option<String>,
106    #[serde(rename = "aws.profile", alias = "profile")]
107    pub profile: Option<String>,
108    #[serde(rename = "aws.msk.signer_timeout_sec")]
109    pub msk_signer_timeout_sec: Option<u64>,
110}
111
112impl EnforceSecret for AwsAuthProps {
113    const ENFORCE_SECRET_PROPERTIES: Set<&'static str> = phf_set! {
114        "access_key",
115        "aws.credentials.access_key_id",
116        "s3.access.key",
117        "secret_key",
118        "aws.credentials.secret_access_key",
119        "s3.secret.key",
120        "session_token",
121        "aws.credentials.session_token",
122    };
123}
124
125impl AwsAuthProps {
126    async fn build_region(&self) -> ConnectorResult<Region> {
127        if let Some(region_name) = &self.region {
128            Ok(Region::new(region_name.clone()))
129        } else {
130            let mut region_chain = DefaultRegionChain::builder();
131            if let Some(profile_name) = &self.profile {
132                region_chain = region_chain.profile_name(profile_name);
133            }
134
135            Ok(region_chain
136                .build()
137                .region()
138                .await
139                .context("region should be provided")?)
140        }
141    }
142
143    async fn build_credential_provider(&self) -> ConnectorResult<SharedCredentialsProvider> {
144        if let (Some(access_key), Some(secret_key)) =
145            (self.access_key.as_ref(), self.secret_key.as_ref())
146        {
147            Ok(SharedCredentialsProvider::new(
148                aws_credential_types::Credentials::from_keys(
149                    access_key,
150                    secret_key,
151                    self.session_token.clone(),
152                ),
153            ))
154        } else if !env_var_is_true(DISABLE_DEFAULT_CREDENTIAL) {
155            Ok(SharedCredentialsProvider::new(
156                aws_config::default_provider::credentials::default_provider().await,
157            ))
158        } else {
159            bail!("Both \"access_key\" and \"secret_key\" are required.")
160        }
161    }
162
163    async fn with_role_provider(
164        &self,
165        credential: SharedCredentialsProvider,
166    ) -> ConnectorResult<SharedCredentialsProvider> {
167        if let Some(role_name) = &self.arn {
168            let region = self.build_region().await?;
169            let mut role = AssumeRoleProvider::builder(role_name)
170                .session_name("RisingWave")
171                .region(region);
172            if let Some(id) = &self.external_id {
173                role = role.external_id(id);
174            }
175            let provider = role.build_from_provider(credential).await;
176            Ok(SharedCredentialsProvider::new(provider))
177        } else {
178            Ok(credential)
179        }
180    }
181
182    pub async fn build_config(&self) -> ConnectorResult<SdkConfig> {
183        let region = self.build_region().await?;
184        let credentials_provider = self
185            .with_role_provider(self.build_credential_provider().await?)
186            .await?;
187        let mut config_loader = aws_config::from_env()
188            .region(region)
189            .credentials_provider(credentials_provider);
190
191        if let Some(endpoint) = self.endpoint.as_ref() {
192            config_loader = config_loader.endpoint_url(endpoint);
193        }
194
195        Ok(config_loader.load().await)
196    }
197}
198
199#[serde_as]
200#[derive(Debug, Clone, Deserialize, WithOptions, PartialEq, Hash, Eq)]
201pub struct KafkaConnectionProps {
202    #[serde(rename = "properties.bootstrap.server", alias = "kafka.brokers")]
203    pub brokers: String,
204
205    /// Security protocol used for RisingWave to communicate with Kafka brokers. Could be
206    /// PLAINTEXT, SSL, `SASL_PLAINTEXT` or `SASL_SSL`.
207    #[serde(rename = "properties.security.protocol")]
208    #[with_option(allow_alter_on_fly)]
209    security_protocol: Option<String>,
210
211    #[serde(rename = "properties.ssl.endpoint.identification.algorithm")]
212    #[with_option(allow_alter_on_fly)]
213    ssl_endpoint_identification_algorithm: Option<String>,
214
215    // For the properties below, please refer to [librdkafka](https://github.com/edenhill/librdkafka/blob/master/CONFIGURATION.md) for more information.
216    /// Path to CA certificate file for verifying the broker's key.
217    #[serde(rename = "properties.ssl.ca.location")]
218    #[with_option(allow_alter_on_fly)]
219    ssl_ca_location: Option<String>,
220
221    /// CA certificate string (PEM format) for verifying the broker's key.
222    #[serde(rename = "properties.ssl.ca.pem")]
223    #[with_option(allow_alter_on_fly)]
224    ssl_ca_pem: Option<String>,
225
226    /// Path to client's certificate file (PEM).
227    #[serde(rename = "properties.ssl.certificate.location")]
228    #[with_option(allow_alter_on_fly)]
229    ssl_certificate_location: Option<String>,
230
231    /// Client's public key string (PEM format) used for authentication.
232    #[serde(rename = "properties.ssl.certificate.pem")]
233    #[with_option(allow_alter_on_fly)]
234    ssl_certificate_pem: Option<String>,
235
236    /// Path to client's private key file (PEM).
237    #[serde(rename = "properties.ssl.key.location")]
238    #[with_option(allow_alter_on_fly)]
239    ssl_key_location: Option<String>,
240
241    /// Client's private key string (PEM format) used for authentication.
242    #[serde(rename = "properties.ssl.key.pem")]
243    #[with_option(allow_alter_on_fly)]
244    ssl_key_pem: Option<String>,
245
246    /// Passphrase of client's private key.
247    #[serde(rename = "properties.ssl.key.password")]
248    #[with_option(allow_alter_on_fly)]
249    ssl_key_password: Option<String>,
250
251    /// SASL mechanism if SASL is enabled. Currently support PLAIN, SCRAM, GSSAPI, and `AWS_MSK_IAM`.
252    #[serde(rename = "properties.sasl.mechanism")]
253    #[with_option(allow_alter_on_fly)]
254    sasl_mechanism: Option<String>,
255
256    /// SASL username for SASL/PLAIN and SASL/SCRAM.
257    #[serde(rename = "properties.sasl.username")]
258    #[with_option(allow_alter_on_fly)]
259    sasl_username: Option<String>,
260
261    /// SASL password for SASL/PLAIN and SASL/SCRAM.
262    #[serde(rename = "properties.sasl.password")]
263    #[with_option(allow_alter_on_fly)]
264    sasl_password: Option<String>,
265
266    /// Kafka server's Kerberos principal name under SASL/GSSAPI, not including /hostname@REALM.
267    #[serde(rename = "properties.sasl.kerberos.service.name")]
268    sasl_kerberos_service_name: Option<String>,
269
270    /// Path to client's Kerberos keytab file under SASL/GSSAPI.
271    #[serde(rename = "properties.sasl.kerberos.keytab")]
272    sasl_kerberos_keytab: Option<String>,
273
274    /// Client's Kerberos principal name under SASL/GSSAPI.
275    #[serde(rename = "properties.sasl.kerberos.principal")]
276    sasl_kerberos_principal: Option<String>,
277
278    /// Shell command to refresh or acquire the client's Kerberos ticket under SASL/GSSAPI.
279    #[serde(rename = "properties.sasl.kerberos.kinit.cmd")]
280    sasl_kerberos_kinit_cmd: Option<String>,
281
282    /// Minimum time in milliseconds between key refresh attempts under SASL/GSSAPI.
283    #[serde(rename = "properties.sasl.kerberos.min.time.before.relogin")]
284    sasl_kerberos_min_time_before_relogin: Option<String>,
285
286    /// Configurations for SASL/OAUTHBEARER.
287    #[serde(rename = "properties.sasl.oauthbearer.config")]
288    sasl_oathbearer_config: Option<String>,
289}
290
291impl EnforceSecret for KafkaConnectionProps {
292    const ENFORCE_SECRET_PROPERTIES: Set<&'static str> = phf_set! {
293        "properties.ssl.key.pem",
294        "properties.ssl.key.password",
295        "properties.sasl.password",
296    };
297}
298
299#[serde_as]
300#[derive(Debug, Clone, Deserialize, WithOptions)]
301pub struct KafkaCommon {
302    // connection related props are moved to `KafkaConnection`
303    #[serde(rename = "topic", alias = "kafka.topic")]
304    pub topic: String,
305
306    #[serde(
307        rename = "properties.sync.call.timeout",
308        deserialize_with = "deserialize_duration_from_string",
309        default = "default_kafka_sync_call_timeout"
310    )]
311    #[with_option(allow_alter_on_fly)]
312    pub sync_call_timeout: Duration,
313}
314
315#[serde_as]
316#[derive(Debug, Clone, Deserialize, WithOptions, PartialEq, Hash, Eq)]
317pub struct KafkaPrivateLinkCommon {
318    /// This is generated from `private_link_targets` and `private_link_endpoint` in frontend, instead of given by users.
319    #[serde(rename = "broker.rewrite.endpoints")]
320    #[serde_as(as = "Option<JsonString>")]
321    pub broker_rewrite_map: Option<BTreeMap<String, String>>,
322}
323
324const fn default_kafka_sync_call_timeout() -> Duration {
325    Duration::from_secs(5)
326}
327
328const fn default_socket_keepalive_enable() -> bool {
329    true
330}
331
332#[serde_as]
333#[derive(Debug, Clone, Deserialize, WithOptions)]
334pub struct RdKafkaPropertiesCommon {
335    /// Maximum Kafka protocol request message size. Due to differing framing overhead between
336    /// protocol versions the producer is unable to reliably enforce a strict max message limit at
337    /// produce time and may exceed the maximum size by one message in protocol `ProduceRequests`,
338    /// the broker will enforce the topic's max.message.bytes limit
339    #[serde(rename = "properties.message.max.bytes")]
340    #[serde_as(as = "Option<DisplayFromStr>")]
341    #[with_option(allow_alter_on_fly)]
342    pub message_max_bytes: Option<usize>,
343
344    /// Maximum Kafka protocol response message size. This serves as a safety precaution to avoid
345    /// memory exhaustion in case of protocol hickups. This value must be at least fetch.max.bytes
346    /// + 512 to allow for protocol overhead; the value is adjusted automatically unless the
347    /// configuration property is explicitly set.
348    #[serde(rename = "properties.receive.message.max.bytes")]
349    #[serde_as(as = "Option<DisplayFromStr>")]
350    #[with_option(allow_alter_on_fly)]
351    pub receive_message_max_bytes: Option<usize>,
352
353    #[serde(rename = "properties.statistics.interval.ms")]
354    #[serde_as(as = "Option<DisplayFromStr>")]
355    #[with_option(allow_alter_on_fly)]
356    pub statistics_interval_ms: Option<usize>,
357
358    /// Client identifier
359    #[serde(rename = "properties.client.id")]
360    #[serde_as(as = "Option<DisplayFromStr>")]
361    #[with_option(allow_alter_on_fly)]
362    pub client_id: Option<String>,
363
364    #[serde(rename = "properties.enable.ssl.certificate.verification")]
365    #[serde_as(as = "Option<DisplayFromStr>")]
366    #[with_option(allow_alter_on_fly)]
367    pub enable_ssl_certificate_verification: Option<bool>,
368
369    /// Initial backoff time in milliseconds before reconnecting to a broker after a connection
370    /// closes.
371    #[serde(rename = "properties.reconnect.backoff.ms")]
372    #[serde_as(as = "Option<DisplayFromStr>")]
373    #[with_option(allow_alter_on_fly)]
374    pub reconnect_backoff_ms: Option<usize>,
375
376    /// Maximum backoff time in milliseconds before reconnecting to a broker after a connection
377    /// closes.
378    #[serde(rename = "properties.reconnect.backoff.max.ms")]
379    #[serde_as(as = "Option<DisplayFromStr>")]
380    #[with_option(allow_alter_on_fly)]
381    pub reconnect_backoff_max_ms: Option<usize>,
382
383    /// Maximum time in milliseconds allowed for broker connection setup, including TCP setup and
384    /// SSL/SASL handshakes.
385    #[serde(rename = "properties.socket.connection.setup.timeout.ms")]
386    #[serde_as(as = "Option<DisplayFromStr>")]
387    #[with_option(allow_alter_on_fly)]
388    pub socket_connection_setup_timeout_ms: Option<usize>,
389
390    #[serde(
391        rename = "properties.socket.keepalive.enable",
392        default = "default_socket_keepalive_enable"
393    )]
394    #[serde_as(as = "DisplayFromStr")]
395    pub socket_keepalive_enable: bool,
396
397    /// Initial backoff time in milliseconds before retrying a failed protocol request.
398    #[serde(rename = "properties.retry.backoff.ms")]
399    #[serde_as(as = "Option<DisplayFromStr>")]
400    #[with_option(allow_alter_on_fly)]
401    pub retry_backoff_ms: Option<usize>,
402
403    /// Maximum backoff time in milliseconds before retrying a failed protocol request.
404    #[serde(rename = "properties.retry.backoff.max.ms")]
405    #[serde_as(as = "Option<DisplayFromStr>")]
406    #[with_option(allow_alter_on_fly)]
407    pub retry_backoff_max_ms: Option<usize>,
408}
409
410impl RdKafkaPropertiesCommon {
411    pub(crate) fn set_client(&self, c: &mut rdkafka::ClientConfig) {
412        if let Some(v) = self.statistics_interval_ms {
413            c.set("statistics.interval.ms", v.to_string());
414        }
415        if let Some(v) = self.message_max_bytes {
416            c.set("message.max.bytes", v.to_string());
417        }
418        if let Some(v) = self.receive_message_max_bytes {
419            c.set("receive.message.max.bytes", v.to_string());
420        }
421        if let Some(v) = self.client_id.as_ref() {
422            c.set("client.id", v);
423        }
424        if let Some(v) = self.enable_ssl_certificate_verification {
425            c.set("enable.ssl.certificate.verification", v.to_string());
426        }
427        if let Some(v) = self.reconnect_backoff_ms {
428            c.set("reconnect.backoff.ms", v.to_string());
429        }
430        if let Some(v) = self.reconnect_backoff_max_ms {
431            c.set("reconnect.backoff.max.ms", v.to_string());
432        }
433        if let Some(v) = self.socket_connection_setup_timeout_ms {
434            c.set("socket.connection.setup.timeout.ms", v.to_string());
435        }
436        c.set(
437            "socket.keepalive.enable",
438            self.socket_keepalive_enable.to_string(),
439        );
440        if let Some(v) = self.retry_backoff_ms {
441            c.set("retry.backoff.ms", v.to_string());
442        }
443        if let Some(v) = self.retry_backoff_max_ms {
444            c.set("retry.backoff.max.ms", v.to_string());
445        }
446    }
447}
448
449impl KafkaConnectionProps {
450    #[cfg(test)]
451    pub fn test_default() -> Self {
452        Self {
453            brokers: "localhost:9092".to_owned(),
454            security_protocol: None,
455            ssl_ca_location: None,
456            ssl_certificate_location: None,
457            ssl_key_location: None,
458            ssl_ca_pem: None,
459            ssl_certificate_pem: None,
460            ssl_key_pem: None,
461            ssl_key_password: None,
462            ssl_endpoint_identification_algorithm: None,
463            sasl_mechanism: None,
464            sasl_username: None,
465            sasl_password: None,
466            sasl_kerberos_service_name: None,
467            sasl_kerberos_keytab: None,
468            sasl_kerberos_principal: None,
469            sasl_kerberos_kinit_cmd: None,
470            sasl_kerberos_min_time_before_relogin: None,
471            sasl_oathbearer_config: None,
472        }
473    }
474
475    pub(crate) fn set_security_properties(&self, config: &mut ClientConfig) {
476        // AWS_MSK_IAM
477        if self.is_aws_msk_iam() {
478            config.set("security.protocol", "SASL_SSL");
479            config.set("sasl.mechanism", "OAUTHBEARER");
480            return;
481        }
482
483        // Security protocol
484        if let Some(security_protocol) = self.security_protocol.as_ref() {
485            config.set("security.protocol", security_protocol);
486        }
487
488        // SSL
489        if let Some(ssl_ca_location) = self.ssl_ca_location.as_ref() {
490            config.set("ssl.ca.location", ssl_ca_location);
491        }
492        if let Some(ssl_ca_pem) = self.ssl_ca_pem.as_ref() {
493            config.set("ssl.ca.pem", ssl_ca_pem);
494        }
495        if let Some(ssl_certificate_location) = self.ssl_certificate_location.as_ref() {
496            config.set("ssl.certificate.location", ssl_certificate_location);
497        }
498        if let Some(ssl_certificate_pem) = self.ssl_certificate_pem.as_ref() {
499            config.set("ssl.certificate.pem", ssl_certificate_pem);
500        }
501        if let Some(ssl_key_location) = self.ssl_key_location.as_ref() {
502            config.set("ssl.key.location", ssl_key_location);
503        }
504        if let Some(ssl_key_pem) = self.ssl_key_pem.as_ref() {
505            config.set("ssl.key.pem", ssl_key_pem);
506        }
507        if let Some(ssl_key_password) = self.ssl_key_password.as_ref() {
508            config.set("ssl.key.password", ssl_key_password);
509        }
510        if let Some(ssl_endpoint_identification_algorithm) =
511            self.ssl_endpoint_identification_algorithm.as_ref()
512        {
513            // accept only `none` and `http` here, let the sdk do the check
514            config.set(
515                "ssl.endpoint.identification.algorithm",
516                ssl_endpoint_identification_algorithm,
517            );
518        }
519
520        // SASL mechanism
521        if let Some(sasl_mechanism) = self.sasl_mechanism.as_ref() {
522            config.set("sasl.mechanism", sasl_mechanism);
523        }
524
525        // SASL/PLAIN & SASL/SCRAM
526        if let Some(sasl_username) = self.sasl_username.as_ref() {
527            config.set("sasl.username", sasl_username);
528        }
529        if let Some(sasl_password) = self.sasl_password.as_ref() {
530            config.set("sasl.password", sasl_password);
531        }
532
533        // SASL/GSSAPI
534        if let Some(sasl_kerberos_service_name) = self.sasl_kerberos_service_name.as_ref() {
535            config.set("sasl.kerberos.service.name", sasl_kerberos_service_name);
536        }
537        if let Some(sasl_kerberos_keytab) = self.sasl_kerberos_keytab.as_ref() {
538            config.set("sasl.kerberos.keytab", sasl_kerberos_keytab);
539        }
540        if let Some(sasl_kerberos_principal) = self.sasl_kerberos_principal.as_ref() {
541            config.set("sasl.kerberos.principal", sasl_kerberos_principal);
542        }
543        if let Some(sasl_kerberos_kinit_cmd) = self.sasl_kerberos_kinit_cmd.as_ref() {
544            config.set("sasl.kerberos.kinit.cmd", sasl_kerberos_kinit_cmd);
545        }
546        if let Some(sasl_kerberos_min_time_before_relogin) =
547            self.sasl_kerberos_min_time_before_relogin.as_ref()
548        {
549            config.set(
550                "sasl.kerberos.min.time.before.relogin",
551                sasl_kerberos_min_time_before_relogin,
552            );
553        }
554
555        // SASL/OAUTHBEARER
556        if let Some(sasl_oathbearer_config) = self.sasl_oathbearer_config.as_ref() {
557            config.set("sasl.oauthbearer.config", sasl_oathbearer_config);
558        }
559        // Currently, we only support unsecured OAUTH.
560        config.set("enable.sasl.oauthbearer.unsecure.jwt", "true");
561    }
562
563    pub(crate) fn is_aws_msk_iam(&self) -> bool {
564        if let Some(sasl_mechanism) = self.sasl_mechanism.as_ref()
565            && sasl_mechanism == AWS_MSK_IAM_AUTH
566        {
567            true
568        } else {
569            false
570        }
571    }
572}
573
574#[derive(Clone, Debug, Deserialize, WithOptions)]
575pub struct PulsarCommon {
576    #[serde(rename = "topic", alias = "pulsar.topic")]
577    pub topic: String,
578
579    #[serde(rename = "service.url", alias = "pulsar.service.url")]
580    pub service_url: String,
581
582    #[serde(rename = "auth.token")]
583    pub auth_token: Option<String>,
584}
585
586impl EnforceSecret for PulsarCommon {
587    const ENFORCE_SECRET_PROPERTIES: Set<&'static str> = phf_set! {
588        "pulsar.auth.token",
589    };
590}
591
592#[derive(Clone, Debug, Deserialize, WithOptions)]
593pub struct PulsarOauthCommon {
594    #[serde(rename = "oauth.issuer.url")]
595    pub issuer_url: String,
596
597    #[serde(rename = "oauth.credentials.url")]
598    pub credentials_url: String,
599
600    #[serde(rename = "oauth.audience")]
601    pub audience: String,
602
603    #[serde(rename = "oauth.scope")]
604    pub scope: Option<String>,
605}
606
607fn create_credential_temp_file(credentials: &[u8]) -> std::io::Result<NamedTempFile> {
608    let mut f = NamedTempFile::new()?;
609    f.write_all(credentials)?;
610    f.as_file().sync_all()?;
611    Ok(f)
612}
613
614impl PulsarCommon {
615    pub(crate) async fn build_client(
616        &self,
617        oauth: &Option<PulsarOauthCommon>,
618        aws_auth_props: &AwsAuthProps,
619        operation_retry_options: Option<OperationRetryOptions>,
620    ) -> ConnectorResult<Pulsar<TokioExecutor>> {
621        let mut pulsar_builder = Pulsar::builder(&self.service_url, TokioExecutor);
622        let mut _temp_file = None; // Keep temp file alive
623
624        if let Some(oauth) = oauth.as_ref() {
625            let (credentials_url, temp_file) = self
626                .resolve_pulsar_credentials_url(oauth, aws_auth_props)
627                .await?;
628            _temp_file = temp_file;
629
630            let auth_params = OAuth2Params {
631                issuer_url: oauth.issuer_url.clone(),
632                credentials_url,
633                audience: Some(oauth.audience.clone()),
634                scope: oauth.scope.clone(),
635            };
636
637            pulsar_builder = pulsar_builder
638                .with_auth_provider(OAuth2Authentication::client_credentials(auth_params));
639        } else if let Some(auth_token) = &self.auth_token {
640            pulsar_builder = pulsar_builder.with_auth(Authentication {
641                name: "token".to_owned(),
642                data: Vec::from(auth_token.as_str()),
643            });
644        }
645
646        if let Some(operation_retry_options) = operation_retry_options {
647            tracing::info!(
648                max_retries = ?operation_retry_options.max_retries,
649                retry_delay_ms = operation_retry_options.retry_delay.as_millis(),
650                "applying Pulsar source operation retry override"
651            );
652            pulsar_builder = pulsar_builder.with_operation_retry_options(operation_retry_options);
653        }
654
655        let res = pulsar_builder.build().await.map_err(|e| anyhow!(e))?;
656        drop(_temp_file); // Explicitly drop temp file after client is built
657        Ok(res)
658    }
659
660    pub(crate) async fn resolve_pulsar_credentials_url(
661        &self,
662        oauth: &PulsarOauthCommon,
663        aws_auth_props: &AwsAuthProps,
664    ) -> ConnectorResult<(String, Option<NamedTempFile>)> {
665        // Try parsing as URL first
666        if let Ok(url) = Url::parse(&oauth.credentials_url) {
667            return self
668                .handle_pulsar_credentials_url(&url, aws_auth_props)
669                .await;
670        }
671
672        // If not a valid URL, check if it's an absolute file path
673        let path = Path::new(&oauth.credentials_url);
674        if !path.is_absolute() {
675            bail!("credentials_url must be a valid URL (s3://, file://) or an absolute file path");
676        }
677
678        // Verify the file exists
679        if !tokio::fs::try_exists(&oauth.credentials_url)
680            .await
681            .unwrap_or(false)
682        {
683            bail!("credentials file does not exist: {}", oauth.credentials_url);
684        }
685
686        // Return absolute path with file:// prefix
687        Ok((format!("file://{}", oauth.credentials_url), None))
688    }
689
690    pub(crate) async fn handle_pulsar_credentials_url(
691        &self,
692        url: &Url,
693        aws_auth_props: &AwsAuthProps,
694    ) -> ConnectorResult<(String, Option<NamedTempFile>)> {
695        match url.scheme() {
696            "s3" => {
697                let credentials = load_file_descriptor_from_s3(url, aws_auth_props).await?;
698                let temp_file = create_credential_temp_file(&credentials)
699                    .context("failed to create temp file for pulsar credentials")?;
700
701                let temp_path = temp_file
702                    .path()
703                    .to_str()
704                    .context("temp file path is not valid UTF-8")?;
705
706                Ok((format!("file://{}", temp_path), Some(temp_file)))
707            }
708            "file" => Ok((url.to_string(), None)),
709            _ => bail!(
710                "invalid credentials_url scheme '{}', only file://, s3://, and absolute file paths are supported",
711                url.scheme()
712            ),
713        }
714    }
715}
716
717#[serde_as]
718#[derive(Deserialize, Debug, Clone, WithOptions)]
719pub struct KinesisCommon {
720    #[serde(rename = "stream", alias = "kinesis.stream.name")]
721    pub stream_name: String,
722    #[serde(rename = "aws.region", alias = "kinesis.stream.region")]
723    pub stream_region: String,
724    #[serde(rename = "endpoint", alias = "kinesis.endpoint")]
725    pub endpoint: Option<String>,
726    #[serde(
727        rename = "aws.credentials.access_key_id",
728        alias = "kinesis.credentials.access"
729    )]
730    pub credentials_access_key: Option<String>,
731    #[serde(
732        rename = "aws.credentials.secret_access_key",
733        alias = "kinesis.credentials.secret"
734    )]
735    pub credentials_secret_access_key: Option<String>,
736    #[serde(
737        rename = "aws.credentials.session_token",
738        alias = "kinesis.credentials.session_token"
739    )]
740    pub session_token: Option<String>,
741    #[serde(rename = "aws.credentials.role.arn", alias = "kinesis.assumerole.arn")]
742    pub assume_role_arn: Option<String>,
743    #[serde(
744        rename = "aws.credentials.role.external_id",
745        alias = "kinesis.assumerole.external_id"
746    )]
747    pub assume_role_external_id: Option<String>,
748
749    // sdk options
750    #[serde(
751        rename = "kinesis.sdk.connect_timeout_ms",
752        default = "kinesis_default_connect_timeout_ms"
753    )]
754    #[serde_as(as = "DisplayFromStr")]
755    pub sdk_connect_timeout_ms: u64,
756
757    #[serde(
758        rename = "kinesis.sdk.read_timeout_ms",
759        default = "kinesis_default_read_timeout_ms"
760    )]
761    #[serde_as(as = "DisplayFromStr")]
762    pub sdk_read_timeout_ms: u64,
763
764    #[serde(
765        rename = "kinesis.sdk.operation_timeout_ms",
766        default = "kinesis_default_operation_timeout_ms"
767    )]
768    #[serde_as(as = "DisplayFromStr")]
769    pub sdk_operation_timeout_ms: u64,
770
771    #[serde(
772        rename = "kinesis.sdk.operation_attempt_timeout_ms",
773        default = "kinesis_default_operation_attempt_timeout_ms"
774    )]
775    #[serde_as(as = "DisplayFromStr")]
776    pub sdk_operation_attempt_timeout_ms: u64,
777
778    #[serde(
779        rename = "kinesis.sdk.max_retry_limit",
780        default = "kinesis_default_max_retry_limit"
781    )]
782    #[serde_as(as = "DisplayFromStr")]
783    pub sdk_max_retry_limit: u32,
784
785    #[serde(
786        rename = "kinesis.sdk.init_backoff_ms",
787        default = "kinesis_default_init_backoff_ms"
788    )]
789    #[serde_as(as = "DisplayFromStr")]
790    pub sdk_init_backoff_ms: u64,
791
792    #[serde(
793        rename = "kinesis.sdk.max_backoff_ms",
794        default = "kinesis_default_max_backoff_ms"
795    )]
796    #[serde_as(as = "DisplayFromStr")]
797    pub sdk_max_backoff_ms: u64,
798}
799
800#[derive(Debug)]
801pub struct KinesisAsyncSleepImpl;
802
803impl AsyncSleep for KinesisAsyncSleepImpl {
804    fn sleep(&self, duration: Duration) -> Sleep {
805        Sleep::new(async move { tokio::time::sleep(duration).await })
806    }
807}
808
809const fn kinesis_default_connect_timeout_ms() -> u64 {
810    10000
811}
812
813const fn kinesis_default_read_timeout_ms() -> u64 {
814    10000
815}
816
817const fn kinesis_default_operation_timeout_ms() -> u64 {
818    10000
819}
820
821const fn kinesis_default_operation_attempt_timeout_ms() -> u64 {
822    10000
823}
824
825const fn kinesis_default_init_backoff_ms() -> u64 {
826    1000
827}
828
829const fn kinesis_default_max_backoff_ms() -> u64 {
830    20000
831}
832
833const fn kinesis_default_max_retry_limit() -> u32 {
834    3
835}
836
837impl EnforceSecret for KinesisCommon {
838    const ENFORCE_SECRET_PROPERTIES: Set<&'static str> = phf_set! {
839        "kinesis.credentials.access",
840        "kinesis.credentials.secret",
841        "kinesis.credentials.session_token",
842    };
843}
844
845impl KinesisCommon {
846    pub(crate) async fn build_client(&self) -> ConnectorResult<KinesisClient> {
847        let config = AwsAuthProps {
848            region: Some(self.stream_region.clone()),
849            endpoint: self.endpoint.clone(),
850            access_key: self.credentials_access_key.clone(),
851            secret_key: self.credentials_secret_access_key.clone(),
852            session_token: self.session_token.clone(),
853            arn: self.assume_role_arn.clone(),
854            external_id: self.assume_role_external_id.clone(),
855            profile: Default::default(),
856            msk_signer_timeout_sec: Default::default(),
857        };
858        let aws_config = config.build_config().await?;
859        let mut builder = aws_sdk_kinesis::config::Builder::from(&aws_config);
860        {
861            // for timeout and retry config
862            let sleep_impl = SharedAsyncSleep::new(KinesisAsyncSleepImpl);
863            builder.set_sleep_impl(Some(sleep_impl));
864            let timeout_config = aws_smithy_types::timeout::TimeoutConfig::builder()
865                .connect_timeout(Duration::from_millis(self.sdk_connect_timeout_ms))
866                .read_timeout(Duration::from_millis(self.sdk_read_timeout_ms))
867                .operation_timeout(Duration::from_millis(self.sdk_operation_timeout_ms))
868                .operation_attempt_timeout(Duration::from_millis(
869                    self.sdk_operation_attempt_timeout_ms,
870                ))
871                .build();
872            builder.set_timeout_config(Some(timeout_config));
873
874            let retry_config = aws_smithy_types::retry::RetryConfig::standard()
875                .with_initial_backoff(Duration::from_millis(self.sdk_init_backoff_ms))
876                .with_max_backoff(Duration::from_millis(self.sdk_max_backoff_ms))
877                .with_max_attempts(self.sdk_max_retry_limit);
878            builder.set_retry_config(Some(retry_config));
879        }
880        if let Some(endpoint) = &config.endpoint {
881            builder = builder.endpoint_url(endpoint);
882        }
883        Ok(KinesisClient::from_conf(builder.build()))
884    }
885}
886
887/// Connection properties for NATS, used as a cache key for shared clients.
888/// This includes all properties that affect the connection itself (not stream/subject specific).
889#[derive(Debug, Clone, PartialEq, Eq, Hash)]
890pub struct NatsConnectionProps {
891    pub server_url: String,
892    pub connect_mode: String,
893    pub user: Option<String>,
894    pub password: Option<String>,
895    pub jwt: Option<String>,
896    pub nkey: Option<String>,
897}
898
899/// Shared NATS client cache.
900/// Client connections are cached as `Weak` pointers in the cache.
901/// NATS Connector can access this cache to reuse existing client connections,
902/// and avoid exhausting host machine ports.
903/// When reading from the cache, the connector should `upgrade` the weak pointer to an `Arc` reference.
904/// After all strong (Arc) references are dropped, the client connection will be cleaned up.
905/// Cache eviction naturally takes care of the dangling weak pointers.
906pub static SHARED_NATS_CLIENT: LazyLock<MokaCache<NatsConnectionProps, Weak<async_nats::Client>>> =
907    LazyLock::new(|| MokaCache::builder().build());
908
909#[serde_as]
910#[derive(Deserialize, Debug, Clone, WithOptions)]
911pub struct NatsCommon {
912    #[serde(rename = "server_url")]
913    pub server_url: String,
914    #[serde(rename = "subject")]
915    pub subject: String,
916    #[serde(rename = "connect_mode")]
917    pub connect_mode: String,
918    #[serde(rename = "username")]
919    pub user: Option<String>,
920    #[serde(rename = "password")]
921    pub password: Option<String>,
922    #[serde(rename = "jwt")]
923    pub jwt: Option<String>,
924    #[serde(rename = "nkey")]
925    pub nkey: Option<String>,
926    #[serde(rename = "max_bytes")]
927    #[serde_as(as = "Option<DisplayFromStr>")]
928    pub max_bytes: Option<i64>,
929    #[serde(rename = "max_messages")]
930    #[serde_as(as = "Option<DisplayFromStr>")]
931    pub max_messages: Option<i64>,
932    #[serde(rename = "max_messages_per_subject")]
933    #[serde_as(as = "Option<DisplayFromStr>")]
934    pub max_messages_per_subject: Option<i64>,
935    #[serde(rename = "max_consumers")]
936    #[serde_as(as = "Option<DisplayFromStr>")]
937    pub max_consumers: Option<i32>,
938    #[serde(rename = "max_message_size")]
939    #[serde_as(as = "Option<DisplayFromStr>")]
940    pub max_message_size: Option<i32>,
941    #[serde(rename = "allow_create_stream", default)]
942    #[serde_as(as = "DisplayFromStr")]
943    pub allow_create_stream: bool,
944}
945
946impl EnforceSecret for NatsCommon {
947    const ENFORCE_SECRET_PROPERTIES: Set<&'static str> = phf_set! {
948        "password",
949        "jwt",
950        "nkey",
951    };
952}
953
954impl NatsCommon {
955    /// Extract connection properties that can be used as a cache key.
956    pub fn connection_props(&self) -> NatsConnectionProps {
957        NatsConnectionProps {
958            server_url: self.server_url.clone(),
959            connect_mode: self.connect_mode.clone(),
960            user: self.user.clone(),
961            password: self.password.clone(),
962            jwt: self.jwt.clone(),
963            nkey: self.nkey.clone(),
964        }
965    }
966
967    /// Build a new NATS client without caching.
968    async fn build_client_inner(&self) -> ConnectorResult<async_nats::Client> {
969        let mut connect_options = async_nats::ConnectOptions::new();
970        match self.connect_mode.as_str() {
971            "user_and_password" => {
972                if let (Some(v_user), Some(v_password)) =
973                    (self.user.as_ref(), self.password.as_ref())
974                {
975                    connect_options =
976                        connect_options.user_and_password(v_user.into(), v_password.into())
977                } else {
978                    bail!("nats connect mode is user_and_password, but user or password is empty");
979                }
980            }
981
982            "credential" => {
983                if let (Some(v_nkey), Some(v_jwt)) = (self.nkey.as_ref(), self.jwt.as_ref()) {
984                    connect_options = connect_options
985                        .credentials(&self.create_credential(v_nkey, v_jwt)?)
986                        .expect("failed to parse static creds")
987                } else {
988                    bail!("nats connect mode is credential, but nkey or jwt is empty");
989                }
990            }
991            "plain" => {}
992            _ => {
993                bail!("nats connect mode only accepts user_and_password/credential/plain");
994            }
995        };
996
997        let servers = self.server_url.split(',').collect::<Vec<&str>>();
998        let client = connect_options
999            .connect(
1000                servers
1001                    .iter()
1002                    .map(|url| url.parse())
1003                    .collect::<Result<Vec<async_nats::ServerAddr>, _>>()?,
1004            )
1005            .await
1006            .context("build nats client error")
1007            .map_err(SinkError::Nats)?;
1008        Ok(client)
1009    }
1010
1011    /// Build a NATS client, attempting to reuse an existing cached client if available.
1012    /// See `SHARED_NATS_CLIENT` for more details.
1013    pub(crate) async fn build_client(&self) -> ConnectorResult<Arc<async_nats::Client>> {
1014        let connection_props = self.connection_props();
1015        let mut client: Option<Arc<async_nats::Client>> = None;
1016
1017        SHARED_NATS_CLIENT
1018            .entry_by_ref(&connection_props)
1019            .and_try_compute_with::<_, _, crate::error::ConnectorError>(|maybe_entry| async {
1020                if let Some(entry) = maybe_entry
1021                    && let entry_value = entry.into_value()
1022                    && let Some(existing_client) = entry_value.upgrade()
1023                {
1024                    match existing_client.connection_state() {
1025                        async_nats::connection::State::Connected => {
1026                            tracing::info!("reuse existing nats client for {}", self.server_url);
1027                            client = Some(existing_client);
1028                            return Ok(Op::Nop);
1029                        }
1030                        _ => {
1031                            tracing::warn!(
1032                                server_url = self.server_url,
1033                                "existing nats client is not connected",
1034                            );
1035                        }
1036                    }
1037                }
1038                tracing::info!(
1039                    server_url = self.server_url,
1040                    "no cached client, or client disconnected, building new nats client"
1041                );
1042                let new_client = Arc::new(self.build_client_inner().await?);
1043                client = Some(new_client.clone());
1044                Ok(Op::Put(Arc::downgrade(&new_client)))
1045            })
1046            .await?;
1047
1048        Ok(client.expect("client should be set"))
1049    }
1050
1051    pub(crate) async fn build_context(&self) -> ConnectorResult<jetstream::Context> {
1052        let client = self.build_client().await?;
1053        let jetstream = async_nats::jetstream::new((*client).clone());
1054        Ok(jetstream)
1055    }
1056
1057    /// Build a `JetStream` context using a pre-existing client.
1058    pub(crate) fn build_context_from_client(
1059        client: &Arc<async_nats::Client>,
1060    ) -> jetstream::Context {
1061        async_nats::jetstream::new((**client).clone())
1062    }
1063
1064    /// Build a NATS `JetStream` consumer.
1065    ///
1066    /// If `existing_client` is provided, it will be used instead of creating/fetching a new one.
1067    /// This allows callers to reuse a client they already hold.
1068    pub(crate) async fn build_consumer(
1069        &self,
1070        stream: String,
1071        durable_consumer_name: String,
1072        split_id: String,
1073        start_sequence: NatsOffset,
1074        mut config: jetstream::consumer::pull::Config,
1075        existing_client: Option<Arc<async_nats::Client>>,
1076    ) -> ConnectorResult<(
1077        async_nats::jetstream::consumer::Consumer<async_nats::jetstream::consumer::pull::Config>,
1078        Arc<async_nats::Client>,
1079    )> {
1080        let client = match existing_client {
1081            Some(c) => c,
1082            None => self.build_client().await?,
1083        };
1084        let context = Self::build_context_from_client(&client);
1085        let stream = self.build_or_get_stream(context.clone(), stream).await?;
1086        let subject_name = self
1087            .subject
1088            .replace(',', "-")
1089            .replace(['.', '>', '*', ' ', '\t'], "_");
1090        let name = format!("risingwave-consumer-{}-{}", subject_name, split_id);
1091
1092        let deliver_policy = match start_sequence {
1093            NatsOffset::Earliest => DeliverPolicy::All,
1094            NatsOffset::Latest => DeliverPolicy::New,
1095            NatsOffset::SequenceNumber(v) => {
1096                // for compatibility, we do not write to any state table now
1097                let parsed = v
1098                    .parse::<u64>()
1099                    .context("failed to parse nats offset as sequence number")?;
1100                DeliverPolicy::ByStartSequence {
1101                    start_sequence: 1 + parsed,
1102                }
1103            }
1104            NatsOffset::Timestamp(v) => DeliverPolicy::ByStartTime {
1105                start_time: OffsetDateTime::from_unix_timestamp_nanos(v as i128 * 1_000_000)
1106                    .context("invalid timestamp for nats offset")?,
1107            },
1108            NatsOffset::None => DeliverPolicy::All,
1109        };
1110
1111        let consumer = match stream.get_consumer(&name).await {
1112            Ok(consumer) => consumer,
1113            _ => {
1114                stream
1115                    .get_or_create_consumer(&name, {
1116                        config.deliver_policy = deliver_policy;
1117                        config.durable_name = Some(durable_consumer_name);
1118                        config.filter_subjects =
1119                            self.subject.split(',').map(|s| s.to_owned()).collect();
1120                        config
1121                    })
1122                    .await?
1123            }
1124        };
1125        Ok((consumer, client))
1126    }
1127
1128    pub(crate) async fn build_or_get_stream(
1129        &self,
1130        jetstream: jetstream::Context,
1131        stream_str: String,
1132    ) -> ConnectorResult<jetstream::stream::Stream> {
1133        let subjects: Vec<String> = self.subject.split(',').map(|s| s.to_owned()).collect();
1134
1135        // In `SourceEnumerator`, we may create a stream
1136        // In `SourceReader`, the desired stream MUST exist
1137        if let Ok(mut stream_instance) = jetstream.get_stream(&stream_str).await {
1138            tracing::info!(
1139                "load existing nats stream ({:?}) with config {:?}",
1140                stream_str,
1141                stream_instance.info().await?
1142            );
1143            return Ok(stream_instance);
1144        }
1145
1146        if !self.allow_create_stream {
1147            return Err(anyhow!(
1148                "stream {} not found, set `allow_create_stream` to true to create a stream",
1149                stream_str
1150            )
1151            .into());
1152        }
1153
1154        let mut config = jetstream::stream::Config {
1155            name: stream_str.clone(),
1156            max_bytes: 1000000,
1157            subjects,
1158            ..Default::default()
1159        };
1160        if let Some(v) = self.max_bytes {
1161            config.max_bytes = v;
1162        }
1163        if let Some(v) = self.max_messages {
1164            config.max_messages = v;
1165        }
1166        if let Some(v) = self.max_messages_per_subject {
1167            config.max_messages_per_subject = v;
1168        }
1169        if let Some(v) = self.max_consumers {
1170            config.max_consumers = v;
1171        }
1172        if let Some(v) = self.max_message_size {
1173            config.max_message_size = v;
1174        }
1175        tracing::info!(
1176            "create nats stream ({:?}) with config {:?}",
1177            &stream_str,
1178            config
1179        );
1180        let stream = jetstream.get_or_create_stream(config).await?;
1181        Ok(stream)
1182    }
1183
1184    pub(crate) fn create_credential(&self, seed: &str, jwt: &str) -> ConnectorResult<String> {
1185        let creds = format!(
1186            "-----BEGIN NATS USER JWT-----\n{}\n------END NATS USER JWT------\n\n\
1187                         ************************* IMPORTANT *************************\n\
1188                         NKEY Seed printed below can be used to sign and prove identity.\n\
1189                         NKEYs are sensitive and should be treated as secrets.\n\n\
1190                         -----BEGIN USER NKEY SEED-----\n{}\n------END USER NKEY SEED------\n\n\
1191                         *************************************************************",
1192            jwt, seed
1193        );
1194        Ok(creds)
1195    }
1196}
1197
1198pub(crate) fn load_certs(
1199    certificates: &str,
1200) -> ConnectorResult<Vec<rustls_pki_types::CertificateDer<'static>>> {
1201    let cert_bytes = if let Some(path) = certificates.strip_prefix("fs://") {
1202        std::fs::read_to_string(path).map(|cert| cert.as_bytes().to_owned())?
1203    } else {
1204        certificates.as_bytes().to_owned()
1205    };
1206
1207    CertificateDer::pem_slice_iter(&cert_bytes)
1208        .collect::<Result<Vec<_>, _>>()
1209        .context("Failed to parse certificates")
1210        .map_err(Into::into)
1211}
1212
1213pub(crate) fn load_private_key(
1214    certificate: &str,
1215) -> ConnectorResult<rustls_pki_types::PrivateKeyDer<'static>> {
1216    let cert_bytes = if let Some(path) = certificate.strip_prefix("fs://") {
1217        std::fs::read_to_string(path).map(|cert| cert.as_bytes().to_owned())?
1218    } else {
1219        certificate.as_bytes().to_owned()
1220    };
1221
1222    let cert = PrivatePkcs8KeyDer::pem_slice_iter(&cert_bytes)
1223        .next()
1224        .ok_or_else(|| anyhow!("No private key found"))?
1225        .context("Failed to parse private key")?;
1226    Ok(cert.into())
1227}
1228
1229#[serde_as]
1230#[derive(Deserialize, Debug, Clone, WithOptions)]
1231pub struct MongodbCommon {
1232    /// The URL of `MongoDB`
1233    #[serde(rename = "mongodb.url")]
1234    pub connect_uri: String,
1235    /// The collection name where data should be written to or read from. For sinks, the format is
1236    /// `db_name.collection_name`. Data can also be written to dynamic collections, see `collection.name.field`
1237    /// for more information.
1238    #[serde(rename = "collection.name")]
1239    pub collection_name: String,
1240}
1241
1242impl EnforceSecret for MongodbCommon {
1243    const ENFORCE_SECRET_PROPERTIES: Set<&'static str> = phf_set! {
1244        "mongodb.url"
1245    };
1246}
1247
1248impl MongodbCommon {
1249    pub(crate) async fn build_client(&self) -> ConnectorResult<mongodb::Client> {
1250        let client = mongodb::Client::with_uri_str(&self.connect_uri).await?;
1251
1252        Ok(client)
1253    }
1254}