iota_config/
object_storage_config.rs1use std::{env, fs, path::PathBuf, sync::Arc, time::Duration};
6
7use anyhow::{Context, Result, anyhow, bail};
8use clap::*;
9use object_store::{ClientOptions, DynObjectStore, aws::AmazonS3Builder};
10use reqwest::header::{HeaderMap, HeaderName, HeaderValue};
11use serde::{Deserialize, Serialize};
12use tracing::info;
13
14#[derive(Debug, Copy, Clone, PartialEq, Eq, Deserialize, Serialize, ValueEnum)]
16pub enum ObjectStoreType {
17 File,
19 S3,
21 GCS,
23 Azure,
25}
26
27#[derive(Default, Debug, Clone, Deserialize, Serialize, Args)]
28#[serde(rename_all = "kebab-case")]
29pub struct ObjectStoreConfig {
30 #[serde(skip_serializing_if = "Option::is_none")]
33 #[arg(value_enum)]
34 pub object_store: Option<ObjectStoreType>,
35 #[serde(skip_serializing_if = "Option::is_none")]
37 #[arg(long)]
38 pub directory: Option<PathBuf>,
39 #[serde(skip_serializing_if = "Option::is_none")]
42 #[arg(long)]
43 pub bucket: Option<String>,
44 #[serde(skip_serializing_if = "Option::is_none")]
47 #[arg(long)]
48 pub aws_access_key_id: Option<String>,
49 #[serde(skip_serializing_if = "Option::is_none")]
52 #[arg(long)]
53 pub aws_secret_access_key: Option<String>,
54 #[serde(skip_serializing_if = "Option::is_none")]
56 #[arg(long)]
57 pub aws_endpoint: Option<String>,
58 #[serde(skip_serializing_if = "Option::is_none")]
61 #[arg(long)]
62 pub aws_region: Option<String>,
63 #[serde(skip_serializing_if = "Option::is_none")]
64 #[arg(long)]
65 pub aws_profile: Option<String>,
66 #[serde(default)]
68 #[arg(long, default_value_t = true)]
69 pub aws_virtual_hosted_style_request: bool,
70 #[serde(default)]
72 #[arg(long, default_value_t = true)]
73 pub aws_allow_http: bool,
74 #[serde(skip_serializing_if = "Option::is_none")]
77 #[arg(long)]
78 pub google_service_account: Option<String>,
79 #[serde(skip_serializing_if = "Option::is_none")]
83 #[arg(long)]
84 pub google_project_id: Option<String>,
85 #[serde(skip_serializing_if = "Option::is_none")]
88 #[arg(long)]
89 pub azure_storage_account: Option<String>,
90 #[serde(skip_serializing_if = "Option::is_none")]
93 #[arg(long)]
94 pub azure_storage_access_key: Option<String>,
95 #[serde(default = "default_object_store_connection_limit")]
96 #[arg(long, default_value_t = 20)]
97 pub object_store_connection_limit: usize,
98 #[serde(default)]
99 #[arg(long, default_value_t = false)]
100 pub no_sign_request: bool,
101}
102
103fn default_object_store_connection_limit() -> usize {
104 20
105}
106
107pub const CONNECT_TIMEOUT: Duration = Duration::from_secs(15);
109
110pub const TRANSFER_STALL_TIMEOUT: Duration = Duration::from_secs(60);
113
114fn transfer_client_options() -> ClientOptions {
117 ClientOptions::new()
118 .with_timeout_disabled()
119 .with_connect_timeout(CONNECT_TIMEOUT)
120 .with_pool_idle_timeout(Duration::from_secs(300))
121}
122
123impl ObjectStoreConfig {
124 fn new_local_fs(&self) -> Result<Arc<DynObjectStore>, anyhow::Error> {
125 info!(directory=?self.directory, object_store_type="File", "Object Store");
126 if let Some(path) = &self.directory {
127 fs::create_dir_all(path).context(anyhow!(
128 "failed to create local directory: {}",
129 path.display()
130 ))?;
131 let store = object_store::local::LocalFileSystem::new_with_prefix(path)
132 .context(anyhow!("failed to create local object store"))?;
133 Ok(Arc::new(store))
134 } else {
135 bail!("no directory provided for local fs storage");
136 }
137 }
138 fn new_s3(&self) -> Result<Arc<DynObjectStore>, anyhow::Error> {
139 use object_store::limit::LimitStore;
140
141 info!(bucket=?self.bucket, object_store_type="S3", "Object Store");
142
143 let mut builder = AmazonS3Builder::new()
144 .with_client_options(transfer_client_options())
145 .with_imdsv1_fallback();
146
147 if self.aws_virtual_hosted_style_request {
148 builder = builder.with_virtual_hosted_style_request(true);
149 }
150 if self.aws_allow_http {
151 builder = builder.with_allow_http(true);
152 }
153 if let Some(region) = &self.aws_region {
154 builder = builder.with_region(region);
155 }
156 if let Some(bucket) = &self.bucket {
157 builder = builder.with_bucket_name(bucket);
158 }
159
160 if let Some(key_id) = &self.aws_access_key_id {
161 builder = builder.with_access_key_id(key_id);
162 } else if let Ok(secret) = env::var("ARCHIVE_READ_AWS_ACCESS_KEY_ID") {
163 builder = builder.with_access_key_id(secret);
164 } else if let Ok(secret) = env::var("FORMAL_SNAPSHOT_WRITE_AWS_ACCESS_KEY_ID") {
165 builder = builder.with_access_key_id(secret);
166 } else if let Ok(secret) = env::var("DB_SNAPSHOT_READ_AWS_ACCESS_KEY_ID") {
167 builder = builder.with_access_key_id(secret);
168 }
169
170 if let Some(secret) = &self.aws_secret_access_key {
171 builder = builder.with_secret_access_key(secret);
172 } else if let Ok(secret) = env::var("ARCHIVE_READ_AWS_SECRET_ACCESS_KEY") {
173 builder = builder.with_secret_access_key(secret);
174 } else if let Ok(secret) = env::var("FORMAL_SNAPSHOT_WRITE_AWS_SECRET_ACCESS_KEY") {
175 builder = builder.with_secret_access_key(secret);
176 } else if let Ok(secret) = env::var("DB_SNAPSHOT_READ_AWS_SECRET_ACCESS_KEY") {
177 builder = builder.with_secret_access_key(secret);
178 }
179
180 if let Some(endpoint) = &self.aws_endpoint {
181 builder = builder.with_endpoint(endpoint);
182 }
183 Ok(Arc::new(LimitStore::new(
184 builder.build().context("invalid s3 config")?,
185 self.object_store_connection_limit,
186 )))
187 }
188 fn new_gcs(&self) -> Result<Arc<DynObjectStore>, anyhow::Error> {
189 use object_store::{gcp::GoogleCloudStorageBuilder, limit::LimitStore};
190
191 info!(bucket=?self.bucket, object_store_type="GCS", "Object Store");
192
193 let mut builder = GoogleCloudStorageBuilder::new();
194
195 if let Some(bucket) = &self.bucket {
196 builder = builder.with_bucket_name(bucket);
197 }
198 if let Some(account) = &self.google_service_account {
199 builder = builder.with_service_account_path(account);
200 }
201
202 let mut client_options = transfer_client_options();
203 if let Some(google_project_id) = &self.google_project_id {
204 let x_project_header = HeaderName::from_static("x-goog-user-project");
205 let iam_req_header = HeaderName::from_static("userproject");
206
207 let mut headers = HeaderMap::new();
208 headers.insert(x_project_header, HeaderValue::from_str(google_project_id)?);
209 headers.insert(iam_req_header, HeaderValue::from_str(google_project_id)?);
210 client_options = client_options.with_default_headers(headers);
211 }
212 builder = builder.with_client_options(client_options);
213
214 Ok(Arc::new(LimitStore::new(
215 builder.build().context("invalid gcs config")?,
216 self.object_store_connection_limit,
217 )))
218 }
219 fn new_azure(&self) -> Result<Arc<DynObjectStore>, anyhow::Error> {
220 use object_store::{azure::MicrosoftAzureBuilder, limit::LimitStore};
221
222 info!(bucket=?self.bucket, account=?self.azure_storage_account,
223 object_store_type="Azure", "Object Store");
224
225 let mut builder =
226 MicrosoftAzureBuilder::new().with_client_options(transfer_client_options());
227
228 if let Some(bucket) = &self.bucket {
229 builder = builder.with_container_name(bucket);
230 }
231 if let Some(account) = &self.azure_storage_account {
232 builder = builder.with_account(account)
233 }
234 if let Some(key) = &self.azure_storage_access_key {
235 builder = builder.with_access_key(key)
236 }
237
238 Ok(Arc::new(LimitStore::new(
239 builder.build().context("invalid azure config")?,
240 self.object_store_connection_limit,
241 )))
242 }
243 pub fn make(&self) -> Result<Arc<DynObjectStore>, anyhow::Error> {
244 match &self.object_store {
245 Some(ObjectStoreType::File) => self.new_local_fs(),
246 Some(ObjectStoreType::S3) => self.new_s3(),
247 Some(ObjectStoreType::GCS) => self.new_gcs(),
248 Some(ObjectStoreType::Azure) => self.new_azure(),
249 _ => bail!("at least one storage backend should be provided"),
250 }
251 }
252}