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 must 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` must be provided")
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, OAUTHBEARER, 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    #[with_option(allow_alter_on_fly)]
269    sasl_kerberos_service_name: Option<String>,
270
271    /// Path to client's Kerberos keytab file under SASL/GSSAPI.
272    #[serde(rename = "properties.sasl.kerberos.keytab")]
273    #[with_option(allow_alter_on_fly)]
274    sasl_kerberos_keytab: Option<String>,
275
276    /// Client's Kerberos principal name under SASL/GSSAPI.
277    #[serde(rename = "properties.sasl.kerberos.principal")]
278    #[with_option(allow_alter_on_fly)]
279    sasl_kerberos_principal: Option<String>,
280
281    /// Shell command to refresh or acquire the client's Kerberos ticket under SASL/GSSAPI.
282    #[serde(rename = "properties.sasl.kerberos.kinit.cmd")]
283    #[with_option(allow_alter_on_fly)]
284    sasl_kerberos_kinit_cmd: Option<String>,
285
286    /// Minimum time in milliseconds between key refresh attempts under SASL/GSSAPI.
287    #[serde(rename = "properties.sasl.kerberos.min.time.before.relogin")]
288    #[with_option(allow_alter_on_fly)]
289    sasl_kerberos_min_time_before_relogin: Option<String>,
290
291    /// Configurations for SASL/OAUTHBEARER.
292    #[serde(rename = "properties.sasl.oauthbearer.config")]
293    #[with_option(allow_alter_on_fly)]
294    sasl_oathbearer_config: Option<String>,
295
296    /// SASL/OAUTHBEARER token acquisition method. Set to "oidc" to use the OIDC client credentials flow.
297    #[serde(rename = "properties.sasl.oauthbearer.method")]
298    #[with_option(allow_alter_on_fly)]
299    sasl_oauthbearer_method: Option<String>,
300
301    /// Client ID for SASL/OAUTHBEARER OIDC provider.
302    #[serde(rename = "properties.sasl.oauthbearer.client.id")]
303    #[with_option(allow_alter_on_fly)]
304    sasl_oauthbearer_client_id: Option<String>,
305
306    /// Client secret for SASL/OAUTHBEARER OIDC provider.
307    #[serde(rename = "properties.sasl.oauthbearer.client.secret")]
308    #[with_option(allow_alter_on_fly)]
309    sasl_oauthbearer_client_secret: Option<String>,
310
311    /// OIDC token endpoint URL for SASL/OAUTHBEARER.
312    #[serde(rename = "properties.sasl.oauthbearer.token.endpoint.url")]
313    #[with_option(allow_alter_on_fly)]
314    sasl_oauthbearer_token_endpoint_url: Option<String>,
315
316    /// OIDC scope for SASL/OAUTHBEARER.
317    #[serde(rename = "properties.sasl.oauthbearer.scope")]
318    #[with_option(allow_alter_on_fly)]
319    sasl_oauthbearer_scope: Option<String>,
320
321    /// SASL/OAUTHBEARER extensions for broker-facing claims (e.g. Confluent Cloud logicalCluster, identityPoolId).
322    #[serde(rename = "properties.sasl.oauthbearer.extensions")]
323    #[with_option(allow_alter_on_fly)]
324    sasl_oauthbearer_extensions: Option<String>,
325}
326
327impl EnforceSecret for KafkaConnectionProps {
328    const ENFORCE_SECRET_PROPERTIES: Set<&'static str> = phf_set! {
329        "properties.ssl.key.pem",
330        "properties.ssl.key.password",
331        "properties.sasl.password",
332        "properties.sasl.oauthbearer.client.secret",
333    };
334}
335
336#[serde_as]
337#[derive(Debug, Clone, Deserialize, WithOptions)]
338pub struct KafkaCommon {
339    // connection related props are moved to `KafkaConnection`
340    #[serde(rename = "topic", alias = "kafka.topic")]
341    pub topic: String,
342
343    #[serde(
344        rename = "properties.sync.call.timeout",
345        deserialize_with = "deserialize_duration_from_string",
346        default = "default_kafka_sync_call_timeout"
347    )]
348    #[with_option(allow_alter_on_fly)]
349    pub sync_call_timeout: Duration,
350}
351
352#[serde_as]
353#[derive(Debug, Clone, Deserialize, WithOptions, PartialEq, Hash, Eq)]
354pub struct KafkaPrivateLinkCommon {
355    /// This is generated from `private_link_targets` and `private_link_endpoint` in frontend, instead of given by users.
356    #[serde(rename = "broker.rewrite.endpoints")]
357    #[serde_as(as = "Option<JsonString>")]
358    pub broker_rewrite_map: Option<BTreeMap<String, String>>,
359}
360
361const fn default_kafka_sync_call_timeout() -> Duration {
362    Duration::from_secs(5)
363}
364
365const fn default_socket_keepalive_enable() -> bool {
366    true
367}
368
369#[serde_as]
370#[derive(Debug, Clone, Deserialize, WithOptions)]
371pub struct RdKafkaPropertiesCommon {
372    /// Maximum Kafka protocol request message size. Due to differing framing overhead between
373    /// protocol versions the producer is unable to reliably enforce a strict max message limit at
374    /// produce time and may exceed the maximum size by one message in protocol `ProduceRequests`,
375    /// the broker will enforce the topic's max.message.bytes limit
376    #[serde(rename = "properties.message.max.bytes")]
377    #[serde_as(as = "Option<DisplayFromStr>")]
378    #[with_option(allow_alter_on_fly)]
379    pub message_max_bytes: Option<usize>,
380
381    /// Maximum Kafka protocol response message size. This serves as a safety precaution to avoid
382    /// memory exhaustion in case of protocol hickups. This value must be at least fetch.max.bytes
383    /// + 512 to allow for protocol overhead; the value is adjusted automatically unless the
384    /// configuration property is explicitly set.
385    #[serde(rename = "properties.receive.message.max.bytes")]
386    #[serde_as(as = "Option<DisplayFromStr>")]
387    #[with_option(allow_alter_on_fly)]
388    pub receive_message_max_bytes: Option<usize>,
389
390    #[serde(rename = "properties.statistics.interval.ms")]
391    #[serde_as(as = "Option<DisplayFromStr>")]
392    #[with_option(allow_alter_on_fly)]
393    pub statistics_interval_ms: Option<usize>,
394
395    /// Client identifier
396    #[serde(rename = "properties.client.id")]
397    #[serde_as(as = "Option<DisplayFromStr>")]
398    #[with_option(allow_alter_on_fly)]
399    pub client_id: Option<String>,
400
401    #[serde(rename = "properties.enable.ssl.certificate.verification")]
402    #[serde_as(as = "Option<DisplayFromStr>")]
403    #[with_option(allow_alter_on_fly)]
404    pub enable_ssl_certificate_verification: Option<bool>,
405
406    /// Initial backoff time in milliseconds before reconnecting to a broker after a connection
407    /// closes.
408    #[serde(rename = "properties.reconnect.backoff.ms")]
409    #[serde_as(as = "Option<DisplayFromStr>")]
410    #[with_option(allow_alter_on_fly)]
411    pub reconnect_backoff_ms: Option<usize>,
412
413    /// Maximum backoff time in milliseconds before reconnecting to a broker after a connection
414    /// closes.
415    #[serde(rename = "properties.reconnect.backoff.max.ms")]
416    #[serde_as(as = "Option<DisplayFromStr>")]
417    #[with_option(allow_alter_on_fly)]
418    pub reconnect_backoff_max_ms: Option<usize>,
419
420    /// Maximum time in milliseconds allowed for broker connection setup, including TCP setup and
421    /// SSL/SASL handshakes.
422    #[serde(rename = "properties.socket.connection.setup.timeout.ms")]
423    #[serde_as(as = "Option<DisplayFromStr>")]
424    #[with_option(allow_alter_on_fly)]
425    pub socket_connection_setup_timeout_ms: Option<usize>,
426
427    #[serde(
428        rename = "properties.socket.keepalive.enable",
429        default = "default_socket_keepalive_enable"
430    )]
431    #[serde_as(as = "DisplayFromStr")]
432    pub socket_keepalive_enable: bool,
433
434    /// Initial backoff time in milliseconds before retrying a failed protocol request.
435    #[serde(rename = "properties.retry.backoff.ms")]
436    #[serde_as(as = "Option<DisplayFromStr>")]
437    #[with_option(allow_alter_on_fly)]
438    pub retry_backoff_ms: Option<usize>,
439
440    /// Maximum backoff time in milliseconds before retrying a failed protocol request.
441    #[serde(rename = "properties.retry.backoff.max.ms")]
442    #[serde_as(as = "Option<DisplayFromStr>")]
443    #[with_option(allow_alter_on_fly)]
444    pub retry_backoff_max_ms: Option<usize>,
445}
446
447impl RdKafkaPropertiesCommon {
448    pub(crate) fn set_client(&self, c: &mut rdkafka::ClientConfig) {
449        if let Some(v) = self.statistics_interval_ms {
450            c.set("statistics.interval.ms", v.to_string());
451        }
452        if let Some(v) = self.message_max_bytes {
453            c.set("message.max.bytes", v.to_string());
454        }
455        if let Some(v) = self.receive_message_max_bytes {
456            c.set("receive.message.max.bytes", v.to_string());
457        }
458        if let Some(v) = self.client_id.as_ref() {
459            c.set("client.id", v);
460        }
461        if let Some(v) = self.enable_ssl_certificate_verification {
462            c.set("enable.ssl.certificate.verification", v.to_string());
463        }
464        if let Some(v) = self.reconnect_backoff_ms {
465            c.set("reconnect.backoff.ms", v.to_string());
466        }
467        if let Some(v) = self.reconnect_backoff_max_ms {
468            c.set("reconnect.backoff.max.ms", v.to_string());
469        }
470        if let Some(v) = self.socket_connection_setup_timeout_ms {
471            c.set("socket.connection.setup.timeout.ms", v.to_string());
472        }
473        c.set(
474            "socket.keepalive.enable",
475            self.socket_keepalive_enable.to_string(),
476        );
477        if let Some(v) = self.retry_backoff_ms {
478            c.set("retry.backoff.ms", v.to_string());
479        }
480        if let Some(v) = self.retry_backoff_max_ms {
481            c.set("retry.backoff.max.ms", v.to_string());
482        }
483    }
484}
485
486impl KafkaConnectionProps {
487    #[cfg(test)]
488    pub fn test_default() -> Self {
489        Self {
490            brokers: "localhost:9092".to_owned(),
491            security_protocol: None,
492            ssl_ca_location: None,
493            ssl_certificate_location: None,
494            ssl_key_location: None,
495            ssl_ca_pem: None,
496            ssl_certificate_pem: None,
497            ssl_key_pem: None,
498            ssl_key_password: None,
499            ssl_endpoint_identification_algorithm: None,
500            sasl_mechanism: None,
501            sasl_username: None,
502            sasl_password: None,
503            sasl_kerberos_service_name: None,
504            sasl_kerberos_keytab: None,
505            sasl_kerberos_principal: None,
506            sasl_kerberos_kinit_cmd: None,
507            sasl_kerberos_min_time_before_relogin: None,
508            sasl_oathbearer_config: None,
509            sasl_oauthbearer_method: None,
510            sasl_oauthbearer_client_id: None,
511            sasl_oauthbearer_client_secret: None,
512            sasl_oauthbearer_token_endpoint_url: None,
513            sasl_oauthbearer_scope: None,
514            sasl_oauthbearer_extensions: None,
515        }
516    }
517
518    pub(crate) fn set_security_properties(&self, config: &mut ClientConfig) {
519        // AWS_MSK_IAM
520        if self.is_aws_msk_iam() {
521            config.set("security.protocol", "SASL_SSL");
522            config.set("sasl.mechanism", "OAUTHBEARER");
523            return;
524        }
525
526        // Security protocol
527        if let Some(security_protocol) = self.security_protocol.as_ref() {
528            config.set("security.protocol", security_protocol);
529        }
530
531        // SSL
532        if let Some(ssl_ca_location) = self.ssl_ca_location.as_ref() {
533            config.set("ssl.ca.location", ssl_ca_location);
534        }
535        if let Some(ssl_ca_pem) = self.ssl_ca_pem.as_ref() {
536            config.set("ssl.ca.pem", ssl_ca_pem);
537        }
538        if let Some(ssl_certificate_location) = self.ssl_certificate_location.as_ref() {
539            config.set("ssl.certificate.location", ssl_certificate_location);
540        }
541        if let Some(ssl_certificate_pem) = self.ssl_certificate_pem.as_ref() {
542            config.set("ssl.certificate.pem", ssl_certificate_pem);
543        }
544        if let Some(ssl_key_location) = self.ssl_key_location.as_ref() {
545            config.set("ssl.key.location", ssl_key_location);
546        }
547        if let Some(ssl_key_pem) = self.ssl_key_pem.as_ref() {
548            config.set("ssl.key.pem", ssl_key_pem);
549        }
550        if let Some(ssl_key_password) = self.ssl_key_password.as_ref() {
551            config.set("ssl.key.password", ssl_key_password);
552        }
553        if let Some(ssl_endpoint_identification_algorithm) =
554            self.ssl_endpoint_identification_algorithm.as_ref()
555        {
556            // accept only `none` and `http` here, let the sdk do the check
557            config.set(
558                "ssl.endpoint.identification.algorithm",
559                ssl_endpoint_identification_algorithm,
560            );
561        }
562
563        // SASL mechanism
564        if let Some(sasl_mechanism) = self.sasl_mechanism.as_ref() {
565            config.set("sasl.mechanism", sasl_mechanism);
566        }
567
568        // SASL/PLAIN & SASL/SCRAM
569        if let Some(sasl_username) = self.sasl_username.as_ref() {
570            config.set("sasl.username", sasl_username);
571        }
572        if let Some(sasl_password) = self.sasl_password.as_ref() {
573            config.set("sasl.password", sasl_password);
574        }
575
576        // SASL/GSSAPI
577        if let Some(sasl_kerberos_service_name) = self.sasl_kerberos_service_name.as_ref() {
578            config.set("sasl.kerberos.service.name", sasl_kerberos_service_name);
579        }
580        if let Some(sasl_kerberos_keytab) = self.sasl_kerberos_keytab.as_ref() {
581            config.set("sasl.kerberos.keytab", sasl_kerberos_keytab);
582        }
583        if let Some(sasl_kerberos_principal) = self.sasl_kerberos_principal.as_ref() {
584            config.set("sasl.kerberos.principal", sasl_kerberos_principal);
585        }
586        if let Some(sasl_kerberos_kinit_cmd) = self.sasl_kerberos_kinit_cmd.as_ref() {
587            config.set("sasl.kerberos.kinit.cmd", sasl_kerberos_kinit_cmd);
588        }
589        if let Some(sasl_kerberos_min_time_before_relogin) =
590            self.sasl_kerberos_min_time_before_relogin.as_ref()
591        {
592            config.set(
593                "sasl.kerberos.min.time.before.relogin",
594                sasl_kerberos_min_time_before_relogin,
595            );
596        }
597
598        // SASL/OAUTHBEARER
599        if let Some(sasl_oathbearer_config) = self.sasl_oathbearer_config.as_ref() {
600            config.set("sasl.oauthbearer.config", sasl_oathbearer_config);
601        }
602        if let Some(sasl_oauthbearer_method) = self.sasl_oauthbearer_method.as_ref() {
603            config.set("sasl.oauthbearer.method", sasl_oauthbearer_method);
604        }
605        if let Some(sasl_oauthbearer_client_id) = self.sasl_oauthbearer_client_id.as_ref() {
606            config.set("sasl.oauthbearer.client.id", sasl_oauthbearer_client_id);
607        }
608        if let Some(sasl_oauthbearer_client_secret) = self.sasl_oauthbearer_client_secret.as_ref() {
609            config.set(
610                "sasl.oauthbearer.client.secret",
611                sasl_oauthbearer_client_secret,
612            );
613        }
614        if let Some(sasl_oauthbearer_token_endpoint_url) =
615            self.sasl_oauthbearer_token_endpoint_url.as_ref()
616        {
617            config.set(
618                "sasl.oauthbearer.token.endpoint.url",
619                sasl_oauthbearer_token_endpoint_url,
620            );
621        }
622        if let Some(sasl_oauthbearer_scope) = self.sasl_oauthbearer_scope.as_ref() {
623            config.set("sasl.oauthbearer.scope", sasl_oauthbearer_scope);
624        }
625        if let Some(sasl_oauthbearer_extensions) = self.sasl_oauthbearer_extensions.as_ref() {
626            config.set("sasl.oauthbearer.extensions", sasl_oauthbearer_extensions);
627        }
628        // Only enable unsecured JWT when not using OIDC method.
629        if !self.is_oauthbearer_oidc() {
630            config.set("enable.sasl.oauthbearer.unsecure.jwt", "true");
631        }
632    }
633
634    pub(crate) fn is_oauthbearer_oidc(&self) -> bool {
635        self.sasl_oauthbearer_method
636            .as_deref()
637            .is_some_and(|m| m.eq_ignore_ascii_case("oidc"))
638    }
639
640    pub(crate) fn is_aws_msk_iam(&self) -> bool {
641        if let Some(sasl_mechanism) = self.sasl_mechanism.as_ref()
642            && sasl_mechanism == AWS_MSK_IAM_AUTH
643        {
644            true
645        } else {
646            false
647        }
648    }
649}
650
651#[derive(Clone, Debug, Deserialize, WithOptions)]
652pub struct PulsarCommon {
653    #[serde(rename = "topic", alias = "pulsar.topic")]
654    pub topic: String,
655
656    #[serde(rename = "service.url", alias = "pulsar.service.url")]
657    pub service_url: String,
658
659    #[serde(rename = "auth.token")]
660    pub auth_token: Option<String>,
661}
662
663impl EnforceSecret for PulsarCommon {
664    const ENFORCE_SECRET_PROPERTIES: Set<&'static str> = phf_set! {
665        "pulsar.auth.token",
666    };
667}
668
669#[derive(Clone, Debug, Deserialize, WithOptions)]
670pub struct PulsarOauthCommon {
671    #[serde(rename = "oauth.issuer.url")]
672    pub issuer_url: String,
673
674    #[serde(rename = "oauth.credentials.url")]
675    pub credentials_url: String,
676
677    #[serde(rename = "oauth.audience")]
678    pub audience: String,
679
680    #[serde(rename = "oauth.scope")]
681    pub scope: Option<String>,
682}
683
684fn create_credential_temp_file(credentials: &[u8]) -> std::io::Result<NamedTempFile> {
685    let mut f = NamedTempFile::new()?;
686    f.write_all(credentials)?;
687    f.as_file().sync_all()?;
688    Ok(f)
689}
690
691impl PulsarCommon {
692    pub(crate) async fn build_client(
693        &self,
694        oauth: &Option<PulsarOauthCommon>,
695        aws_auth_props: &AwsAuthProps,
696        operation_retry_options: Option<OperationRetryOptions>,
697    ) -> ConnectorResult<Pulsar<TokioExecutor>> {
698        let mut pulsar_builder = Pulsar::builder(&self.service_url, TokioExecutor);
699        let mut _temp_file = None; // Keep temp file alive
700
701        if let Some(oauth) = oauth.as_ref() {
702            let (credentials_url, temp_file) = self
703                .resolve_pulsar_credentials_url(oauth, aws_auth_props)
704                .await?;
705            _temp_file = temp_file;
706
707            let auth_params = OAuth2Params {
708                issuer_url: oauth.issuer_url.clone(),
709                credentials_url,
710                audience: Some(oauth.audience.clone()),
711                scope: oauth.scope.clone(),
712            };
713
714            pulsar_builder = pulsar_builder
715                .with_auth_provider(OAuth2Authentication::client_credentials(auth_params));
716        } else if let Some(auth_token) = &self.auth_token {
717            pulsar_builder = pulsar_builder.with_auth(Authentication {
718                name: "token".to_owned(),
719                data: Vec::from(auth_token.as_str()),
720            });
721        }
722
723        if let Some(operation_retry_options) = operation_retry_options {
724            tracing::info!(
725                max_retries = ?operation_retry_options.max_retries,
726                retry_delay_ms = operation_retry_options.retry_delay.as_millis(),
727                "applying Pulsar source operation retry override"
728            );
729            pulsar_builder = pulsar_builder.with_operation_retry_options(operation_retry_options);
730        }
731
732        let res = pulsar_builder.build().await.map_err(|e| anyhow!(e))?;
733        drop(_temp_file); // Explicitly drop temp file after client is built
734        Ok(res)
735    }
736
737    pub(crate) async fn resolve_pulsar_credentials_url(
738        &self,
739        oauth: &PulsarOauthCommon,
740        aws_auth_props: &AwsAuthProps,
741    ) -> ConnectorResult<(String, Option<NamedTempFile>)> {
742        // Try parsing as URL first
743        if let Ok(url) = Url::parse(&oauth.credentials_url) {
744            return self
745                .handle_pulsar_credentials_url(&url, aws_auth_props)
746                .await;
747        }
748
749        // If not a valid URL, check if it's an absolute file path
750        let path = Path::new(&oauth.credentials_url);
751        if !path.is_absolute() {
752            bail!("credentials_url must be a valid URL (s3://, file://) or an absolute file path");
753        }
754
755        // Verify the file exists
756        if !tokio::fs::try_exists(&oauth.credentials_url)
757            .await
758            .unwrap_or(false)
759        {
760            bail!("credentials file does not exist: {}", oauth.credentials_url);
761        }
762
763        // Return absolute path with file:// prefix
764        Ok((format!("file://{}", oauth.credentials_url), None))
765    }
766
767    pub(crate) async fn handle_pulsar_credentials_url(
768        &self,
769        url: &Url,
770        aws_auth_props: &AwsAuthProps,
771    ) -> ConnectorResult<(String, Option<NamedTempFile>)> {
772        match url.scheme() {
773            "s3" => {
774                let credentials = load_file_descriptor_from_s3(url, aws_auth_props).await?;
775                let temp_file = create_credential_temp_file(&credentials)
776                    .context("failed to create temp file for pulsar credentials")?;
777
778                let temp_path = temp_file
779                    .path()
780                    .to_str()
781                    .context("temp file path is not valid UTF-8")?;
782
783                Ok((format!("file://{}", temp_path), Some(temp_file)))
784            }
785            "file" => Ok((url.to_string(), None)),
786            _ => bail!(
787                "invalid credentials_url scheme '{}', only file://, s3://, and absolute file paths are supported",
788                url.scheme()
789            ),
790        }
791    }
792}
793
794#[serde_as]
795#[derive(Deserialize, Debug, Clone, WithOptions)]
796pub struct KinesisCommon {
797    #[serde(rename = "stream", alias = "kinesis.stream.name")]
798    pub stream_name: String,
799    #[serde(rename = "aws.region", alias = "kinesis.stream.region")]
800    pub stream_region: String,
801    #[serde(rename = "endpoint", alias = "kinesis.endpoint")]
802    pub endpoint: Option<String>,
803    #[serde(
804        rename = "aws.credentials.access_key_id",
805        alias = "kinesis.credentials.access"
806    )]
807    pub credentials_access_key: Option<String>,
808    #[serde(
809        rename = "aws.credentials.secret_access_key",
810        alias = "kinesis.credentials.secret"
811    )]
812    pub credentials_secret_access_key: Option<String>,
813    #[serde(
814        rename = "aws.credentials.session_token",
815        alias = "kinesis.credentials.session_token"
816    )]
817    pub session_token: Option<String>,
818    #[serde(rename = "aws.credentials.role.arn", alias = "kinesis.assumerole.arn")]
819    pub assume_role_arn: Option<String>,
820    #[serde(
821        rename = "aws.credentials.role.external_id",
822        alias = "kinesis.assumerole.external_id"
823    )]
824    pub assume_role_external_id: Option<String>,
825
826    // sdk options
827    #[serde(
828        rename = "kinesis.sdk.connect_timeout_ms",
829        default = "kinesis_default_connect_timeout_ms"
830    )]
831    #[serde_as(as = "DisplayFromStr")]
832    pub sdk_connect_timeout_ms: u64,
833
834    #[serde(
835        rename = "kinesis.sdk.read_timeout_ms",
836        default = "kinesis_default_read_timeout_ms"
837    )]
838    #[serde_as(as = "DisplayFromStr")]
839    pub sdk_read_timeout_ms: u64,
840
841    #[serde(
842        rename = "kinesis.sdk.operation_timeout_ms",
843        default = "kinesis_default_operation_timeout_ms"
844    )]
845    #[serde_as(as = "DisplayFromStr")]
846    pub sdk_operation_timeout_ms: u64,
847
848    #[serde(
849        rename = "kinesis.sdk.operation_attempt_timeout_ms",
850        default = "kinesis_default_operation_attempt_timeout_ms"
851    )]
852    #[serde_as(as = "DisplayFromStr")]
853    pub sdk_operation_attempt_timeout_ms: u64,
854
855    #[serde(
856        rename = "kinesis.sdk.max_retry_limit",
857        default = "kinesis_default_max_retry_limit"
858    )]
859    #[serde_as(as = "DisplayFromStr")]
860    pub sdk_max_retry_limit: u32,
861
862    #[serde(
863        rename = "kinesis.sdk.init_backoff_ms",
864        default = "kinesis_default_init_backoff_ms"
865    )]
866    #[serde_as(as = "DisplayFromStr")]
867    pub sdk_init_backoff_ms: u64,
868
869    #[serde(
870        rename = "kinesis.sdk.max_backoff_ms",
871        default = "kinesis_default_max_backoff_ms"
872    )]
873    #[serde_as(as = "DisplayFromStr")]
874    pub sdk_max_backoff_ms: u64,
875}
876
877#[derive(Debug)]
878pub struct KinesisAsyncSleepImpl;
879
880impl AsyncSleep for KinesisAsyncSleepImpl {
881    fn sleep(&self, duration: Duration) -> Sleep {
882        Sleep::new(async move { tokio::time::sleep(duration).await })
883    }
884}
885
886const fn kinesis_default_connect_timeout_ms() -> u64 {
887    10000
888}
889
890const fn kinesis_default_read_timeout_ms() -> u64 {
891    10000
892}
893
894const fn kinesis_default_operation_timeout_ms() -> u64 {
895    10000
896}
897
898const fn kinesis_default_operation_attempt_timeout_ms() -> u64 {
899    10000
900}
901
902const fn kinesis_default_init_backoff_ms() -> u64 {
903    1000
904}
905
906const fn kinesis_default_max_backoff_ms() -> u64 {
907    20000
908}
909
910const fn kinesis_default_max_retry_limit() -> u32 {
911    3
912}
913
914impl EnforceSecret for KinesisCommon {
915    const ENFORCE_SECRET_PROPERTIES: Set<&'static str> = phf_set! {
916        "kinesis.credentials.access",
917        "kinesis.credentials.secret",
918        "kinesis.credentials.session_token",
919    };
920}
921
922impl KinesisCommon {
923    pub(crate) async fn build_client(&self) -> ConnectorResult<KinesisClient> {
924        let config = AwsAuthProps {
925            region: Some(self.stream_region.clone()),
926            endpoint: self.endpoint.clone(),
927            access_key: self.credentials_access_key.clone(),
928            secret_key: self.credentials_secret_access_key.clone(),
929            session_token: self.session_token.clone(),
930            arn: self.assume_role_arn.clone(),
931            external_id: self.assume_role_external_id.clone(),
932            profile: Default::default(),
933            msk_signer_timeout_sec: Default::default(),
934        };
935        let aws_config = config.build_config().await?;
936        let mut builder = aws_sdk_kinesis::config::Builder::from(&aws_config);
937        {
938            // for timeout and retry config
939            let sleep_impl = SharedAsyncSleep::new(KinesisAsyncSleepImpl);
940            builder.set_sleep_impl(Some(sleep_impl));
941            let timeout_config = aws_smithy_types::timeout::TimeoutConfig::builder()
942                .connect_timeout(Duration::from_millis(self.sdk_connect_timeout_ms))
943                .read_timeout(Duration::from_millis(self.sdk_read_timeout_ms))
944                .operation_timeout(Duration::from_millis(self.sdk_operation_timeout_ms))
945                .operation_attempt_timeout(Duration::from_millis(
946                    self.sdk_operation_attempt_timeout_ms,
947                ))
948                .build();
949            builder.set_timeout_config(Some(timeout_config));
950
951            let retry_config = aws_smithy_types::retry::RetryConfig::standard()
952                .with_initial_backoff(Duration::from_millis(self.sdk_init_backoff_ms))
953                .with_max_backoff(Duration::from_millis(self.sdk_max_backoff_ms))
954                .with_max_attempts(self.sdk_max_retry_limit);
955            builder.set_retry_config(Some(retry_config));
956        }
957        if let Some(endpoint) = &config.endpoint {
958            builder = builder.endpoint_url(endpoint);
959        }
960        Ok(KinesisClient::from_conf(builder.build()))
961    }
962}
963
964/// Connection properties for NATS, used as a cache key for shared clients.
965/// This includes all properties that affect the connection itself (not stream/subject specific).
966#[derive(Debug, Clone, PartialEq, Eq, Hash)]
967pub struct NatsConnectionProps {
968    pub server_url: String,
969    pub connect_mode: String,
970    pub user: Option<String>,
971    pub password: Option<String>,
972    pub jwt: Option<String>,
973    pub nkey: Option<String>,
974}
975
976/// Shared NATS client cache.
977/// Client connections are cached as `Weak` pointers in the cache.
978/// NATS Connector can access this cache to reuse existing client connections,
979/// and avoid exhausting host machine ports.
980/// When reading from the cache, the connector should `upgrade` the weak pointer to an `Arc` reference.
981/// After all strong (Arc) references are dropped, the client connection will be cleaned up.
982/// Cache eviction naturally takes care of the dangling weak pointers.
983pub static SHARED_NATS_CLIENT: LazyLock<MokaCache<NatsConnectionProps, Weak<async_nats::Client>>> =
984    LazyLock::new(|| MokaCache::builder().build());
985
986#[serde_as]
987#[derive(Deserialize, Debug, Clone, WithOptions)]
988pub struct NatsCommon {
989    #[serde(rename = "server_url")]
990    pub server_url: String,
991    #[serde(rename = "subject")]
992    pub subject: String,
993    #[serde(rename = "connect_mode")]
994    pub connect_mode: String,
995    #[serde(rename = "username")]
996    pub user: Option<String>,
997    #[serde(rename = "password")]
998    pub password: Option<String>,
999    #[serde(rename = "jwt")]
1000    pub jwt: Option<String>,
1001    #[serde(rename = "nkey")]
1002    pub nkey: Option<String>,
1003    #[serde(rename = "max_bytes")]
1004    #[serde_as(as = "Option<DisplayFromStr>")]
1005    pub max_bytes: Option<i64>,
1006    #[serde(rename = "max_messages")]
1007    #[serde_as(as = "Option<DisplayFromStr>")]
1008    pub max_messages: Option<i64>,
1009    #[serde(rename = "max_messages_per_subject")]
1010    #[serde_as(as = "Option<DisplayFromStr>")]
1011    pub max_messages_per_subject: Option<i64>,
1012    #[serde(rename = "max_consumers")]
1013    #[serde_as(as = "Option<DisplayFromStr>")]
1014    pub max_consumers: Option<i32>,
1015    #[serde(rename = "max_message_size")]
1016    #[serde_as(as = "Option<DisplayFromStr>")]
1017    pub max_message_size: Option<i32>,
1018    #[serde(rename = "allow_create_stream", default)]
1019    #[serde_as(as = "DisplayFromStr")]
1020    pub allow_create_stream: bool,
1021}
1022
1023impl EnforceSecret for NatsCommon {
1024    const ENFORCE_SECRET_PROPERTIES: Set<&'static str> = phf_set! {
1025        "password",
1026        "jwt",
1027        "nkey",
1028    };
1029}
1030
1031impl NatsCommon {
1032    /// Extract connection properties that can be used as a cache key.
1033    pub fn connection_props(&self) -> NatsConnectionProps {
1034        NatsConnectionProps {
1035            server_url: self.server_url.clone(),
1036            connect_mode: self.connect_mode.clone(),
1037            user: self.user.clone(),
1038            password: self.password.clone(),
1039            jwt: self.jwt.clone(),
1040            nkey: self.nkey.clone(),
1041        }
1042    }
1043
1044    /// Build a new NATS client without caching.
1045    async fn build_client_inner(&self) -> ConnectorResult<async_nats::Client> {
1046        let mut connect_options = async_nats::ConnectOptions::new();
1047        match self.connect_mode.as_str() {
1048            "user_and_password" => {
1049                if let (Some(v_user), Some(v_password)) =
1050                    (self.user.as_ref(), self.password.as_ref())
1051                {
1052                    connect_options =
1053                        connect_options.user_and_password(v_user.into(), v_password.into())
1054                } else {
1055                    bail!(
1056                        "NATS connect mode `user_and_password` requires both `user` and `password`"
1057                    );
1058                }
1059            }
1060
1061            "credential" => {
1062                if let (Some(v_nkey), Some(v_jwt)) = (self.nkey.as_ref(), self.jwt.as_ref()) {
1063                    connect_options = connect_options
1064                        .credentials(&self.create_credential(v_nkey, v_jwt)?)
1065                        .expect("failed to parse static creds")
1066                } else {
1067                    bail!("NATS connect mode `credential` requires both `nkey` and `jwt`");
1068                }
1069            }
1070            "plain" => {}
1071            _ => {
1072                bail!(
1073                    "NATS connect mode must be one of `user_and_password`, `credential`, or `plain`"
1074                );
1075            }
1076        };
1077
1078        let servers = self.server_url.split(',').collect::<Vec<&str>>();
1079        let client = connect_options
1080            .connect(
1081                servers
1082                    .iter()
1083                    .map(|url| url.parse())
1084                    .collect::<Result<Vec<async_nats::ServerAddr>, _>>()?,
1085            )
1086            .await
1087            .context("failed to build the NATS client")
1088            .map_err(SinkError::Nats)?;
1089        Ok(client)
1090    }
1091
1092    /// Build a NATS client, attempting to reuse an existing cached client if available.
1093    /// See `SHARED_NATS_CLIENT` for more details.
1094    pub(crate) async fn build_client(&self) -> ConnectorResult<Arc<async_nats::Client>> {
1095        let connection_props = self.connection_props();
1096        let mut client: Option<Arc<async_nats::Client>> = None;
1097
1098        SHARED_NATS_CLIENT
1099            .entry_by_ref(&connection_props)
1100            .and_try_compute_with::<_, _, crate::error::ConnectorError>(|maybe_entry| async {
1101                if let Some(entry) = maybe_entry
1102                    && let entry_value = entry.into_value()
1103                    && let Some(existing_client) = entry_value.upgrade()
1104                {
1105                    match existing_client.connection_state() {
1106                        async_nats::connection::State::Connected => {
1107                            tracing::info!("reusing existing NATS client for {}", self.server_url);
1108                            client = Some(existing_client);
1109                            return Ok(Op::Nop);
1110                        }
1111                        _ => {
1112                            tracing::warn!(
1113                                server_url = self.server_url,
1114                                "existing NATS client is not connected",
1115                            );
1116                        }
1117                    }
1118                }
1119                tracing::info!(
1120                    server_url = self.server_url,
1121                    "no cached NATS client was found, or the cached client disconnected; building a new client"
1122                );
1123                let new_client = Arc::new(self.build_client_inner().await?);
1124                client = Some(new_client.clone());
1125                Ok(Op::Put(Arc::downgrade(&new_client)))
1126            })
1127            .await?;
1128
1129        Ok(client.expect("client should be set"))
1130    }
1131
1132    pub(crate) async fn build_context(&self) -> ConnectorResult<jetstream::Context> {
1133        let client = self.build_client().await?;
1134        let jetstream = async_nats::jetstream::new((*client).clone());
1135        Ok(jetstream)
1136    }
1137
1138    /// Build a `JetStream` context using a pre-existing client.
1139    pub(crate) fn build_context_from_client(
1140        client: &Arc<async_nats::Client>,
1141    ) -> jetstream::Context {
1142        async_nats::jetstream::new((**client).clone())
1143    }
1144
1145    /// Build a NATS `JetStream` consumer.
1146    ///
1147    /// If `existing_client` is provided, it will be used instead of creating/fetching a new one.
1148    /// This allows callers to reuse a client they already hold.
1149    pub(crate) async fn build_consumer(
1150        &self,
1151        stream: String,
1152        durable_consumer_name: String,
1153        split_id: String,
1154        start_sequence: NatsOffset,
1155        mut config: jetstream::consumer::pull::Config,
1156        existing_client: Option<Arc<async_nats::Client>>,
1157    ) -> ConnectorResult<(
1158        async_nats::jetstream::consumer::Consumer<async_nats::jetstream::consumer::pull::Config>,
1159        Arc<async_nats::Client>,
1160    )> {
1161        let client = match existing_client {
1162            Some(c) => c,
1163            None => self.build_client().await?,
1164        };
1165        let context = Self::build_context_from_client(&client);
1166        let stream = self.build_or_get_stream(context.clone(), stream).await?;
1167        let subject_name = self
1168            .subject
1169            .replace(',', "-")
1170            .replace(['.', '>', '*', ' ', '\t'], "_");
1171        let name = format!("risingwave-consumer-{}-{}", subject_name, split_id);
1172
1173        let deliver_policy = match start_sequence {
1174            NatsOffset::Earliest => DeliverPolicy::All,
1175            NatsOffset::Latest => DeliverPolicy::New,
1176            NatsOffset::SequenceNumber(v) => {
1177                // for compatibility, we do not write to any state table now
1178                let parsed = v
1179                    .parse::<u64>()
1180                    .context("failed to parse nats offset as sequence number")?;
1181                DeliverPolicy::ByStartSequence {
1182                    start_sequence: 1 + parsed,
1183                }
1184            }
1185            NatsOffset::Timestamp(v) => DeliverPolicy::ByStartTime {
1186                start_time: OffsetDateTime::from_unix_timestamp_nanos(v as i128 * 1_000_000)
1187                    .context("invalid timestamp for nats offset")?,
1188            },
1189            NatsOffset::None => DeliverPolicy::All,
1190        };
1191
1192        let consumer = match stream.get_consumer(&name).await {
1193            Ok(consumer) => consumer,
1194            _ => {
1195                stream
1196                    .get_or_create_consumer(&name, {
1197                        config.deliver_policy = deliver_policy;
1198                        config.durable_name = Some(durable_consumer_name);
1199                        config.filter_subjects =
1200                            self.subject.split(',').map(|s| s.to_owned()).collect();
1201                        config
1202                    })
1203                    .await?
1204            }
1205        };
1206        Ok((consumer, client))
1207    }
1208
1209    pub(crate) async fn build_or_get_stream(
1210        &self,
1211        jetstream: jetstream::Context,
1212        stream_str: String,
1213    ) -> ConnectorResult<jetstream::stream::Stream> {
1214        let subjects: Vec<String> = self.subject.split(',').map(|s| s.to_owned()).collect();
1215
1216        // In `SourceEnumerator`, we may create a stream
1217        // In `SourceReader`, the desired stream MUST exist
1218        if let Ok(mut stream_instance) = jetstream.get_stream(&stream_str).await {
1219            tracing::info!(
1220                "load existing nats stream ({:?}) with config {:?}",
1221                stream_str,
1222                stream_instance.info().await?
1223            );
1224            return Ok(stream_instance);
1225        }
1226
1227        if !self.allow_create_stream {
1228            return Err(anyhow!(
1229                "stream {} not found, set `allow_create_stream` to true to create a stream",
1230                stream_str
1231            )
1232            .into());
1233        }
1234
1235        let mut config = jetstream::stream::Config {
1236            name: stream_str.clone(),
1237            max_bytes: 1000000,
1238            subjects,
1239            ..Default::default()
1240        };
1241        if let Some(v) = self.max_bytes {
1242            config.max_bytes = v;
1243        }
1244        if let Some(v) = self.max_messages {
1245            config.max_messages = v;
1246        }
1247        if let Some(v) = self.max_messages_per_subject {
1248            config.max_messages_per_subject = v;
1249        }
1250        if let Some(v) = self.max_consumers {
1251            config.max_consumers = v;
1252        }
1253        if let Some(v) = self.max_message_size {
1254            config.max_message_size = v;
1255        }
1256        tracing::info!(
1257            "create nats stream ({:?}) with config {:?}",
1258            &stream_str,
1259            config
1260        );
1261        let stream = jetstream.get_or_create_stream(config).await?;
1262        Ok(stream)
1263    }
1264
1265    pub(crate) fn create_credential(&self, seed: &str, jwt: &str) -> ConnectorResult<String> {
1266        let creds = format!(
1267            "-----BEGIN NATS USER JWT-----\n{}\n------END NATS USER JWT------\n\n\
1268                         ************************* IMPORTANT *************************\n\
1269                         NKEY Seed printed below can be used to sign and prove identity.\n\
1270                         NKEYs are sensitive and should be treated as secrets.\n\n\
1271                         -----BEGIN USER NKEY SEED-----\n{}\n------END USER NKEY SEED------\n\n\
1272                         *************************************************************",
1273            jwt, seed
1274        );
1275        Ok(creds)
1276    }
1277}
1278
1279pub(crate) fn load_certs(
1280    certificates: &str,
1281) -> ConnectorResult<Vec<rustls_pki_types::CertificateDer<'static>>> {
1282    let cert_bytes = if let Some(path) = certificates.strip_prefix("fs://") {
1283        std::fs::read_to_string(path).map(|cert| cert.as_bytes().to_owned())?
1284    } else {
1285        certificates.as_bytes().to_owned()
1286    };
1287
1288    CertificateDer::pem_slice_iter(&cert_bytes)
1289        .collect::<Result<Vec<_>, _>>()
1290        .context("failed to parse certificates")
1291        .map_err(Into::into)
1292}
1293
1294pub(crate) fn load_private_key(
1295    certificate: &str,
1296) -> ConnectorResult<rustls_pki_types::PrivateKeyDer<'static>> {
1297    let cert_bytes = if let Some(path) = certificate.strip_prefix("fs://") {
1298        std::fs::read_to_string(path).map(|cert| cert.as_bytes().to_owned())?
1299    } else {
1300        certificate.as_bytes().to_owned()
1301    };
1302
1303    let cert = PrivatePkcs8KeyDer::pem_slice_iter(&cert_bytes)
1304        .next()
1305        .ok_or_else(|| anyhow!("No private key found"))?
1306        .context("failed to parse the private key")?;
1307    Ok(cert.into())
1308}
1309
1310#[serde_as]
1311#[derive(Deserialize, Debug, Clone, WithOptions)]
1312pub struct MongodbCommon {
1313    /// The URL of `MongoDB`
1314    #[serde(rename = "mongodb.url")]
1315    pub connect_uri: String,
1316    /// The collection name where data should be written to or read from. For sinks, the format is
1317    /// `db_name.collection_name`. Data can also be written to dynamic collections, see `collection.name.field`
1318    /// for more information.
1319    #[serde(rename = "collection.name")]
1320    pub collection_name: String,
1321}
1322
1323impl EnforceSecret for MongodbCommon {
1324    const ENFORCE_SECRET_PROPERTIES: Set<&'static str> = phf_set! {
1325        "mongodb.url"
1326    };
1327}
1328
1329impl MongodbCommon {
1330    pub(crate) async fn build_client(&self) -> ConnectorResult<mongodb::Client> {
1331        let client = mongodb::Client::with_uri_str(&self.connect_uri).await?;
1332
1333        Ok(client)
1334    }
1335}
1336
1337/// TCP keepalive knobs for long-lived Postgres clients, see `create_pg_client`.
1338#[serde_as]
1339#[derive(Debug, Clone, Deserialize, WithOptions)]
1340pub struct TcpKeepaliveConfig {
1341    #[serde(rename = "tcp.keepalive.idle", default = "default_tcp_keepalive_idle")]
1342    #[serde_as(as = "DisplayFromStr")]
1343    pub tcp_keepalive_idle: u32,
1344    #[serde(
1345        rename = "tcp.keepalive.interval",
1346        default = "default_tcp_keepalive_interval"
1347    )]
1348    #[serde_as(as = "DisplayFromStr")]
1349    pub tcp_keepalive_interval: u32,
1350    #[serde(
1351        rename = "tcp.keepalive.count",
1352        default = "default_tcp_keepalive_count"
1353    )]
1354    #[serde_as(as = "DisplayFromStr")]
1355    pub tcp_keepalive_count: u32,
1356}
1357
1358const fn default_tcp_keepalive_idle() -> u32 {
1359    10 * 60
1360}
1361
1362const fn default_tcp_keepalive_interval() -> u32 {
1363    10
1364}
1365
1366const fn default_tcp_keepalive_count() -> u32 {
1367    3
1368}
1369
1370#[cfg(all(test, not(madsim)))]
1371mod tests {
1372    use super::*;
1373
1374    #[test]
1375    fn test_oauthbearer_oidc_does_not_set_unsecure_jwt() {
1376        let mut props = KafkaConnectionProps::test_default();
1377        props.sasl_mechanism = Some("OAUTHBEARER".to_owned());
1378        props.sasl_oauthbearer_method = Some("oidc".to_owned());
1379        props.sasl_oauthbearer_client_id = Some("my-client".to_owned());
1380        props.sasl_oauthbearer_client_secret = Some("my-secret".to_owned());
1381        props.sasl_oauthbearer_token_endpoint_url =
1382            Some("https://idp.example.com/token".to_owned());
1383        props.sasl_oauthbearer_scope = Some("kafka".to_owned());
1384
1385        let mut config = rdkafka::ClientConfig::new();
1386        props.set_security_properties(&mut config);
1387
1388        let map = config.config_map();
1389        assert_eq!(map.get("sasl.mechanism").unwrap(), "OAUTHBEARER");
1390        assert_eq!(map.get("sasl.oauthbearer.method").unwrap(), "oidc");
1391        assert_eq!(map.get("sasl.oauthbearer.client.id").unwrap(), "my-client");
1392        assert_eq!(
1393            map.get("sasl.oauthbearer.client.secret").unwrap(),
1394            "my-secret"
1395        );
1396        assert_eq!(
1397            map.get("sasl.oauthbearer.token.endpoint.url").unwrap(),
1398            "https://idp.example.com/token"
1399        );
1400        assert_eq!(map.get("sasl.oauthbearer.scope").unwrap(), "kafka");
1401        assert!(
1402            !map.contains_key("enable.sasl.oauthbearer.unsecure.jwt"),
1403            "unsecure JWT must not be set when method=oidc"
1404        );
1405    }
1406
1407    #[test]
1408    fn test_default_oauthbearer_sets_unsecure_jwt() {
1409        let mut props = KafkaConnectionProps::test_default();
1410        props.sasl_mechanism = Some("OAUTHBEARER".to_owned());
1411        props.sasl_oathbearer_config = Some("principal=user".to_owned());
1412
1413        let mut config = rdkafka::ClientConfig::new();
1414        props.set_security_properties(&mut config);
1415
1416        let map = config.config_map();
1417        assert_eq!(map.get("sasl.mechanism").unwrap(), "OAUTHBEARER");
1418        assert_eq!(
1419            map.get("enable.sasl.oauthbearer.unsecure.jwt").unwrap(),
1420            "true"
1421        );
1422        assert!(!map.contains_key("sasl.oauthbearer.method"));
1423    }
1424
1425    #[test]
1426    fn test_aws_msk_iam_unaffected_by_oidc() {
1427        let mut props = KafkaConnectionProps::test_default();
1428        props.sasl_mechanism = Some("AWS_MSK_IAM".to_owned());
1429
1430        let mut config = rdkafka::ClientConfig::new();
1431        props.set_security_properties(&mut config);
1432
1433        let map = config.config_map();
1434        // MSK IAM takes the early-return path
1435        assert_eq!(map.get("security.protocol").unwrap(), "SASL_SSL");
1436        assert_eq!(map.get("sasl.mechanism").unwrap(), "OAUTHBEARER");
1437        // Should not have any OIDC or unsecure JWT settings
1438        assert!(!map.contains_key("sasl.oauthbearer.method"));
1439        assert!(!map.contains_key("enable.sasl.oauthbearer.unsecure.jwt"));
1440    }
1441
1442    #[test]
1443    fn test_oauthbearer_oidc_with_extensions() {
1444        let mut props = KafkaConnectionProps::test_default();
1445        props.sasl_mechanism = Some("OAUTHBEARER".to_owned());
1446        props.sasl_oauthbearer_method = Some("oidc".to_owned());
1447        props.sasl_oauthbearer_client_id = Some("client".to_owned());
1448        props.sasl_oauthbearer_client_secret = Some("".to_owned());
1449        props.sasl_oauthbearer_token_endpoint_url =
1450            Some("https://idp.example.com/token".to_owned());
1451        props.sasl_oauthbearer_extensions =
1452            Some("logicalCluster=lkc-abc,identityPoolId=pool-xyz".to_owned());
1453
1454        let mut config = rdkafka::ClientConfig::new();
1455        props.set_security_properties(&mut config);
1456
1457        let map = config.config_map();
1458        assert_eq!(
1459            map.get("sasl.oauthbearer.extensions").unwrap(),
1460            "logicalCluster=lkc-abc,identityPoolId=pool-xyz"
1461        );
1462    }
1463}