Skip to main content

tuwunel_service/storage/provider/
s3.rs

1//! S3-compatible storage-provider construction.
2//!
3//! Configuration and environment values feed the object-store S3 builder.
4//! Providers without a URL or bucket are treated as disabled, while enabled
5//! providers expose signing through the common interface.
6
7use std::{sync::Arc, time::Duration};
8
9/// Object-store transfer types used by the S3 provider boundary.
10///
11/// These re-exports match the common storage module's transfer vocabulary and
12/// avoid exposing backend-specific paths to callers.
13/// Their behavior remains defined by the object-store backend.
14pub use object_store::{GetResult, GetResultPayload, PutPayload, PutResult};
15use object_store::{aws::AmazonS3Builder, client::ClientOptions, signer::Signer};
16use tuwunel_core::{
17	Result,
18	config::{StorageProvider, StorageProviderS3},
19	debug, debug_info, error, trace,
20	version::user_agent,
21};
22
23use super::Provider;
24
25/// Builds an enabled S3-compatible provider.
26///
27/// A configuration with neither a URL nor a bucket returns `None`. Other
28/// settings override the environment-derived builder before the client and its
29/// URL signer are retained by the provider.
30#[tracing::instrument(name = "new", level = "info", skip_all, err)]
31pub(in super::super) fn new(
32	args: &crate::Args<'_>,
33	name: &str,
34	config: &StorageProviderS3,
35) -> Result<Option<(String, Arc<Provider>)>> {
36	// Fail successfully if this provider is disabled by the configuration..
37	if config.url.is_none() && config.bucket.is_none() {
38		debug!(?name, "s3_provider.bucket not set. This configuration will be skipped");
39		return Ok(None);
40	}
41
42	let mut builder = AmazonS3Builder::from_env().with_client_options(
43		ClientOptions::new()
44			.with_user_agent(user_agent().try_into()?)
45			.with_pool_max_idle_per_host(args.server.config.request_idle_per_host.into())
46			.with_pool_idle_timeout(Duration::from_secs(args.server.config.request_idle_timeout)),
47	);
48
49	if let Some(url) = config.url.clone() {
50		builder = builder.with_url(url);
51	}
52
53	if let Some(region) = config.region.clone() {
54		builder = builder.with_region(region);
55	}
56
57	if let Some(bucket) = config.bucket.clone() {
58		builder = builder.with_bucket_name(bucket);
59	}
60
61	if let Some(key) = config.key.clone() {
62		builder = builder.with_access_key_id(key);
63	}
64
65	if let Some(secret) = config.secret.clone() {
66		builder = builder.with_secret_access_key(secret);
67	}
68
69	if let Some(kms) = config.kms.clone() {
70		builder = builder.with_ssec_encryption(kms);
71	}
72
73	if let Some(token) = config.token.clone() {
74		builder = builder.with_token(token);
75	}
76
77	if let Some(endpoint) = config.endpoint.clone() {
78		builder = builder.with_endpoint(endpoint);
79	}
80
81	if let Some(use_bucket_key) = config.use_bucket_key {
82		builder = builder.with_bucket_key(use_bucket_key);
83	}
84
85	if let Some(use_https) = config.use_https {
86		builder = builder.with_allow_http(!use_https);
87	}
88
89	if let Some(use_signatures) = config.use_signatures {
90		builder = builder.with_skip_signature(!use_signatures);
91	}
92
93	if let Some(use_payload_signatures) = config.use_payload_signatures {
94		builder = builder.with_unsigned_payload(!use_payload_signatures);
95	}
96
97	if let Some(use_vhost_request) = config.use_vhost_request {
98		builder = builder.with_virtual_hosted_style_request(use_vhost_request);
99	}
100
101	trace!(?name, ?config, "Initializing S3...");
102
103	let client = builder
104		.build()
105		.inspect_err(|e| error!("Failed to configure S3 storage client: {e}"))?;
106
107	debug_info!(
108		credentials = ?client.credentials(),
109		"Started S3 storage client."
110	);
111
112	#[allow(clippy::allow_attributes, clippy::redundant_clone)] // buggy, nursery
113	let signer: Arc<dyn Signer> = Arc::new(client.clone());
114
115	let provider = Provider {
116		name: name.to_owned(),
117		base_path: config.base_path.clone().map(Into::into),
118		config: StorageProvider::s3(Box::new(config.clone())),
119		startup_check: config.startup_check,
120		services: args.services.clone(),
121		provider: Box::new(client),
122		signer: Some(signer),
123	};
124
125	Ok(Some((name.to_owned(), Arc::new(provider))))
126}