1use common_base::secrets::{ExposeSecret, SecretString};
16use common_error::ext::BoxedError;
17use object_store::services::{Azblob, Fs, Gcs, Oss, S3};
18use object_store::util::{with_instrument_layers, with_retry_layers};
19use object_store::{AzblobConnection, GcsConnection, ObjectStore, OssConnection, S3Connection};
20use paste::paste;
21use snafu::ResultExt;
22
23use crate::error::{self};
24
25trait IntoField<T> {
29 fn into_field(self) -> T;
30}
31
32impl<T> IntoField<T> for T {
34 fn into_field(self) -> T {
35 self
36 }
37}
38
39impl IntoField<SecretString> for Option<SecretString> {
41 fn into_field(self) -> SecretString {
42 self.unwrap_or_default()
43 }
44}
45
46trait FieldValidator {
52 fn is_empty(&self) -> bool;
54}
55
56impl FieldValidator for String {
58 fn is_empty(&self) -> bool {
59 self.is_empty()
60 }
61}
62
63impl FieldValidator for bool {
65 fn is_empty(&self) -> bool {
66 !self
67 }
68}
69
70impl FieldValidator for Option<String> {
72 fn is_empty(&self) -> bool {
73 self.as_ref().is_none_or(|s| s.is_empty())
74 }
75}
76
77impl FieldValidator for Option<SecretString> {
80 fn is_empty(&self) -> bool {
81 self.as_ref().is_none_or(|s| s.expose_secret().is_empty())
82 }
83}
84
85macro_rules! wrap_with_clap_prefix {
86 (
87 $new_name:ident, $prefix:literal, $enable_flag:literal, $base:ty, {
88 $( $( #[doc = $doc:expr] )? $( #[alias = $alias:literal] )? $field:ident : $type:ty $( = $default:expr )? ),* $(,)?
89 }
90 ) => {
91 paste!{
92 #[derive(clap::Parser, Debug, Clone, PartialEq, Default)]
93 pub struct $new_name {
94 $(
95 $( #[doc = $doc] )?
96 $( #[clap(alias = $alias)] )?
97 #[clap(long, requires = $enable_flag $(, default_value_t = $default )? )]
98 pub [<$prefix $field>]: $type,
99 )*
100 }
101
102 impl From<$new_name> for $base {
103 fn from(w: $new_name) -> Self {
104 Self {
105 $( $field: w.[<$prefix $field>].into_field() ),*
107 }
108 }
109 }
110 }
111 };
112}
113
114macro_rules! validate_backend {
155 (
156 enable: $enable:expr,
157 name: $backend_name:expr,
158 required: [ $( ($field:expr, $field_name:expr) ),* $(,)? ]
159 $(, custom_validator: $custom_validator:expr)?
160 ) => {{
161 if $enable {
162 let mut missing = Vec::new();
164 $(
165 if FieldValidator::is_empty($field) {
166 missing.push($field_name);
167 }
168 )*
169
170 $(
172 $custom_validator(&mut missing);
173 )?
174
175 if !missing.is_empty() {
176 return Err(BoxedError::new(
177 error::MissingConfigSnafu {
178 msg: format!(
179 "{} {} must be set when --{} is enabled.",
180 $backend_name,
181 missing.join(", "),
182 $backend_name.to_lowercase()
183 ),
184 }
185 .build(),
186 ));
187 }
188 }
189
190 Ok(())
191 }};
192}
193
194wrap_with_clap_prefix! {
195 PrefixedAzblobConnection,
196 "azblob-",
197 "enable_azblob",
198 AzblobConnection,
199 {
200 #[doc = "The container of the object store."]
201 container: String = Default::default(),
202 #[doc = "The root of the object store."]
203 root: String = Default::default(),
204 #[doc = "The account name of the object store."]
205 account_name: Option<SecretString>,
206 #[doc = "The account key of the object store."]
207 account_key: Option<SecretString>,
208 #[doc = "The endpoint of the object store."]
209 endpoint: String = Default::default(),
210 #[doc = "The SAS token of the object store."]
211 sas_token: Option<String>,
212 }
213}
214
215impl PrefixedAzblobConnection {
216 pub fn validate(&self) -> Result<(), BoxedError> {
217 validate_backend!(
218 enable: true,
219 name: "AzBlob",
220 required: [
221 (&self.azblob_container, "container"),
222 (&self.azblob_root, "root"),
223 (&self.azblob_account_name, "account name"),
224 (&self.azblob_endpoint, "endpoint"),
225 ],
226 custom_validator: |missing: &mut Vec<&str>| {
227 if self.azblob_sas_token.is_none()
229 && self.azblob_account_key.is_empty()
230 {
231 missing.push("account key (when sas_token is not provided)");
232 }
233 }
234 )
235 }
236}
237
238wrap_with_clap_prefix! {
239 PrefixedS3Connection,
240 "s3-",
241 "enable_s3",
242 S3Connection,
243 {
244 #[doc = "The bucket of the object store."]
245 bucket: String = Default::default(),
246 #[doc = "The root of the object store."]
247 root: String = Default::default(),
248 #[doc = "The access key ID of the object store."]
249 access_key_id: Option<SecretString>,
250 #[doc = "The secret access key of the object store."]
251 secret_access_key: Option<SecretString>,
252 #[doc = "The endpoint of the object store."]
253 endpoint: Option<String>,
254 #[doc = "The region of the object store."]
255 region: Option<String>,
256 #[doc = "Enable virtual host style for the object store."]
257 enable_virtual_host_style: bool = Default::default(),
258 #[doc = "Disable EC2 metadata service for the object store."]
259 disable_ec2_metadata: bool = Default::default(),
260 }
261}
262
263impl PrefixedS3Connection {
264 pub fn validate(&self) -> Result<(), BoxedError> {
265 validate_backend!(
266 enable: true,
267 name: "S3",
268 required: [
269 (&self.s3_bucket, "bucket"),
270 (&self.s3_access_key_id, "access key ID"),
271 (&self.s3_secret_access_key, "secret access key"),
272 (&self.s3_region, "region"),
273 ]
274 )
275 }
276}
277
278wrap_with_clap_prefix! {
279 PrefixedOssConnection,
280 "oss-",
281 "enable_oss",
282 OssConnection,
283 {
284 #[doc = "The bucket of the object store."]
285 bucket: String = Default::default(),
286 #[doc = "The root of the object store."]
287 root: String = Default::default(),
288 #[doc = "The access key ID of the object store."]
289 access_key_id: Option<SecretString>,
290 #[doc = "The access key secret of the object store."]
291 access_key_secret: Option<SecretString>,
292 #[doc = "The endpoint of the object store."]
293 endpoint: String = Default::default(),
294 }
295}
296
297impl PrefixedOssConnection {
298 pub fn validate(&self) -> Result<(), BoxedError> {
299 validate_backend!(
300 enable: true,
301 name: "OSS",
302 required: [
303 (&self.oss_bucket, "bucket"),
304 (&self.oss_access_key_id, "access key ID"),
305 (&self.oss_access_key_secret, "access key secret"),
306 (&self.oss_endpoint, "endpoint"),
307 ]
308 )
309 }
310}
311
312wrap_with_clap_prefix! {
313 PrefixedGcsConnection,
314 "gcs-",
315 "enable_gcs",
316 GcsConnection,
317 {
318 #[doc = "The root of the object store."]
319 root: String = Default::default(),
320 #[doc = "The bucket of the object store."]
321 bucket: String = Default::default(),
322 #[doc = "The scope of the object store."]
323 scope: String = Default::default(),
324 #[doc = "The credential path of the object store."]
325 credential_path: Option<SecretString>,
326 #[doc = "The credential of the object store."]
327 credential: Option<SecretString>,
328 #[doc = "The endpoint of the object store."]
329 endpoint: String = Default::default(),
330 }
331}
332
333impl PrefixedGcsConnection {
334 pub fn validate(&self) -> Result<(), BoxedError> {
335 validate_backend!(
336 enable: true,
337 name: "GCS",
338 required: [
339 (&self.gcs_bucket, "bucket"),
340 (&self.gcs_root, "root"),
341 (&self.gcs_scope, "scope"),
342 ]
343 )
347 }
348}
349
350#[derive(clap::Parser, Debug, Clone, PartialEq, Default)]
366#[clap(group(clap::ArgGroup::new("storage_backend").required(false).multiple(false)))]
367pub struct ObjectStoreConfig {
368 #[clap(long = "s3", group = "storage_backend")]
370 pub enable_s3: bool,
371
372 #[clap(flatten)]
373 pub s3: PrefixedS3Connection,
374
375 #[clap(long = "oss", group = "storage_backend")]
377 pub enable_oss: bool,
378
379 #[clap(flatten)]
380 pub oss: PrefixedOssConnection,
381
382 #[clap(long = "gcs", group = "storage_backend")]
384 pub enable_gcs: bool,
385
386 #[clap(flatten)]
387 pub gcs: PrefixedGcsConnection,
388
389 #[clap(long = "azblob", group = "storage_backend")]
391 pub enable_azblob: bool,
392
393 #[clap(flatten)]
394 pub azblob: PrefixedAzblobConnection,
395}
396
397pub fn new_fs_object_store(root: &str) -> std::result::Result<ObjectStore, BoxedError> {
399 let builder = Fs::default().root(root);
400 let object_store = ObjectStore::new(builder)
401 .context(error::InitBackendSnafu)
402 .map_err(BoxedError::new)?
403 .finish();
404
405 Ok(with_instrument_layers(object_store, false))
406}
407
408macro_rules! gen_object_store_builder {
409 ($method:ident, $field:ident, $conn_type:ty, $service_type:ty) => {
410 pub fn $method(&self) -> Result<ObjectStore, BoxedError> {
411 let config = <$conn_type>::from(self.$field.clone());
412 common_telemetry::info!(
413 "Building object store with {}: {:?}",
414 stringify!($field),
415 config
416 );
417 let object_store = ObjectStore::new(<$service_type>::from(&config))
418 .context(error::InitBackendSnafu)
419 .map_err(BoxedError::new)?
420 .finish();
421 Ok(with_instrument_layers(
422 with_retry_layers(object_store),
423 false,
424 ))
425 }
426 };
427}
428
429impl ObjectStoreConfig {
430 gen_object_store_builder!(build_s3, s3, S3Connection, S3);
431
432 gen_object_store_builder!(build_oss, oss, OssConnection, Oss);
433
434 gen_object_store_builder!(build_gcs, gcs, GcsConnection, Gcs);
435
436 gen_object_store_builder!(build_azblob, azblob, AzblobConnection, Azblob);
437
438 pub fn validate(&self) -> Result<(), BoxedError> {
439 if self.enable_s3 {
440 self.s3.validate()?;
441 }
442 if self.enable_oss {
443 self.oss.validate()?;
444 }
445 if self.enable_gcs {
446 self.gcs.validate()?;
447 }
448 if self.enable_azblob {
449 self.azblob.validate()?;
450 }
451 Ok(())
452 }
453
454 pub fn build(&self) -> Result<Option<ObjectStore>, BoxedError> {
456 self.validate()?;
457
458 if self.enable_s3 {
459 self.build_s3().map(Some)
460 } else if self.enable_oss {
461 self.build_oss().map(Some)
462 } else if self.enable_gcs {
463 self.build_gcs().map(Some)
464 } else if self.enable_azblob {
465 self.build_azblob().map(Some)
466 } else {
467 Ok(None)
468 }
469 }
470}