laminar_connectors/storage/
validation.rs1use super::provider::StorageProvider;
9use super::resolver::ResolvedStorageOptions;
10
11#[derive(Debug, Clone)]
13pub struct CloudValidationResult {
14 pub errors: Vec<CloudValidationError>,
16 pub warnings: Vec<CloudValidationWarning>,
18}
19
20impl CloudValidationResult {
21 #[must_use]
23 pub fn is_valid(&self) -> bool {
24 self.errors.is_empty()
25 }
26
27 #[must_use]
29 pub fn error_message(&self) -> String {
30 self.errors
31 .iter()
32 .map(|e| e.message.as_str())
33 .collect::<Vec<_>>()
34 .join("; ")
35 }
36}
37
38#[derive(Debug, Clone)]
40pub struct CloudValidationError {
41 pub key: String,
43 pub env_var: Option<String>,
45 pub message: String,
47}
48
49#[derive(Debug, Clone)]
51pub struct CloudValidationWarning {
52 pub key: String,
54 pub message: String,
56}
57
58pub struct CloudConfigValidator;
60
61impl CloudConfigValidator {
62 #[must_use]
69 pub fn validate(resolved: &ResolvedStorageOptions) -> CloudValidationResult {
70 match resolved.provider {
71 StorageProvider::AwsS3 => Self::validate_s3(resolved),
72 StorageProvider::AzureAdls => Self::validate_azure(resolved),
73 StorageProvider::Gcs => Self::validate_gcs(resolved),
74 StorageProvider::Local => CloudValidationResult {
75 errors: Vec::new(),
76 warnings: Vec::new(),
77 },
78 }
79 }
80
81 fn validate_s3(resolved: &ResolvedStorageOptions) -> CloudValidationResult {
82 let mut errors = Vec::new();
83 let mut warnings = Vec::new();
84
85 if !resolved.options.contains_key("aws_region") {
87 errors.push(CloudValidationError {
88 key: "aws_region".into(),
89 env_var: Some("AWS_REGION".into()),
90 message: "S3 paths require 'storage.aws_region' in config \
91 or AWS_REGION environment variable"
92 .into(),
93 });
94 }
95
96 if resolved.options.contains_key("aws_access_key_id")
98 && !resolved.options.contains_key("aws_secret_access_key")
99 {
100 errors.push(CloudValidationError {
101 key: "aws_secret_access_key".into(),
102 env_var: Some("AWS_SECRET_ACCESS_KEY".into()),
103 message: "'storage.aws_access_key_id' provided without \
104 'storage.aws_secret_access_key'"
105 .into(),
106 });
107 }
108
109 if resolved.options.contains_key("aws_secret_access_key")
111 && !resolved.options.contains_key("aws_access_key_id")
112 {
113 errors.push(CloudValidationError {
114 key: "aws_access_key_id".into(),
115 env_var: Some("AWS_ACCESS_KEY_ID".into()),
116 message: "'storage.aws_secret_access_key' provided without \
117 'storage.aws_access_key_id'"
118 .into(),
119 });
120 }
121
122 if !resolved.has_credentials() {
124 warnings.push(CloudValidationWarning {
125 key: "aws_access_key_id".into(),
126 message: "No AWS credentials found in config or environment. \
127 Will fall back to instance metadata (IAM role). \
128 Set 'storage.aws_access_key_id' / \
129 'storage.aws_secret_access_key' or AWS_ACCESS_KEY_ID / \
130 AWS_SECRET_ACCESS_KEY if needed."
131 .into(),
132 });
133 }
134
135 CloudValidationResult { errors, warnings }
136 }
137
138 fn validate_azure(resolved: &ResolvedStorageOptions) -> CloudValidationResult {
139 let mut errors = Vec::new();
140 let mut warnings = Vec::new();
141
142 if !resolved.options.contains_key("azure_storage_account_name") {
144 errors.push(CloudValidationError {
145 key: "azure_storage_account_name".into(),
146 env_var: Some("AZURE_STORAGE_ACCOUNT_NAME".into()),
147 message: "Azure paths require 'storage.azure_storage_account_name' \
148 in config or AZURE_STORAGE_ACCOUNT_NAME environment variable"
149 .into(),
150 });
151 }
152
153 if !resolved.has_credentials() {
155 warnings.push(CloudValidationWarning {
156 key: "azure_storage_account_key".into(),
157 message: "No Azure credentials found in config or environment. \
158 Will fall back to Managed Identity. \
159 Set 'storage.azure_storage_account_key' / \
160 AZURE_STORAGE_ACCOUNT_KEY, or \
161 'storage.azure_storage_sas_token' / \
162 AZURE_STORAGE_SAS_TOKEN if needed."
163 .into(),
164 });
165 }
166
167 CloudValidationResult { errors, warnings }
168 }
169
170 fn validate_gcs(resolved: &ResolvedStorageOptions) -> CloudValidationResult {
171 let mut warnings = Vec::new();
172
173 if !resolved.has_credentials() {
175 warnings.push(CloudValidationWarning {
176 key: "google_service_account_path".into(),
177 message: "No GCS credentials found in config or environment. \
178 Will fall back to Application Default Credentials / \
179 Workload Identity. Set \
180 'storage.google_service_account_path' / \
181 GOOGLE_APPLICATION_CREDENTIALS if needed."
182 .into(),
183 });
184 }
185
186 CloudValidationResult {
187 errors: Vec::new(),
188 warnings,
189 }
190 }
191}
192
193#[cfg(test)]
194#[allow(clippy::disallowed_types)] mod tests {
196 use std::collections::HashMap;
197
198 use super::*;
199
200 fn make_resolved(provider: StorageProvider, keys: &[(&str, &str)]) -> ResolvedStorageOptions {
201 let mut options = HashMap::new();
202 for (k, v) in keys {
203 options.insert((*k).to_string(), (*v).to_string());
204 }
205 ResolvedStorageOptions {
206 provider,
207 options,
208 env_resolved_keys: Vec::new(),
209 }
210 }
211
212 #[test]
215 fn test_validate_s3_all_present() {
216 let resolved = make_resolved(
217 StorageProvider::AwsS3,
218 &[
219 ("aws_access_key_id", "AKID"),
220 ("aws_secret_access_key", "SECRET"),
221 ("aws_region", "us-east-1"),
222 ],
223 );
224 let result = CloudConfigValidator::validate(&resolved);
225 assert!(result.is_valid());
226 assert!(result.warnings.is_empty());
227 }
228
229 #[test]
230 fn test_validate_s3_missing_region() {
231 let resolved = make_resolved(
232 StorageProvider::AwsS3,
233 &[
234 ("aws_access_key_id", "AKID"),
235 ("aws_secret_access_key", "SECRET"),
236 ],
237 );
238 let result = CloudConfigValidator::validate(&resolved);
239 assert!(!result.is_valid());
240 assert!(result.errors.iter().any(|e| e.key == "aws_region"));
241 assert_eq!(result.errors[0].env_var.as_deref(), Some("AWS_REGION"));
242 }
243
244 #[test]
245 fn test_validate_s3_missing_credentials_warns() {
246 let resolved = make_resolved(StorageProvider::AwsS3, &[("aws_region", "us-east-1")]);
247 let result = CloudConfigValidator::validate(&resolved);
248 assert!(result.is_valid()); assert!(!result.warnings.is_empty());
250 }
251
252 #[test]
253 fn test_validate_s3_access_key_without_secret() {
254 let resolved = make_resolved(
255 StorageProvider::AwsS3,
256 &[("aws_access_key_id", "AKID"), ("aws_region", "us-east-1")],
257 );
258 let result = CloudConfigValidator::validate(&resolved);
259 assert!(!result.is_valid());
260 assert!(result
261 .errors
262 .iter()
263 .any(|e| e.key == "aws_secret_access_key"));
264 }
265
266 #[test]
267 fn test_validate_s3_secret_key_without_access() {
268 let resolved = make_resolved(
269 StorageProvider::AwsS3,
270 &[
271 ("aws_secret_access_key", "SECRET"),
272 ("aws_region", "us-east-1"),
273 ],
274 );
275 let result = CloudConfigValidator::validate(&resolved);
276 assert!(!result.is_valid());
277 assert!(result.errors.iter().any(|e| e.key == "aws_access_key_id"));
278 }
279
280 #[test]
281 fn test_validate_s3_profile_sufficient() {
282 let resolved = make_resolved(
283 StorageProvider::AwsS3,
284 &[("aws_profile", "production"), ("aws_region", "us-east-1")],
285 );
286 let result = CloudConfigValidator::validate(&resolved);
287 assert!(result.is_valid());
288 assert!(result.warnings.is_empty()); }
290
291 #[test]
294 fn test_validate_azure_all_present() {
295 let resolved = make_resolved(
296 StorageProvider::AzureAdls,
297 &[
298 ("azure_storage_account_name", "myaccount"),
299 ("azure_storage_account_key", "base64key=="),
300 ],
301 );
302 let result = CloudConfigValidator::validate(&resolved);
303 assert!(result.is_valid());
304 assert!(result.warnings.is_empty());
305 }
306
307 #[test]
308 fn test_validate_azure_missing_account_name() {
309 let resolved = make_resolved(
310 StorageProvider::AzureAdls,
311 &[("azure_storage_account_key", "base64key==")],
312 );
313 let result = CloudConfigValidator::validate(&resolved);
314 assert!(!result.is_valid());
315 assert!(result
316 .errors
317 .iter()
318 .any(|e| e.key == "azure_storage_account_name"));
319 }
320
321 #[test]
322 fn test_validate_azure_sas_token_sufficient() {
323 let resolved = make_resolved(
324 StorageProvider::AzureAdls,
325 &[
326 ("azure_storage_account_name", "myaccount"),
327 ("azure_storage_sas_token", "sv=2021-06&sig=abc"),
328 ],
329 );
330 let result = CloudConfigValidator::validate(&resolved);
331 assert!(result.is_valid());
332 assert!(result.warnings.is_empty());
333 }
334
335 #[test]
336 fn test_validate_azure_client_id_sufficient() {
337 let resolved = make_resolved(
338 StorageProvider::AzureAdls,
339 &[
340 ("azure_storage_account_name", "myaccount"),
341 ("azure_storage_client_id", "client-123"),
342 ],
343 );
344 let result = CloudConfigValidator::validate(&resolved);
345 assert!(result.is_valid());
346 }
347
348 #[test]
349 fn test_validate_azure_missing_credentials_warns() {
350 let resolved = make_resolved(
351 StorageProvider::AzureAdls,
352 &[("azure_storage_account_name", "myaccount")],
353 );
354 let result = CloudConfigValidator::validate(&resolved);
355 assert!(result.is_valid());
356 assert!(!result.warnings.is_empty());
357 }
358
359 #[test]
362 fn test_validate_gcs_all_present() {
363 let resolved = make_resolved(
364 StorageProvider::Gcs,
365 &[("google_service_account_path", "/path/to/creds.json")],
366 );
367 let result = CloudConfigValidator::validate(&resolved);
368 assert!(result.is_valid());
369 assert!(result.warnings.is_empty());
370 }
371
372 #[test]
373 fn test_validate_gcs_inline_key_sufficient() {
374 let resolved = make_resolved(
375 StorageProvider::Gcs,
376 &[(
377 "google_service_account_key",
378 "{\"type\":\"service_account\"}",
379 )],
380 );
381 let result = CloudConfigValidator::validate(&resolved);
382 assert!(result.is_valid());
383 assert!(result.warnings.is_empty());
384 }
385
386 #[test]
387 fn test_validate_gcs_missing_credentials_warns() {
388 let resolved = make_resolved(StorageProvider::Gcs, &[]);
389 let result = CloudConfigValidator::validate(&resolved);
390 assert!(result.is_valid()); assert!(!result.warnings.is_empty());
392 }
393
394 #[test]
397 fn test_validate_local_always_valid() {
398 let resolved = make_resolved(StorageProvider::Local, &[]);
399 let result = CloudConfigValidator::validate(&resolved);
400 assert!(result.is_valid());
401 assert!(result.warnings.is_empty());
402 }
403
404 #[test]
407 fn test_error_message_formatting() {
408 let resolved = make_resolved(StorageProvider::AwsS3, &[]);
409 let result = CloudConfigValidator::validate(&resolved);
410 assert!(!result.is_valid());
411 let msg = result.error_message();
412 assert!(msg.contains("aws_region"));
413 }
414
415 #[test]
416 fn test_error_includes_env_var_hint() {
417 let resolved = make_resolved(StorageProvider::AwsS3, &[]);
418 let result = CloudConfigValidator::validate(&resolved);
419 let region_err = result
420 .errors
421 .iter()
422 .find(|e| e.key == "aws_region")
423 .unwrap();
424 assert_eq!(region_err.env_var.as_deref(), Some("AWS_REGION"));
425 assert!(region_err.message.contains("AWS_REGION"));
426 }
427}