1use std::sync::Arc;
12
13use async_trait::async_trait;
14use http::{Method, StatusCode};
15use parking_lot::RwLockWriteGuard;
16use percent_encoding::percent_decode_str;
17use serde_json::{json, Map, Value};
18use tokio::sync::Mutex as AsyncMutex;
19use uuid::Uuid;
20
21use fakecloud_core::multi_account::MultiAccountState;
22use fakecloud_core::service::{AwsRequest, AwsResponse, AwsService, AwsServiceError};
23use fakecloud_persistence::SnapshotStore;
24
25use crate::persistence::save_snapshot;
26use crate::state::{EfsData, SharedEfsState};
27
28pub const EFS_ACTIONS: &[&str] = &[
30 "CreateAccessPoint",
31 "CreateFileSystem",
32 "CreateMountTarget",
33 "CreateReplicationConfiguration",
34 "CreateTags",
35 "DeleteAccessPoint",
36 "DeleteFileSystem",
37 "DeleteFileSystemPolicy",
38 "DeleteMountTarget",
39 "DeleteReplicationConfiguration",
40 "DeleteTags",
41 "DescribeAccessPoints",
42 "DescribeAccountPreferences",
43 "DescribeBackupPolicy",
44 "DescribeFileSystemPolicy",
45 "DescribeFileSystems",
46 "DescribeLifecycleConfiguration",
47 "DescribeMountTargetSecurityGroups",
48 "DescribeMountTargets",
49 "DescribeReplicationConfigurations",
50 "DescribeTags",
51 "ListTagsForResource",
52 "ModifyMountTargetSecurityGroups",
53 "PutAccountPreferences",
54 "PutBackupPolicy",
55 "PutFileSystemPolicy",
56 "PutLifecycleConfiguration",
57 "TagResource",
58 "UntagResource",
59 "UpdateFileSystem",
60 "UpdateFileSystemProtection",
61];
62
63const MUTATING: &[&str] = &[
65 "CreateAccessPoint",
66 "CreateFileSystem",
67 "CreateMountTarget",
68 "CreateReplicationConfiguration",
69 "CreateTags",
70 "DeleteAccessPoint",
71 "DeleteFileSystem",
72 "DeleteFileSystemPolicy",
73 "DeleteMountTarget",
74 "DeleteReplicationConfiguration",
75 "DeleteTags",
76 "ModifyMountTargetSecurityGroups",
77 "PutAccountPreferences",
78 "PutBackupPolicy",
79 "PutFileSystemPolicy",
80 "PutLifecycleConfiguration",
81 "TagResource",
82 "UntagResource",
83 "UpdateFileSystem",
84 "UpdateFileSystemProtection",
85];
86
87pub struct EfsService {
88 state: SharedEfsState,
89 snapshot_store: Option<Arc<dyn SnapshotStore>>,
90 snapshot_lock: Arc<AsyncMutex<()>>,
91 ec2_state: Option<fakecloud_ec2::SharedEc2State>,
96}
97
98impl EfsService {
99 pub fn new(state: SharedEfsState) -> Self {
100 Self {
101 state,
102 snapshot_store: None,
103 snapshot_lock: Arc::new(AsyncMutex::new(())),
104 ec2_state: None,
105 }
106 }
107
108 pub fn with_snapshot_store(mut self, store: Arc<dyn SnapshotStore>) -> Self {
109 self.snapshot_store = Some(store);
110 self
111 }
112
113 pub fn with_ec2_state(mut self, ec2_state: fakecloud_ec2::SharedEc2State) -> Self {
114 self.ec2_state = Some(ec2_state);
115 self
116 }
117
118 pub fn snapshot_hook(&self) -> Option<fakecloud_persistence::SnapshotHook> {
124 let store = self.snapshot_store.clone()?;
125 let state = self.state.clone();
126 let lock = self.snapshot_lock.clone();
127 Some(Arc::new(move || {
128 let state = state.clone();
129 let store = store.clone();
130 let lock = lock.clone();
131 Box::pin(async move {
132 save_snapshot(&state, Some(store), &lock).await;
133 })
134 }))
135 }
136
137 fn resolve_subnet(&self, account: &str, subnet_id: &str) -> Option<(String, String, String)> {
142 let ec2 = self.ec2_state.as_ref()?;
143 let guard = ec2.read();
144 let subnet = guard.get(account)?.subnets.get(subnet_id)?;
145 Some((
146 subnet.availability_zone.clone(),
147 subnet.availability_zone_id.clone(),
148 subnet.vpc_id.clone(),
149 ))
150 }
151
152 async fn save(&self) {
153 save_snapshot(
154 &self.state,
155 self.snapshot_store.clone(),
156 &self.snapshot_lock,
157 )
158 .await;
159 }
160
161 fn resolve_action(req: &AwsRequest) -> Option<(&'static str, Option<String>)> {
164 let raw = req.raw_path.split('?').next().unwrap_or(&req.raw_path);
165 let trimmed = raw.strip_prefix('/').unwrap_or(raw);
166 let segs: Vec<String> = if trimmed.is_empty() {
172 Vec::new()
173 } else {
174 trimmed
175 .split('/')
176 .map(|s| percent_decode_str(s).decode_utf8_lossy().into_owned())
177 .collect()
178 };
179 let s: Vec<&str> = segs.iter().map(String::as_str).collect();
180 let m = &req.method;
181 let get = m == Method::GET;
182 let post = m == Method::POST;
183 let put = m == Method::PUT;
184 let del = m == Method::DELETE;
185 const V: &str = "2015-02-01";
186 let (action, label): (&'static str, Option<String>) = match s.as_slice() {
187 [v, "access-points"] if *v == V && post => ("CreateAccessPoint", None),
189 [v, "access-points"] if *v == V && get => ("DescribeAccessPoints", None),
190 [v, "access-points", id] if *v == V && del => {
191 ("DeleteAccessPoint", Some((*id).to_string()))
192 }
193 [v, "mount-targets"] if *v == V && post => ("CreateMountTarget", None),
195 [v, "mount-targets"] if *v == V && get => ("DescribeMountTargets", None),
196 [v, "mount-targets", id] if *v == V && del => {
197 ("DeleteMountTarget", Some((*id).to_string()))
198 }
199 [v, "mount-targets", id, "security-groups"] if *v == V && get => {
200 ("DescribeMountTargetSecurityGroups", Some((*id).to_string()))
201 }
202 [v, "mount-targets", id, "security-groups"] if *v == V && put => {
203 ("ModifyMountTargetSecurityGroups", Some((*id).to_string()))
204 }
205 [v, "account-preferences"] if *v == V && get => ("DescribeAccountPreferences", None),
207 [v, "account-preferences"] if *v == V && put => ("PutAccountPreferences", None),
208 [v, "file-systems"] if *v == V && post => ("CreateFileSystem", None),
210 [v, "file-systems"] if *v == V && get => ("DescribeFileSystems", None),
211 [v, "file-systems", "replication-configurations"] if *v == V && get => {
212 ("DescribeReplicationConfigurations", None)
213 }
214 [v, "file-systems", id, "replication-configuration"] if *v == V && post => {
215 ("CreateReplicationConfiguration", Some((*id).to_string()))
216 }
217 [v, "file-systems", id, "replication-configuration"] if *v == V && del => {
218 ("DeleteReplicationConfiguration", Some((*id).to_string()))
219 }
220 [v, "file-systems", id, "policy"] if *v == V && get => {
221 ("DescribeFileSystemPolicy", Some((*id).to_string()))
222 }
223 [v, "file-systems", id, "policy"] if *v == V && put => {
224 ("PutFileSystemPolicy", Some((*id).to_string()))
225 }
226 [v, "file-systems", id, "policy"] if *v == V && del => {
227 ("DeleteFileSystemPolicy", Some((*id).to_string()))
228 }
229 [v, "file-systems", id, "backup-policy"] if *v == V && get => {
230 ("DescribeBackupPolicy", Some((*id).to_string()))
231 }
232 [v, "file-systems", id, "backup-policy"] if *v == V && put => {
233 ("PutBackupPolicy", Some((*id).to_string()))
234 }
235 [v, "file-systems", id, "lifecycle-configuration"] if *v == V && get => {
236 ("DescribeLifecycleConfiguration", Some((*id).to_string()))
237 }
238 [v, "file-systems", id, "lifecycle-configuration"] if *v == V && put => {
239 ("PutLifecycleConfiguration", Some((*id).to_string()))
240 }
241 [v, "file-systems", id, "protection"] if *v == V && put => {
242 ("UpdateFileSystemProtection", Some((*id).to_string()))
243 }
244 [v, "file-systems", id] if *v == V && del => {
245 ("DeleteFileSystem", Some((*id).to_string()))
246 }
247 [v, "file-systems", id] if *v == V && put => {
248 ("UpdateFileSystem", Some((*id).to_string()))
249 }
250 [v, "create-tags", id] if *v == V && post => ("CreateTags", Some((*id).to_string())),
252 [v, "delete-tags", id] if *v == V && post => ("DeleteTags", Some((*id).to_string())),
253 [v, "tags", id] if *v == V && get => ("DescribeTags", Some((*id).to_string())),
254 [v, "resource-tags", id] if *v == V && get => {
256 ("ListTagsForResource", Some((*id).to_string()))
257 }
258 [v, "resource-tags", id] if *v == V && post => ("TagResource", Some((*id).to_string())),
259 [v, "resource-tags", id] if *v == V && del => {
260 ("UntagResource", Some((*id).to_string()))
261 }
262 _ => return None,
263 };
264 Some((action, label))
265 }
266}
267
268#[async_trait]
269impl AwsService for EfsService {
270 fn service_name(&self) -> &str {
271 "elasticfilesystem"
272 }
273
274 async fn handle(&self, req: AwsRequest) -> Result<AwsResponse, AwsServiceError> {
275 let Some((action, label)) = Self::resolve_action(&req) else {
276 return Err(AwsServiceError::aws_error(
277 StatusCode::NOT_FOUND,
278 "UnknownOperationException",
279 format!("Unknown operation: {} {}", req.method, req.raw_path),
280 ));
281 };
282 let result = self.dispatch(action, label, &req);
283 if MUTATING.contains(&action)
284 && matches!(result.as_ref(), Ok(resp) if resp.status.is_success())
285 {
286 self.save().await;
287 }
288 result
289 }
290
291 fn supported_actions(&self) -> &[&str] {
292 EFS_ACTIONS
293 }
294}
295
296struct Ctx {
298 account: String,
299 region: String,
300}
301
302impl EfsService {
303 #[allow(clippy::too_many_lines)]
304 fn dispatch(
305 &self,
306 action: &str,
307 label: Option<String>,
308 req: &AwsRequest,
309 ) -> Result<AwsResponse, AwsServiceError> {
310 let body = parse_body(req)?;
311 crate::validate::validate_input(action, &body)?;
312 let ctx = Ctx {
313 account: req.account_id.clone(),
314 region: req.region.clone(),
315 };
316 let q = parse_query(&req.raw_query);
317 crate::validate::validate_query(&q)?;
318 let label = label.unwrap_or_default();
319 match action {
320 "CreateFileSystem" => self.create_file_system(&ctx, &body),
321 "DescribeFileSystems" => self.describe_file_systems(&ctx, &q),
322 "DeleteFileSystem" => self.delete_file_system(&ctx, &label),
323 "UpdateFileSystem" => self.update_file_system(&ctx, &label, &body),
324 "UpdateFileSystemProtection" => self.update_file_system_protection(&ctx, &label, &body),
325 "CreateMountTarget" => self.create_mount_target(&ctx, &body),
326 "DescribeMountTargets" => self.describe_mount_targets(&ctx, &q),
327 "DeleteMountTarget" => self.delete_mount_target(&ctx, &label),
328 "DescribeMountTargetSecurityGroups" => self.describe_mt_security_groups(&ctx, &label),
329 "ModifyMountTargetSecurityGroups" => {
330 self.modify_mt_security_groups(&ctx, &label, &body)
331 }
332 "CreateAccessPoint" => self.create_access_point(&ctx, &body),
333 "DescribeAccessPoints" => self.describe_access_points(&ctx, &q),
334 "DeleteAccessPoint" => self.delete_access_point(&ctx, &label),
335 "PutLifecycleConfiguration" => self.put_lifecycle_configuration(&ctx, &label, &body),
336 "DescribeLifecycleConfiguration" => self.describe_lifecycle_configuration(&ctx, &label),
337 "PutBackupPolicy" => self.put_backup_policy(&ctx, &label, &body),
338 "DescribeBackupPolicy" => self.describe_backup_policy(&ctx, &label),
339 "PutFileSystemPolicy" => self.put_file_system_policy(&ctx, &label, &body),
340 "DescribeFileSystemPolicy" => self.describe_file_system_policy(&ctx, &label),
341 "DeleteFileSystemPolicy" => self.delete_file_system_policy(&ctx, &label),
342 "CreateReplicationConfiguration" => {
343 self.create_replication_configuration(&ctx, &label, &body)
344 }
345 "DescribeReplicationConfigurations" => {
346 self.describe_replication_configurations(&ctx, &q)
347 }
348 "DeleteReplicationConfiguration" => self.delete_replication_configuration(&ctx, &label),
349 "CreateTags" => self.create_tags(&ctx, &label, &body),
350 "DeleteTags" => self.delete_tags(&ctx, &label, &body),
351 "DescribeTags" => self.describe_tags(&ctx, &label, &q),
352 "TagResource" => self.tag_resource(&ctx, &label, &body),
353 "UntagResource" => self.untag_resource(&ctx, &label, &q),
354 "ListTagsForResource" => self.list_tags_for_resource(&ctx, &label, &q),
355 "DescribeAccountPreferences" => self.describe_account_preferences(&ctx),
356 "PutAccountPreferences" => self.put_account_preferences(&ctx, &body),
357 _ => Err(AwsServiceError::action_not_implemented(
358 "elasticfilesystem",
359 action,
360 )),
361 }
362 }
363}
364
365fn ok(status: StatusCode, v: Value) -> Result<AwsResponse, AwsServiceError> {
368 Ok(AwsResponse::json_value(status, v))
369}
370
371fn empty(status: StatusCode) -> Result<AwsResponse, AwsServiceError> {
372 Ok(AwsResponse::json_value(status, json!({})))
373}
374
375fn parse_body(req: &AwsRequest) -> Result<Value, AwsServiceError> {
376 if req.body.is_empty() {
377 return Ok(json!({}));
378 }
379 serde_json::from_slice(&req.body)
380 .map_err(|e| bad_request(&format!("Request body is malformed: {e}")))
381}
382
383fn bad_request(msg: &str) -> AwsServiceError {
384 AwsServiceError::aws_error(StatusCode::BAD_REQUEST, "BadRequest", msg)
385}
386
387fn fs_not_found(id: &str) -> AwsServiceError {
388 AwsServiceError::aws_error(
389 StatusCode::NOT_FOUND,
390 "FileSystemNotFound",
391 format!("File system '{id}' does not exist."),
392 )
393}
394
395fn mt_not_found(id: &str) -> AwsServiceError {
396 AwsServiceError::aws_error(
397 StatusCode::NOT_FOUND,
398 "MountTargetNotFound",
399 format!("Mount target '{id}' does not exist."),
400 )
401}
402
403fn ap_not_found(id: &str) -> AwsServiceError {
404 AwsServiceError::aws_error(
405 StatusCode::NOT_FOUND,
406 "AccessPointNotFound",
407 format!("Access point '{id}' does not exist."),
408 )
409}
410
411fn subnet_not_found(id: &str) -> AwsServiceError {
412 AwsServiceError::aws_error(
413 StatusCode::BAD_REQUEST,
414 "SubnetNotFound",
415 format!("The subnet ID '{id}' is invalid or does not exist."),
416 )
417}
418
419fn replication_not_found(id: &str) -> AwsServiceError {
420 AwsServiceError::aws_error(
421 StatusCode::NOT_FOUND,
422 "ReplicationNotFound",
423 format!("Replication configuration for file system '{id}' does not exist."),
424 )
425}
426
427fn incorrect_fs_lifecycle_state(id: &str, state: &str) -> AwsServiceError {
428 AwsServiceError::aws_error(
429 StatusCode::CONFLICT,
430 "IncorrectFileSystemLifeCycleState",
431 format!(
432 "File system '{id}' is in life cycle state '{state}' but must be 'available' for this operation."
433 ),
434 )
435}
436
437fn conflict(code: &str, msg: &str) -> AwsServiceError {
438 AwsServiceError::aws_error(StatusCode::CONFLICT, code, msg)
439}
440
441fn hex17() -> String {
443 Uuid::new_v4()
444 .simple()
445 .to_string()
446 .chars()
447 .filter(char::is_ascii_hexdigit)
448 .take(17)
449 .collect()
450}
451
452fn now_ts() -> f64 {
453 chrono::Utc::now().timestamp() as f64
454}
455
456fn fs_arn(ctx: &Ctx, fsid: &str) -> String {
457 format!(
458 "arn:aws:elasticfilesystem:{}:{}:file-system/{}",
459 ctx.region, ctx.account, fsid
460 )
461}
462
463fn destination_file_system(ctx: &Ctx, fsid: &str, region: &str, az: Option<&str>) -> Value {
468 let mut fs = Map::new();
469 fs.insert("OwnerId".into(), json!(ctx.account));
470 fs.insert("FileSystemId".into(), json!(fsid));
471 fs.insert(
472 "FileSystemArn".into(),
473 json!(format!(
474 "arn:aws:elasticfilesystem:{}:{}:file-system/{}",
475 region, ctx.account, fsid
476 )),
477 );
478 fs.insert("CreationTime".into(), json!(now_ts()));
479 fs.insert("LifeCycleState".into(), json!("creating"));
481 fs.insert("NumberOfMountTargets".into(), json!(0));
482 fs.insert(
483 "SizeInBytes".into(),
484 json!({
485 "Value": 6144,
486 "Timestamp": now_ts(),
487 "ValueInIA": 0,
488 "ValueInStandard": 6144,
489 "ValueInArchive": 0
490 }),
491 );
492 fs.insert("PerformanceMode".into(), json!("generalPurpose"));
493 fs.insert("ThroughputMode".into(), json!("bursting"));
494 fs.insert("Encrypted".into(), json!(true));
495 fs.insert("Tags".into(), json!([]));
496 fs.insert(
497 "FileSystemProtection".into(),
498 json!({ "ReplicationOverwriteProtection": "DISABLED" }),
499 );
500 if let Some(az) = az {
501 fs.insert("AvailabilityZoneName".into(), json!(az));
502 fs.insert("AvailabilityZoneId".into(), json!(format!("{region}-az1")));
503 }
504 Value::Object(fs)
505}
506
507fn ap_arn(ctx: &Ctx, apid: &str) -> String {
508 format!(
509 "arn:aws:elasticfilesystem:{}:{}:access-point/{}",
510 ctx.region, ctx.account, apid
511 )
512}
513
514fn normalize_fs_id(raw: &str) -> String {
517 raw.rsplit('/').next().unwrap_or(raw).to_string()
518}
519
520fn hash_str(s: &str) -> u64 {
522 let mut h: u64 = 0xcbf2_9ce4_8422_2325;
523 for b in s.as_bytes() {
524 h ^= *b as u64;
525 h = h.wrapping_mul(0x0000_0100_0000_01b3);
526 }
527 h
528}
529
530fn parse_query(raw: &str) -> Vec<(String, String)> {
531 raw.split('&')
532 .filter(|p| !p.is_empty())
533 .map(|pair| {
534 let (k, v) = pair.split_once('=').unwrap_or((pair, ""));
535 (
536 percent_decode_str(k).decode_utf8_lossy().into_owned(),
537 percent_decode_str(v).decode_utf8_lossy().into_owned(),
538 )
539 })
540 .collect()
541}
542
543fn query_one<'a>(q: &'a [(String, String)], key: &str) -> Option<&'a str> {
544 q.iter()
545 .find(|(k, _)| k == key)
546 .map(|(_, v)| v.as_str())
547 .filter(|v| !v.is_empty())
548}
549
550fn query_all(q: &[(String, String)], key: &str) -> Vec<String> {
551 q.iter()
552 .filter(|(k, _)| k == key)
553 .map(|(_, v)| v.clone())
554 .collect()
555}
556
557fn tags_to_map(b: &Value) -> Map<String, Value> {
559 let mut out = Map::new();
560 if let Some(arr) = b.get("Tags").and_then(Value::as_array) {
561 for t in arr {
562 if let (Some(k), Some(v)) = (
563 t.get("Key").and_then(Value::as_str),
564 t.get("Value").and_then(Value::as_str),
565 ) {
566 out.insert(k.to_string(), json!(v));
567 }
568 }
569 }
570 out
571}
572
573fn tags_list(data: &EfsData, resource_id: &str) -> Value {
575 let arr: Vec<Value> = data
576 .tags
577 .get(resource_id)
578 .map(|m| {
579 m.iter()
580 .map(|(k, v)| json!({ "Key": k, "Value": v }))
581 .collect()
582 })
583 .unwrap_or_default();
584 Value::Array(arr)
585}
586
587impl EfsService {
588 fn account<'g>(
589 &self,
590 guard: &'g mut RwLockWriteGuard<'_, MultiAccountState<EfsData>>,
591 ctx: &Ctx,
592 ) -> &'g mut EfsData {
593 guard.get_or_create(&ctx.account)
594 }
595}
596
597impl EfsService {
600 fn create_file_system(&self, ctx: &Ctx, b: &Value) -> Result<AwsResponse, AwsServiceError> {
601 let creation_token = b
602 .get("CreationToken")
603 .and_then(Value::as_str)
604 .unwrap_or_default()
605 .to_string();
606 let mut guard = self.state.write();
607 let data = guard.get_or_create(&ctx.account);
608 if let Some((existing_id, _)) = data.file_systems.iter().find(|(_, fs)| {
611 fs.get("CreationToken").and_then(Value::as_str) == Some(creation_token.as_str())
612 }) {
613 let existing_id = existing_id.clone();
614 return Err(AwsServiceError::aws_error_with_fields(
615 StatusCode::CONFLICT,
616 "FileSystemAlreadyExists",
617 format!("File system already exists with creation token {creation_token}"),
618 vec![("FileSystemId".to_string(), existing_id)],
619 ));
620 }
621
622 let fsid = format!("fs-{}", hex17());
623 let performance_mode = b
624 .get("PerformanceMode")
625 .and_then(Value::as_str)
626 .unwrap_or("generalPurpose");
627 let throughput_mode = b
628 .get("ThroughputMode")
629 .and_then(Value::as_str)
630 .unwrap_or("bursting");
631 if throughput_mode == "provisioned" {
635 match b
636 .get("ProvisionedThroughputInMibps")
637 .and_then(Value::as_f64)
638 {
639 None => {
640 return Err(bad_request(
641 "ProvisionedThroughputInMibps is required when ThroughputMode is set to provisioned.",
642 ));
643 }
644 Some(v) if v < 1.0 => {
645 return Err(bad_request(
646 "Value at 'ProvisionedThroughputInMibps' failed to satisfy constraint: Member must have value greater than or equal to 1",
647 ));
648 }
649 Some(_) => {}
650 }
651 } else if b.get("ProvisionedThroughputInMibps").is_some() {
652 return Err(bad_request(
653 "ProvisionedThroughputInMibps is only applicable when ThroughputMode is set to provisioned.",
654 ));
655 }
656 let encrypted = b.get("Encrypted").and_then(Value::as_bool).unwrap_or(false);
657
658 let mut fs = Map::new();
659 fs.insert("OwnerId".into(), json!(ctx.account));
660 fs.insert("CreationToken".into(), json!(creation_token));
661 fs.insert("FileSystemId".into(), json!(fsid));
662 fs.insert("FileSystemArn".into(), json!(fs_arn(ctx, &fsid)));
663 fs.insert("CreationTime".into(), json!(now_ts()));
664 fs.insert("LifeCycleState".into(), json!("creating"));
667 fs.insert("NumberOfMountTargets".into(), json!(0));
668 fs.insert(
669 "SizeInBytes".into(),
670 json!({
671 "Value": 6144,
672 "Timestamp": now_ts(),
673 "ValueInIA": 0,
674 "ValueInStandard": 6144,
675 "ValueInArchive": 0
676 }),
677 );
678 fs.insert("PerformanceMode".into(), json!(performance_mode));
679 fs.insert("ThroughputMode".into(), json!(throughput_mode));
680 fs.insert("Encrypted".into(), json!(encrypted));
681 if encrypted {
682 let kms = b
683 .get("KmsKeyId")
684 .and_then(Value::as_str)
685 .map(str::to_string)
686 .unwrap_or_else(|| {
687 format!(
688 "arn:aws:kms:{}:{}:key/{}-{}-{}-{}-{}",
689 ctx.region,
690 ctx.account,
691 &hex17()[..8],
692 &hex17()[..4],
693 &hex17()[..4],
694 &hex17()[..4],
695 &hex17()[..12.min(hex17().len())]
696 )
697 });
698 fs.insert("KmsKeyId".into(), json!(kms));
699 }
700 if throughput_mode == "provisioned" {
701 if let Some(p) = b.get("ProvisionedThroughputInMibps") {
702 fs.insert("ProvisionedThroughputInMibps".into(), p.clone());
703 }
704 }
705 if let Some(az) = b.get("AvailabilityZoneName").and_then(Value::as_str) {
706 fs.insert("AvailabilityZoneName".into(), json!(az));
707 fs.insert(
708 "AvailabilityZoneId".into(),
709 json!(format!("{}-az1", ctx.region)),
710 );
711 }
712 let tag_map = tags_to_map(b);
714 if let Some(name) = tag_map.get("Name").and_then(Value::as_str) {
715 fs.insert("Name".into(), json!(name));
716 }
717 fs.insert("Tags".into(), tags_list_from_map(&tag_map));
718 fs.insert(
719 "FileSystemProtection".into(),
720 json!({ "ReplicationOverwriteProtection": "ENABLED" }),
721 );
722
723 if !tag_map.is_empty() {
724 let entry = data.tags.entry(fsid.clone()).or_default();
725 for (k, v) in &tag_map {
726 if let Some(vs) = v.as_str() {
727 entry.insert(k.clone(), vs.to_string());
728 }
729 }
730 }
731 data.file_systems
732 .insert(fsid.clone(), Value::Object(fs.clone()));
733 ok(StatusCode::CREATED, Value::Object(fs))
734 }
735
736 fn describe_file_systems(
737 &self,
738 ctx: &Ctx,
739 q: &[(String, String)],
740 ) -> Result<AwsResponse, AwsServiceError> {
741 let filter_id = query_one(q, "FileSystemId").map(normalize_fs_id);
742 let filter_token = query_one(q, "CreationToken");
743 let mut guard = self.state.write();
744 let data = self.account(&mut guard, ctx);
745 data.reconcile_lifecycle();
746
747 if let Some(id) = &filter_id {
748 if !data.file_systems.contains_key(id) {
749 return Err(fs_not_found(id));
750 }
751 }
752 let rows: Vec<Value> = data
753 .file_systems
754 .values()
755 .filter(|fs| {
756 filter_id
757 .as_ref()
758 .map(|id| fs.get("FileSystemId").and_then(Value::as_str) == Some(id.as_str()))
759 .unwrap_or(true)
760 })
761 .filter(|fs| {
762 filter_token
763 .map(|t| fs.get("CreationToken").and_then(Value::as_str) == Some(t))
764 .unwrap_or(true)
765 })
766 .map(|fs| with_live_mount_count(fs, data))
767 .collect();
768
769 let (page, next) =
771 paginate_marker(rows, query_one(q, "Marker"), query_one(q, "MaxItems"), 100);
772 let mut out = Map::new();
773 out.insert("FileSystems".into(), Value::Array(page));
774 if let Some(n) = next {
775 out.insert("NextMarker".into(), json!(n));
776 }
777 ok(StatusCode::OK, Value::Object(out))
778 }
779
780 fn require_fs<'a>(
781 &self,
782 data: &'a mut EfsData,
783 fsid: &str,
784 ) -> Result<&'a mut Value, AwsServiceError> {
785 data.file_systems
786 .get_mut(fsid)
787 .ok_or_else(|| fs_not_found(fsid))
788 }
789
790 fn delete_file_system(&self, ctx: &Ctx, label: &str) -> Result<AwsResponse, AwsServiceError> {
791 let fsid = normalize_fs_id(label);
792 let mut guard = self.state.write();
793 let data = self.account(&mut guard, ctx);
794 if !data.file_systems.contains_key(&fsid) {
795 return Err(fs_not_found(&fsid));
796 }
797 let has_mt = data
801 .mount_targets
802 .values()
803 .any(|mt| mt.get("FileSystemId").and_then(Value::as_str) == Some(fsid.as_str()));
804 let has_ap = data.access_points.values().any(|ap| {
805 normalize_fs_id(ap.get("FileSystemId").and_then(Value::as_str).unwrap_or("")) == fsid
806 });
807 if has_mt || has_ap {
808 return Err(conflict(
809 "FileSystemInUse",
810 &format!("File system '{fsid}' is in use and cannot be deleted."),
811 ));
812 }
813 data.file_systems.remove(&fsid);
814 data.tags.remove(&fsid);
815 data.lifecycle_configs.remove(&fsid);
816 data.backup_policies.remove(&fsid);
817 data.file_system_policies.remove(&fsid);
818 data.replications.remove(&fsid);
819 empty(StatusCode::NO_CONTENT)
820 }
821
822 fn update_file_system(
823 &self,
824 ctx: &Ctx,
825 label: &str,
826 b: &Value,
827 ) -> Result<AwsResponse, AwsServiceError> {
828 let fsid = normalize_fs_id(label);
829 let mut guard = self.state.write();
830 let data = self.account(&mut guard, ctx);
831 let fs = self.require_fs(data, &fsid)?;
832 let obj = fs.as_object_mut().unwrap();
833 if obj.get("LifeCycleState").and_then(Value::as_str) == Some("creating") {
834 obj.insert("LifeCycleState".into(), json!("available"));
835 }
836 if let Some(tm) = b.get("ThroughputMode") {
837 obj.insert("ThroughputMode".into(), tm.clone());
838 }
839 if let Some(p) = b.get("ProvisionedThroughputInMibps") {
840 obj.insert("ProvisionedThroughputInMibps".into(), p.clone());
841 }
842 if obj.get("ThroughputMode").and_then(Value::as_str) != Some("provisioned") {
844 obj.remove("ProvisionedThroughputInMibps");
845 }
846 let fs_snapshot = fs.clone();
847 let updated = with_live_mount_count(&fs_snapshot, data);
848 ok(StatusCode::ACCEPTED, updated)
849 }
850
851 fn update_file_system_protection(
852 &self,
853 ctx: &Ctx,
854 label: &str,
855 b: &Value,
856 ) -> Result<AwsResponse, AwsServiceError> {
857 let fsid = normalize_fs_id(label);
858 let mut guard = self.state.write();
859 let data = self.account(&mut guard, ctx);
860 let fs = self.require_fs(data, &fsid)?;
861 let protection = b
862 .get("ReplicationOverwriteProtection")
863 .and_then(Value::as_str)
864 .unwrap_or("ENABLED");
865 fs.as_object_mut().unwrap().insert(
866 "FileSystemProtection".into(),
867 json!({ "ReplicationOverwriteProtection": protection }),
868 );
869 ok(
870 StatusCode::OK,
871 json!({ "ReplicationOverwriteProtection": protection }),
872 )
873 }
874}
875
876fn tags_list_from_map(m: &Map<String, Value>) -> Value {
877 Value::Array(
878 m.iter()
879 .map(|(k, v)| json!({ "Key": k, "Value": v }))
880 .collect(),
881 )
882}
883
884fn with_live_mount_count(fs: &Value, data: &EfsData) -> Value {
887 let mut obj = fs.as_object().cloned().unwrap_or_default();
888 let fsid = obj
889 .get("FileSystemId")
890 .and_then(Value::as_str)
891 .unwrap_or("");
892 let count = data
893 .mount_targets
894 .values()
895 .filter(|mt| mt.get("FileSystemId").and_then(Value::as_str) == Some(fsid))
896 .count();
897 obj.insert("NumberOfMountTargets".into(), json!(count));
898 Value::Object(obj)
899}
900
901fn paginate_marker(
906 rows: Vec<Value>,
907 marker: Option<&str>,
908 max: Option<&str>,
909 default_max: usize,
910) -> (Vec<Value>, Option<String>) {
911 let start = marker.and_then(|m| m.parse::<usize>().ok()).unwrap_or(0);
912 let max = max
913 .and_then(|m| m.parse::<usize>().ok())
914 .map(|m| m.max(1))
915 .unwrap_or(default_max);
916 let end = start.saturating_add(max).min(rows.len());
917 let page = rows.get(start..end).unwrap_or(&[]).to_vec();
918 let next = if end < rows.len() {
919 Some(end.to_string())
920 } else {
921 None
922 };
923 (page, next)
924}
925
926impl EfsService {
929 fn create_mount_target(&self, ctx: &Ctx, b: &Value) -> Result<AwsResponse, AwsServiceError> {
930 let fsid = normalize_fs_id(b.get("FileSystemId").and_then(Value::as_str).unwrap_or(""));
931 let subnet_id = b
932 .get("SubnetId")
933 .and_then(Value::as_str)
934 .unwrap_or("")
935 .to_string();
936 let h = hash_str(&subnet_id);
941 let (az_name, az_id, vpc_id) = match self.resolve_subnet(&ctx.account, &subnet_id) {
942 Some((az, azid, vpc)) => (az, azid, vpc),
943 None => {
944 if self.ec2_state.is_some() {
949 return Err(subnet_not_found(&subnet_id));
950 }
951 let az_index = (h % 3) as u8;
952 (
953 format!("{}{}", ctx.region, (b'a' + az_index) as char),
954 format!("{}-az{}", ctx.region, az_index + 1),
955 format!("vpc-{:017x}", h & 0x000f_ffff_ffff_ffff),
956 )
957 }
958 };
959 let mut guard = self.state.write();
960 let data = self.account(&mut guard, ctx);
961 let fs = data
967 .file_systems
968 .get(&fsid)
969 .ok_or_else(|| fs_not_found(&fsid))?;
970 let fs_state = fs
971 .get("LifeCycleState")
972 .and_then(Value::as_str)
973 .unwrap_or("");
974 if fs_state != "available" {
975 return Err(incorrect_fs_lifecycle_state(&fsid, fs_state));
976 }
977 for mt in data.mount_targets.values() {
979 if mt.get("FileSystemId").and_then(Value::as_str) == Some(fsid.as_str())
980 && mt.get("AvailabilityZoneName").and_then(Value::as_str) == Some(az_name.as_str())
981 {
982 return Err(conflict(
983 "MountTargetConflict",
984 "A mount target already exists in this Availability Zone for the file system.",
985 ));
986 }
987 }
988
989 let mtid = format!("fsmt-{}", hex17());
990 let ip = b
991 .get("IpAddress")
992 .and_then(Value::as_str)
993 .map(str::to_string)
994 .unwrap_or_else(|| format!("10.0.{}.{}", (h >> 8) % 256, h % 254 + 1));
995 let eni_id = format!("eni-{}", hex17());
996 let security_groups: Vec<String> = b
997 .get("SecurityGroups")
998 .and_then(Value::as_array)
999 .map(|a| {
1000 a.iter()
1001 .filter_map(|v| v.as_str().map(str::to_string))
1002 .collect()
1003 })
1004 .filter(|v: &Vec<String>| !v.is_empty())
1005 .unwrap_or_else(|| vec![format!("sg-{:017x}", h & 0x000f_ffff_ffff_ffff)]);
1006
1007 let mut mt = Map::new();
1008 mt.insert("OwnerId".into(), json!(ctx.account));
1009 mt.insert("MountTargetId".into(), json!(mtid));
1010 mt.insert("FileSystemId".into(), json!(fsid));
1011 mt.insert("SubnetId".into(), json!(subnet_id));
1012 mt.insert("LifeCycleState".into(), json!("creating"));
1013 mt.insert("IpAddress".into(), json!(ip));
1014 mt.insert("NetworkInterfaceId".into(), json!(eni_id));
1015 mt.insert("AvailabilityZoneId".into(), json!(az_id));
1016 mt.insert("AvailabilityZoneName".into(), json!(az_name));
1017 mt.insert("VpcId".into(), json!(vpc_id));
1018 if let Some(ip6) = b.get("Ipv6Address") {
1019 mt.insert("Ipv6Address".into(), ip6.clone());
1020 }
1021
1022 data.mount_target_security_groups
1023 .insert(mtid.clone(), security_groups);
1024 data.mount_targets
1025 .insert(mtid.clone(), Value::Object(mt.clone()));
1026 ok(StatusCode::OK, Value::Object(mt))
1027 }
1028
1029 fn describe_mount_targets(
1030 &self,
1031 ctx: &Ctx,
1032 q: &[(String, String)],
1033 ) -> Result<AwsResponse, AwsServiceError> {
1034 let by_fs = query_one(q, "FileSystemId").map(normalize_fs_id);
1035 let by_mt = query_one(q, "MountTargetId");
1036 let by_ap = query_one(q, "AccessPointId");
1037 let filter_count = usize::from(by_fs.is_some())
1040 + usize::from(by_mt.is_some())
1041 + usize::from(by_ap.is_some());
1042 if filter_count != 1 {
1043 return Err(bad_request(
1044 "Exactly one of FileSystemId, MountTargetId, or AccessPointId must be specified.",
1045 ));
1046 }
1047 let mut guard = self.state.write();
1048 let data = self.account(&mut guard, ctx);
1049 data.reconcile_lifecycle();
1050
1051 let ap_fs = if let Some(ap) = by_ap {
1053 match data.access_points.get(ap) {
1054 Some(ap_obj) => Some(normalize_fs_id(
1055 ap_obj
1056 .get("FileSystemId")
1057 .and_then(Value::as_str)
1058 .unwrap_or(""),
1059 )),
1060 None => return Err(ap_not_found(ap)),
1061 }
1062 } else {
1063 None
1064 };
1065 if let Some(fsid) = &by_fs {
1066 if !data.file_systems.contains_key(fsid) {
1067 return Err(fs_not_found(fsid));
1068 }
1069 }
1070 if let Some(mtid) = by_mt {
1071 if !data.mount_targets.contains_key(mtid) {
1072 return Err(mt_not_found(mtid));
1073 }
1074 }
1075
1076 let rows: Vec<Value> = data
1077 .mount_targets
1078 .iter()
1079 .filter(|(mtid, mt)| {
1080 let fs = mt.get("FileSystemId").and_then(Value::as_str);
1081 by_fs.as_deref().map(|f| fs == Some(f)).unwrap_or(true)
1082 && by_mt.map(|m| mtid.as_str() == m).unwrap_or(true)
1083 && ap_fs.as_deref().map(|f| fs == Some(f)).unwrap_or(true)
1084 })
1085 .map(|(_, mt)| mt.clone())
1086 .collect();
1087
1088 let (page, next) =
1090 paginate_marker(rows, query_one(q, "Marker"), query_one(q, "MaxItems"), 10);
1091 let mut out = Map::new();
1092 out.insert("MountTargets".into(), Value::Array(page));
1093 if let Some(n) = next {
1094 out.insert("NextMarker".into(), json!(n));
1095 }
1096 ok(StatusCode::OK, Value::Object(out))
1097 }
1098
1099 fn delete_mount_target(&self, ctx: &Ctx, label: &str) -> Result<AwsResponse, AwsServiceError> {
1100 let mut guard = self.state.write();
1101 let data = self.account(&mut guard, ctx);
1102 if data.mount_targets.remove(label).is_none() {
1103 return Err(mt_not_found(label));
1104 }
1105 data.mount_target_security_groups.remove(label);
1106 empty(StatusCode::NO_CONTENT)
1107 }
1108
1109 fn describe_mt_security_groups(
1110 &self,
1111 ctx: &Ctx,
1112 label: &str,
1113 ) -> Result<AwsResponse, AwsServiceError> {
1114 let mut guard = self.state.write();
1115 let data = self.account(&mut guard, ctx);
1116 data.reconcile_lifecycle();
1117 if !data.mount_targets.contains_key(label) {
1118 return Err(mt_not_found(label));
1119 }
1120 let sgs = data
1121 .mount_target_security_groups
1122 .get(label)
1123 .cloned()
1124 .unwrap_or_default();
1125 ok(StatusCode::OK, json!({ "SecurityGroups": sgs }))
1126 }
1127
1128 fn modify_mt_security_groups(
1129 &self,
1130 ctx: &Ctx,
1131 label: &str,
1132 b: &Value,
1133 ) -> Result<AwsResponse, AwsServiceError> {
1134 let mut guard = self.state.write();
1135 let data = self.account(&mut guard, ctx);
1136 if !data.mount_targets.contains_key(label) {
1137 return Err(mt_not_found(label));
1138 }
1139 let sgs: Vec<String> = b
1140 .get("SecurityGroups")
1141 .and_then(Value::as_array)
1142 .map(|a| {
1143 a.iter()
1144 .filter_map(|v| v.as_str().map(str::to_string))
1145 .collect()
1146 })
1147 .unwrap_or_default();
1148 data.mount_target_security_groups
1149 .insert(label.to_string(), sgs);
1150 empty(StatusCode::NO_CONTENT)
1151 }
1152}
1153
1154impl EfsService {
1157 fn create_access_point(&self, ctx: &Ctx, b: &Value) -> Result<AwsResponse, AwsServiceError> {
1158 let client_token = b
1159 .get("ClientToken")
1160 .and_then(Value::as_str)
1161 .unwrap_or_default()
1162 .to_string();
1163 let fsid = normalize_fs_id(b.get("FileSystemId").and_then(Value::as_str).unwrap_or(""));
1164 let mut guard = self.state.write();
1165 let data = self.account(&mut guard, ctx);
1166 if !data.file_systems.contains_key(&fsid) {
1167 return Err(fs_not_found(&fsid));
1168 }
1169 if let Some((existing_id, _)) = data.access_points.iter().find(|(_, ap)| {
1170 ap.get("ClientToken").and_then(Value::as_str) == Some(client_token.as_str())
1171 }) {
1172 let existing_id = existing_id.clone();
1173 return Err(AwsServiceError::aws_error_with_fields(
1174 StatusCode::CONFLICT,
1175 "AccessPointAlreadyExists",
1176 format!("Access point already exists with client token {client_token}"),
1177 vec![("AccessPointId".to_string(), existing_id)],
1178 ));
1179 }
1180
1181 let apid = format!("fsap-{}", hex17());
1182 let tag_map = tags_to_map(b);
1183 let mut ap = Map::new();
1184 ap.insert("ClientToken".into(), json!(client_token));
1185 if let Some(name) = tag_map.get("Name").and_then(Value::as_str) {
1186 ap.insert("Name".into(), json!(name));
1187 }
1188 ap.insert("Tags".into(), tags_list_from_map(&tag_map));
1189 ap.insert("AccessPointId".into(), json!(apid));
1190 ap.insert("AccessPointArn".into(), json!(ap_arn(ctx, &apid)));
1191 ap.insert("FileSystemId".into(), json!(fsid));
1192 if let Some(pu) = b.get("PosixUser") {
1193 ap.insert("PosixUser".into(), pu.clone());
1194 }
1195 if let Some(rd) = b.get("RootDirectory") {
1196 ap.insert("RootDirectory".into(), rd.clone());
1197 } else {
1198 ap.insert("RootDirectory".into(), json!({ "Path": "/" }));
1199 }
1200 ap.insert("OwnerId".into(), json!(ctx.account));
1201 ap.insert("LifeCycleState".into(), json!("creating"));
1202
1203 if !tag_map.is_empty() {
1204 let entry = data.tags.entry(apid.clone()).or_default();
1205 for (k, v) in &tag_map {
1206 if let Some(vs) = v.as_str() {
1207 entry.insert(k.clone(), vs.to_string());
1208 }
1209 }
1210 }
1211 data.access_points
1212 .insert(apid.clone(), Value::Object(ap.clone()));
1213 ok(StatusCode::OK, Value::Object(ap))
1214 }
1215
1216 fn describe_access_points(
1217 &self,
1218 ctx: &Ctx,
1219 q: &[(String, String)],
1220 ) -> Result<AwsResponse, AwsServiceError> {
1221 let by_ap = query_one(q, "AccessPointId");
1222 let by_fs = query_one(q, "FileSystemId").map(normalize_fs_id);
1223 let mut guard = self.state.write();
1224 let data = self.account(&mut guard, ctx);
1225 data.reconcile_lifecycle();
1226
1227 if let Some(ap) = by_ap {
1228 if !data.access_points.contains_key(ap) {
1229 return Err(ap_not_found(ap));
1230 }
1231 }
1232 if let Some(fsid) = &by_fs {
1233 if !data.file_systems.contains_key(fsid) {
1234 return Err(fs_not_found(fsid));
1235 }
1236 }
1237 let rows: Vec<Value> = data
1238 .access_points
1239 .iter()
1240 .filter(|(apid, ap)| {
1241 by_ap.map(|a| apid.as_str() == a).unwrap_or(true)
1242 && by_fs
1243 .as_deref()
1244 .map(|f| ap.get("FileSystemId").and_then(Value::as_str) == Some(f))
1245 .unwrap_or(true)
1246 })
1247 .map(|(_, ap)| ap.clone())
1248 .collect();
1249
1250 let start = query_one(q, "NextToken")
1252 .and_then(|t| t.parse::<usize>().ok())
1253 .unwrap_or(0);
1254 let max = query_one(q, "MaxResults")
1255 .and_then(|m| m.parse::<usize>().ok())
1256 .map(|m| m.max(1))
1257 .unwrap_or(100);
1258 let end = start.saturating_add(max).min(rows.len());
1259 let page = rows.get(start..end).unwrap_or(&[]).to_vec();
1260 let mut out = Map::new();
1261 out.insert("AccessPoints".into(), Value::Array(page));
1262 if end < rows.len() {
1263 out.insert("NextToken".into(), json!(end.to_string()));
1264 }
1265 ok(StatusCode::OK, Value::Object(out))
1266 }
1267
1268 fn delete_access_point(&self, ctx: &Ctx, label: &str) -> Result<AwsResponse, AwsServiceError> {
1269 let mut guard = self.state.write();
1270 let data = self.account(&mut guard, ctx);
1271 if data.access_points.remove(label).is_none() {
1272 return Err(ap_not_found(label));
1273 }
1274 data.tags.remove(label);
1275 empty(StatusCode::NO_CONTENT)
1276 }
1277}
1278
1279impl EfsService {
1282 fn put_lifecycle_configuration(
1283 &self,
1284 ctx: &Ctx,
1285 label: &str,
1286 b: &Value,
1287 ) -> Result<AwsResponse, AwsServiceError> {
1288 let fsid = normalize_fs_id(label);
1289 let mut guard = self.state.write();
1290 let data = self.account(&mut guard, ctx);
1291 if !data.file_systems.contains_key(&fsid) {
1292 return Err(fs_not_found(&fsid));
1293 }
1294 let policies = b.get("LifecyclePolicies").cloned().unwrap_or(json!([]));
1295 data.lifecycle_configs.insert(fsid, policies.clone());
1296 ok(StatusCode::OK, json!({ "LifecyclePolicies": policies }))
1297 }
1298
1299 fn describe_lifecycle_configuration(
1300 &self,
1301 ctx: &Ctx,
1302 label: &str,
1303 ) -> Result<AwsResponse, AwsServiceError> {
1304 let fsid = normalize_fs_id(label);
1305 let guard = self.state.read();
1306 let data = guard.get(&ctx.account);
1307 let exists = data
1308 .map(|d| d.file_systems.contains_key(&fsid))
1309 .unwrap_or(false);
1310 if !exists {
1311 return Err(fs_not_found(&fsid));
1312 }
1313 let policies = data
1314 .and_then(|d| d.lifecycle_configs.get(&fsid).cloned())
1315 .unwrap_or(json!([]));
1316 ok(StatusCode::OK, json!({ "LifecyclePolicies": policies }))
1317 }
1318
1319 fn put_backup_policy(
1320 &self,
1321 ctx: &Ctx,
1322 label: &str,
1323 b: &Value,
1324 ) -> Result<AwsResponse, AwsServiceError> {
1325 let fsid = normalize_fs_id(label);
1326 let mut guard = self.state.write();
1327 let data = self.account(&mut guard, ctx);
1328 if !data.file_systems.contains_key(&fsid) {
1329 return Err(fs_not_found(&fsid));
1330 }
1331 let status = b
1332 .get("BackupPolicy")
1333 .and_then(|p| p.get("Status"))
1334 .and_then(Value::as_str)
1335 .unwrap_or("DISABLED")
1336 .to_string();
1337 data.backup_policies.insert(fsid, status.clone());
1338 ok(
1339 StatusCode::OK,
1340 json!({ "BackupPolicy": { "Status": status } }),
1341 )
1342 }
1343
1344 fn describe_backup_policy(
1345 &self,
1346 ctx: &Ctx,
1347 label: &str,
1348 ) -> Result<AwsResponse, AwsServiceError> {
1349 let fsid = normalize_fs_id(label);
1350 let guard = self.state.read();
1351 let data = guard.get(&ctx.account);
1352 let exists = data
1353 .map(|d| d.file_systems.contains_key(&fsid))
1354 .unwrap_or(false);
1355 if !exists {
1356 return Err(fs_not_found(&fsid));
1357 }
1358 let status = data
1359 .and_then(|d| d.backup_policies.get(&fsid).cloned())
1360 .unwrap_or_else(|| "DISABLED".to_string());
1361 ok(
1362 StatusCode::OK,
1363 json!({ "BackupPolicy": { "Status": status } }),
1364 )
1365 }
1366
1367 fn put_file_system_policy(
1368 &self,
1369 ctx: &Ctx,
1370 label: &str,
1371 b: &Value,
1372 ) -> Result<AwsResponse, AwsServiceError> {
1373 let fsid = normalize_fs_id(label);
1374 let mut guard = self.state.write();
1375 let data = self.account(&mut guard, ctx);
1376 if !data.file_systems.contains_key(&fsid) {
1377 return Err(fs_not_found(&fsid));
1378 }
1379 let policy = b
1380 .get("Policy")
1381 .and_then(Value::as_str)
1382 .unwrap_or_default()
1383 .to_string();
1384 data.file_system_policies
1385 .insert(fsid.clone(), policy.clone());
1386 ok(
1387 StatusCode::OK,
1388 json!({ "FileSystemId": fsid, "Policy": policy }),
1389 )
1390 }
1391
1392 fn describe_file_system_policy(
1393 &self,
1394 ctx: &Ctx,
1395 label: &str,
1396 ) -> Result<AwsResponse, AwsServiceError> {
1397 let fsid = normalize_fs_id(label);
1398 let guard = self.state.read();
1399 let data = guard.get(&ctx.account);
1400 let exists = data
1401 .map(|d| d.file_systems.contains_key(&fsid))
1402 .unwrap_or(false);
1403 if !exists {
1404 return Err(fs_not_found(&fsid));
1405 }
1406 match data.and_then(|d| d.file_system_policies.get(&fsid).cloned()) {
1407 Some(policy) => ok(
1408 StatusCode::OK,
1409 json!({ "FileSystemId": fsid, "Policy": policy }),
1410 ),
1411 None => Err(AwsServiceError::aws_error(
1412 StatusCode::NOT_FOUND,
1413 "PolicyNotFound",
1414 format!("No policy is attached to file system '{fsid}'."),
1415 )),
1416 }
1417 }
1418
1419 fn delete_file_system_policy(
1420 &self,
1421 ctx: &Ctx,
1422 label: &str,
1423 ) -> Result<AwsResponse, AwsServiceError> {
1424 let fsid = normalize_fs_id(label);
1425 let mut guard = self.state.write();
1426 let data = self.account(&mut guard, ctx);
1427 if !data.file_systems.contains_key(&fsid) {
1428 return Err(fs_not_found(&fsid));
1429 }
1430 data.file_system_policies.remove(&fsid);
1431 empty(StatusCode::OK)
1432 }
1433}
1434
1435impl EfsService {
1438 fn create_replication_configuration(
1439 &self,
1440 ctx: &Ctx,
1441 label: &str,
1442 b: &Value,
1443 ) -> Result<AwsResponse, AwsServiceError> {
1444 let fsid = normalize_fs_id(label);
1445 let mut guard = self.state.write();
1446 let data = self.account(&mut guard, ctx);
1447 if !data.file_systems.contains_key(&fsid) {
1448 return Err(fs_not_found(&fsid));
1449 }
1450 let requested = b
1451 .get("Destinations")
1452 .and_then(Value::as_array)
1453 .cloned()
1454 .unwrap_or_default();
1455 let mut destinations: Vec<Value> = Vec::with_capacity(requested.len());
1456 let mut new_dest_systems: Vec<(String, Value)> = Vec::new();
1460 for d in &requested {
1461 let region = d
1462 .get("Region")
1463 .and_then(Value::as_str)
1464 .unwrap_or(&ctx.region)
1465 .to_string();
1466 let az = d.get("AvailabilityZoneName").and_then(Value::as_str);
1467 let provided = d
1468 .get("FileSystemId")
1469 .and_then(Value::as_str)
1470 .map(normalize_fs_id);
1471 let dest_fs = provided
1472 .clone()
1473 .unwrap_or_else(|| format!("fs-{}", hex17()));
1474 if !data.file_systems.contains_key(&dest_fs) {
1477 new_dest_systems.push((
1478 dest_fs.clone(),
1479 destination_file_system(ctx, &dest_fs, ®ion, az),
1480 ));
1481 }
1482 let mut dest = Map::new();
1483 dest.insert("Status".into(), json!("ENABLED"));
1484 dest.insert("FileSystemId".into(), json!(dest_fs));
1485 dest.insert("Region".into(), json!(region));
1486 dest.insert("LastReplicatedTimestamp".into(), json!(now_ts()));
1487 dest.insert("OwnerId".into(), json!(ctx.account));
1488 if let Some(role) = d.get("RoleArn") {
1489 dest.insert("RoleArn".into(), role.clone());
1490 }
1491 destinations.push(Value::Object(dest));
1492 }
1493 for (id, fs) in new_dest_systems {
1494 data.file_systems.insert(id, fs);
1495 }
1496
1497 let desc = json!({
1498 "SourceFileSystemId": fsid,
1499 "SourceFileSystemRegion": ctx.region,
1500 "SourceFileSystemArn": fs_arn(ctx, &fsid),
1501 "OriginalSourceFileSystemArn": fs_arn(ctx, &fsid),
1502 "CreationTime": now_ts(),
1503 "Destinations": destinations,
1504 "SourceFileSystemOwnerId": ctx.account,
1505 });
1506 data.replications.insert(fsid, desc.clone());
1507 ok(StatusCode::OK, desc)
1508 }
1509
1510 fn describe_replication_configurations(
1511 &self,
1512 ctx: &Ctx,
1513 q: &[(String, String)],
1514 ) -> Result<AwsResponse, AwsServiceError> {
1515 let by_fs = query_one(q, "FileSystemId").map(normalize_fs_id);
1516 let guard = self.state.read();
1517 let data = guard.get(&ctx.account);
1518 if let Some(fsid) = &by_fs {
1519 let fs_exists = data
1520 .map(|d| d.file_systems.contains_key(fsid))
1521 .unwrap_or(false);
1522 let has_replication = data
1523 .map(|d| d.replications.contains_key(fsid))
1524 .unwrap_or(false);
1525 if !fs_exists && !has_replication {
1526 return Err(fs_not_found(fsid));
1527 }
1528 if !has_replication {
1531 return Err(replication_not_found(fsid));
1532 }
1533 }
1534 let rows: Vec<Value> = data
1535 .map(|d| {
1536 d.replications
1537 .iter()
1538 .filter(|(src, _)| by_fs.as_deref().map(|f| src.as_str() == f).unwrap_or(true))
1539 .map(|(_, r)| r.clone())
1540 .collect()
1541 })
1542 .unwrap_or_default();
1543 ok(StatusCode::OK, json!({ "Replications": rows }))
1544 }
1545
1546 fn delete_replication_configuration(
1547 &self,
1548 ctx: &Ctx,
1549 label: &str,
1550 ) -> Result<AwsResponse, AwsServiceError> {
1551 let fsid = normalize_fs_id(label);
1552 let mut guard = self.state.write();
1553 let data = self.account(&mut guard, ctx);
1554 if !data.file_systems.contains_key(&fsid) {
1555 return Err(fs_not_found(&fsid));
1556 }
1557 if data.replications.remove(&fsid).is_none() {
1558 return Err(AwsServiceError::aws_error(
1559 StatusCode::NOT_FOUND,
1560 "ReplicationNotFound",
1561 format!("No replication configuration found for file system '{fsid}'."),
1562 ));
1563 }
1564 empty(StatusCode::NO_CONTENT)
1565 }
1566}
1567
1568impl EfsService {
1571 fn create_tags(
1572 &self,
1573 ctx: &Ctx,
1574 label: &str,
1575 b: &Value,
1576 ) -> Result<AwsResponse, AwsServiceError> {
1577 let fsid = normalize_fs_id(label);
1578 let mut guard = self.state.write();
1579 let data = self.account(&mut guard, ctx);
1580 if !data.file_systems.contains_key(&fsid) {
1581 return Err(fs_not_found(&fsid));
1582 }
1583 merge_tags(data, &fsid, b);
1584 sync_resource_tags(data, &fsid);
1585 empty(StatusCode::NO_CONTENT)
1586 }
1587
1588 fn delete_tags(
1589 &self,
1590 ctx: &Ctx,
1591 label: &str,
1592 b: &Value,
1593 ) -> Result<AwsResponse, AwsServiceError> {
1594 let fsid = normalize_fs_id(label);
1595 let mut guard = self.state.write();
1596 let data = self.account(&mut guard, ctx);
1597 if !data.file_systems.contains_key(&fsid) {
1598 return Err(fs_not_found(&fsid));
1599 }
1600 if let Some(keys) = b.get("TagKeys").and_then(Value::as_array) {
1601 if let Some(entry) = data.tags.get_mut(&fsid) {
1602 for k in keys {
1603 if let Some(ks) = k.as_str() {
1604 entry.remove(ks);
1605 }
1606 }
1607 }
1608 }
1609 sync_resource_tags(data, &fsid);
1610 empty(StatusCode::NO_CONTENT)
1611 }
1612
1613 fn describe_tags(
1614 &self,
1615 ctx: &Ctx,
1616 label: &str,
1617 q: &[(String, String)],
1618 ) -> Result<AwsResponse, AwsServiceError> {
1619 let fsid = normalize_fs_id(label);
1620 let guard = self.state.read();
1621 let data = guard.get(&ctx.account);
1622 let exists = data
1623 .map(|d| d.file_systems.contains_key(&fsid))
1624 .unwrap_or(false);
1625 if !exists {
1626 return Err(fs_not_found(&fsid));
1627 }
1628 let all = data
1629 .map(|d| tags_list(d, &fsid))
1630 .unwrap_or_else(|| json!([]));
1631 let rows = all.as_array().cloned().unwrap_or_default();
1632 let (page, next) =
1634 paginate_marker(rows, query_one(q, "Marker"), query_one(q, "MaxItems"), 100);
1635 let mut out = Map::new();
1636 out.insert("Tags".into(), Value::Array(page));
1637 if let Some(n) = next {
1638 out.insert("NextMarker".into(), json!(n));
1639 }
1640 ok(StatusCode::OK, Value::Object(out))
1641 }
1642
1643 fn tag_resource(
1644 &self,
1645 ctx: &Ctx,
1646 label: &str,
1647 b: &Value,
1648 ) -> Result<AwsResponse, AwsServiceError> {
1649 let rid = normalize_fs_id(label);
1650 let mut guard = self.state.write();
1651 let data = self.account(&mut guard, ctx);
1652 self.require_taggable(data, &rid)?;
1653 merge_tags(data, &rid, b);
1654 sync_resource_tags(data, &rid);
1655 empty(StatusCode::OK)
1656 }
1657
1658 fn untag_resource(
1659 &self,
1660 ctx: &Ctx,
1661 label: &str,
1662 q: &[(String, String)],
1663 ) -> Result<AwsResponse, AwsServiceError> {
1664 let rid = normalize_fs_id(label);
1665 let mut guard = self.state.write();
1666 let data = self.account(&mut guard, ctx);
1667 self.require_taggable(data, &rid)?;
1668 let keys = query_all(q, "tagKeys");
1669 if let Some(entry) = data.tags.get_mut(&rid) {
1670 for k in &keys {
1671 entry.remove(k);
1672 }
1673 }
1674 sync_resource_tags(data, &rid);
1675 empty(StatusCode::OK)
1676 }
1677
1678 fn list_tags_for_resource(
1679 &self,
1680 ctx: &Ctx,
1681 label: &str,
1682 q: &[(String, String)],
1683 ) -> Result<AwsResponse, AwsServiceError> {
1684 let rid = normalize_fs_id(label);
1685 let guard = self.state.read();
1686 let data = guard.get(&ctx.account);
1687 let known = data
1689 .map(|d| {
1690 (rid.starts_with("fsap-") && d.access_points.contains_key(&rid))
1691 || (rid.starts_with("fs-") && d.file_systems.contains_key(&rid))
1692 })
1693 .unwrap_or(false);
1694 if !known {
1695 return Err(if rid.starts_with("fsap-") {
1696 ap_not_found(&rid)
1697 } else {
1698 fs_not_found(&rid)
1699 });
1700 }
1701 let all = data
1702 .map(|d| tags_list(d, &rid))
1703 .unwrap_or_else(|| json!([]));
1704 let rows = all.as_array().cloned().unwrap_or_default();
1705 let start = query_one(q, "NextToken")
1707 .and_then(|t| t.parse::<usize>().ok())
1708 .unwrap_or(0);
1709 let max = query_one(q, "MaxResults")
1710 .and_then(|m| m.parse::<usize>().ok())
1711 .map(|m| m.max(1))
1712 .unwrap_or(100);
1713 let end = start.saturating_add(max).min(rows.len());
1714 let page = rows.get(start..end).unwrap_or(&[]).to_vec();
1715 let mut out = Map::new();
1716 out.insert("Tags".into(), Value::Array(page));
1717 if end < rows.len() {
1718 out.insert("NextToken".into(), json!(end.to_string()));
1719 }
1720 ok(StatusCode::OK, Value::Object(out))
1721 }
1722
1723 fn require_taggable(&self, data: &EfsData, rid: &str) -> Result<(), AwsServiceError> {
1724 if rid.starts_with("fsap-") {
1725 if data.access_points.contains_key(rid) {
1726 Ok(())
1727 } else {
1728 Err(ap_not_found(rid))
1729 }
1730 } else if data.file_systems.contains_key(rid) {
1731 Ok(())
1732 } else {
1733 Err(fs_not_found(rid))
1734 }
1735 }
1736
1737 fn describe_account_preferences(&self, ctx: &Ctx) -> Result<AwsResponse, AwsServiceError> {
1738 let guard = self.state.read();
1739 match guard
1742 .get(&ctx.account)
1743 .and_then(|d| d.resource_id_preference.clone())
1744 {
1745 Some(pref) => ok(
1746 StatusCode::OK,
1747 json!({
1748 "ResourceIdPreference": {
1749 "ResourceIdType": pref,
1750 "Resources": ["FILE_SYSTEM", "MOUNT_TARGET"]
1751 }
1752 }),
1753 ),
1754 None => empty(StatusCode::OK),
1755 }
1756 }
1757
1758 fn put_account_preferences(
1759 &self,
1760 ctx: &Ctx,
1761 b: &Value,
1762 ) -> Result<AwsResponse, AwsServiceError> {
1763 let id_type = b
1764 .get("ResourceIdType")
1765 .and_then(Value::as_str)
1766 .unwrap_or("LONG_ID")
1767 .to_string();
1768 let mut guard = self.state.write();
1769 let data = self.account(&mut guard, ctx);
1770 data.resource_id_preference = Some(id_type.clone());
1771 ok(
1772 StatusCode::OK,
1773 json!({
1774 "ResourceIdPreference": {
1775 "ResourceIdType": id_type,
1776 "Resources": ["FILE_SYSTEM", "MOUNT_TARGET"]
1777 }
1778 }),
1779 )
1780 }
1781}
1782
1783fn merge_tags(data: &mut EfsData, resource_id: &str, b: &Value) {
1785 let map = tags_to_map(b);
1786 if map.is_empty() {
1787 return;
1788 }
1789 let entry = data.tags.entry(resource_id.to_string()).or_default();
1790 for (k, v) in &map {
1791 if let Some(vs) = v.as_str() {
1792 entry.insert(k.clone(), vs.to_string());
1793 }
1794 }
1795}
1796
1797fn sync_resource_tags(data: &mut EfsData, rid: &str) {
1802 let tags = data.tags.get(rid).cloned().unwrap_or_default();
1803 let obj = if rid.starts_with("fsap-") {
1804 data.access_points.get_mut(rid)
1805 } else {
1806 data.file_systems.get_mut(rid)
1807 }
1808 .and_then(Value::as_object_mut);
1809 if let Some(obj) = obj {
1810 match tags.get("Name") {
1811 Some(name) => {
1812 obj.insert("Name".into(), json!(name));
1813 }
1814 None => {
1815 obj.remove("Name");
1816 }
1817 }
1818 obj.insert(
1819 "Tags".into(),
1820 Value::Array(
1821 tags.iter()
1822 .map(|(k, v)| json!({ "Key": k, "Value": v }))
1823 .collect(),
1824 ),
1825 );
1826 }
1827}
1828
1829#[cfg(test)]
1830mod tests {
1831 use super::*;
1832 use parking_lot::RwLock;
1833
1834 fn ctx() -> Ctx {
1835 Ctx {
1836 account: "000000000000".to_string(),
1837 region: "us-east-1".to_string(),
1838 }
1839 }
1840
1841 fn svc() -> EfsService {
1842 let state: SharedEfsState = Arc::new(RwLock::new(MultiAccountState::new(
1843 "000000000000",
1844 "us-east-1",
1845 "",
1846 )));
1847 EfsService::new(state)
1848 }
1849
1850 fn empty_ec2() -> fakecloud_ec2::SharedEc2State {
1851 Arc::new(RwLock::new(MultiAccountState::new(
1852 "000000000000",
1853 "us-east-1",
1854 "",
1855 )))
1856 }
1857
1858 fn seed_fs(s: &EfsService, fsid: &str, life_cycle: &str) {
1859 let mut g = s.state.write();
1860 let d = g.get_or_create("000000000000");
1861 d.file_systems.insert(
1862 fsid.to_string(),
1863 json!({ "FileSystemId": fsid, "LifeCycleState": life_cycle }),
1864 );
1865 }
1866
1867 fn body_value(resp: &AwsResponse) -> Value {
1868 serde_json::from_slice(resp.body.expect_bytes()).unwrap()
1869 }
1870
1871 #[test]
1874 fn create_mount_target_nonexistent_subnet_is_subnet_not_found() {
1875 let s = svc().with_ec2_state(empty_ec2());
1876 seed_fs(&s, "fs-1", "available");
1877 let err = s
1878 .create_mount_target(
1879 &ctx(),
1880 &json!({ "FileSystemId": "fs-1", "SubnetId": "subnet-does-not-exist" }),
1881 )
1882 .err()
1883 .unwrap();
1884 assert_eq!(err.code(), "SubnetNotFound");
1885 assert_eq!(err.status(), StatusCode::BAD_REQUEST);
1886 }
1887
1888 #[test]
1891 fn create_mount_target_without_ec2_synthesizes_subnet() {
1892 let s = svc();
1893 seed_fs(&s, "fs-1", "available");
1894 let resp = s
1895 .create_mount_target(
1896 &ctx(),
1897 &json!({ "FileSystemId": "fs-1", "SubnetId": "subnet-synth" }),
1898 )
1899 .unwrap();
1900 assert!(resp.status.is_success());
1901 }
1902
1903 #[test]
1906 fn create_mount_target_on_creating_fs_is_incorrect_lifecycle() {
1907 let s = svc();
1908 seed_fs(&s, "fs-1", "creating");
1909 let err = s
1910 .create_mount_target(
1911 &ctx(),
1912 &json!({ "FileSystemId": "fs-1", "SubnetId": "subnet-1" }),
1913 )
1914 .err()
1915 .unwrap();
1916 assert_eq!(err.code(), "IncorrectFileSystemLifeCycleState");
1917 assert_eq!(err.status(), StatusCode::CONFLICT);
1918 }
1919
1920 #[test]
1922 fn delete_file_system_with_access_point_is_in_use() {
1923 let s = svc();
1924 seed_fs(&s, "fs-1", "available");
1925 {
1926 let mut g = s.state.write();
1927 let d = g.get_or_create("000000000000");
1928 d.access_points.insert(
1929 "fsap-1".to_string(),
1930 json!({ "AccessPointId": "fsap-1", "FileSystemId": "fs-1" }),
1931 );
1932 }
1933 let err = s.delete_file_system(&ctx(), "fs-1").err().unwrap();
1934 assert_eq!(err.code(), "FileSystemInUse");
1935 assert_eq!(err.status(), StatusCode::CONFLICT);
1936 }
1937
1938 #[test]
1941 fn describe_replication_missing_is_replication_not_found() {
1942 let s = svc();
1943 seed_fs(&s, "fs-1", "available");
1944 let err = s
1945 .describe_replication_configurations(
1946 &ctx(),
1947 &[("FileSystemId".to_string(), "fs-1".to_string())],
1948 )
1949 .err()
1950 .unwrap();
1951 assert_eq!(err.code(), "ReplicationNotFound");
1952 assert_eq!(err.status(), StatusCode::NOT_FOUND);
1953 }
1954
1955 #[test]
1957 fn describe_replication_no_filter_is_empty_ok() {
1958 let s = svc();
1959 let resp = s.describe_replication_configurations(&ctx(), &[]).unwrap();
1960 assert!(resp.status.is_success());
1961 assert_eq!(
1962 body_value(&resp)["Replications"].as_array().unwrap().len(),
1963 0
1964 );
1965 }
1966
1967 #[test]
1970 fn create_replication_creates_the_destination_file_system() {
1971 let s = svc();
1972 seed_fs(&s, "fs-source", "available");
1973 let resp = s
1974 .create_replication_configuration(
1975 &ctx(),
1976 "fs-source",
1977 &json!({ "Destinations": [{ "Region": "us-west-2" }] }),
1978 )
1979 .unwrap();
1980 assert!(resp.status.is_success());
1981 let dest_id = body_value(&resp)["Destinations"][0]["FileSystemId"]
1982 .as_str()
1983 .unwrap()
1984 .to_string();
1985 assert!(dest_id.starts_with("fs-"));
1986
1987 let described = s
1989 .describe_file_systems(&ctx(), &[("FileSystemId".to_string(), dest_id.clone())])
1990 .unwrap();
1991 let fs = &body_value(&described)["FileSystems"][0];
1992 assert_eq!(fs["FileSystemId"], dest_id);
1993 assert_eq!(
1994 fs["FileSystemProtection"]["ReplicationOverwriteProtection"],
1995 "DISABLED"
1996 );
1997
1998 let repl = s
2000 .describe_replication_configurations(
2001 &ctx(),
2002 &[("FileSystemId".to_string(), "fs-source".to_string())],
2003 )
2004 .unwrap();
2005 assert!(repl.status.is_success());
2006 }
2007
2008 #[test]
2011 fn describe_mount_targets_two_filters_is_bad_request() {
2012 let s = svc();
2013 let err = s
2014 .describe_mount_targets(
2015 &ctx(),
2016 &[
2017 ("FileSystemId".to_string(), "fs-1".to_string()),
2018 ("MountTargetId".to_string(), "fsmt-1".to_string()),
2019 ],
2020 )
2021 .err()
2022 .unwrap();
2023 assert_eq!(err.code(), "BadRequest");
2024 assert_eq!(err.status(), StatusCode::BAD_REQUEST);
2025 }
2026
2027 #[test]
2030 fn create_file_system_provisioned_without_mibps_is_bad_request() {
2031 let s = svc();
2032 let err = s
2033 .create_file_system(
2034 &ctx(),
2035 &json!({ "CreationToken": "tok-1", "ThroughputMode": "provisioned" }),
2036 )
2037 .err()
2038 .unwrap();
2039 assert_eq!(err.code(), "BadRequest");
2040
2041 let err2 = s
2042 .create_file_system(
2043 &ctx(),
2044 &json!({
2045 "CreationToken": "tok-2",
2046 "ThroughputMode": "bursting",
2047 "ProvisionedThroughputInMibps": 128
2048 }),
2049 )
2050 .err()
2051 .unwrap();
2052 assert_eq!(err2.code(), "BadRequest");
2053
2054 let ok = s
2056 .create_file_system(
2057 &ctx(),
2058 &json!({
2059 "CreationToken": "tok-3",
2060 "ThroughputMode": "provisioned",
2061 "ProvisionedThroughputInMibps": 256
2062 }),
2063 )
2064 .unwrap();
2065 assert!(ok.status.is_success());
2066 }
2067
2068 #[test]
2071 fn describe_account_preferences_fresh_account_is_empty() {
2072 let s = svc();
2073 let resp = s.describe_account_preferences(&ctx()).unwrap();
2074 assert!(resp.status.is_success());
2075 let v = body_value(&resp);
2076 assert!(
2077 v.get("ResourceIdPreference").is_none(),
2078 "expected empty {{}}, got {v}"
2079 );
2080
2081 s.put_account_preferences(&ctx(), &json!({ "ResourceIdType": "SHORT_ID" }))
2083 .unwrap();
2084 let resp2 = s.describe_account_preferences(&ctx()).unwrap();
2085 let v2 = body_value(&resp2);
2086 assert_eq!(v2["ResourceIdPreference"]["ResourceIdType"], "SHORT_ID");
2087 }
2088
2089 #[test]
2092 fn access_point_tags_resync_agrees() {
2093 let s = svc();
2094 seed_fs(&s, "fs-1", "available");
2095 let created = s
2096 .create_access_point(
2097 &ctx(),
2098 &json!({ "ClientToken": "ct-1", "FileSystemId": "fs-1" }),
2099 )
2100 .unwrap();
2101 let apid = body_value(&created)["AccessPointId"]
2102 .as_str()
2103 .unwrap()
2104 .to_string();
2105
2106 s.tag_resource(
2107 &ctx(),
2108 &apid,
2109 &json!({ "Tags": [{ "Key": "Env", "Value": "prod" }] }),
2110 )
2111 .unwrap();
2112
2113 let embedded: Vec<(String, String)> = {
2115 let g = s.state.read();
2116 let ap = g
2117 .get("000000000000")
2118 .unwrap()
2119 .access_points
2120 .get(&apid)
2121 .unwrap()
2122 .clone();
2123 ap["Tags"]
2124 .as_array()
2125 .unwrap()
2126 .iter()
2127 .map(|t| {
2128 (
2129 t["Key"].as_str().unwrap().to_string(),
2130 t["Value"].as_str().unwrap().to_string(),
2131 )
2132 })
2133 .collect()
2134 };
2135
2136 let listed_resp = s.list_tags_for_resource(&ctx(), &apid, &[]).unwrap();
2138 let listed: Vec<(String, String)> = body_value(&listed_resp)["Tags"]
2139 .as_array()
2140 .unwrap()
2141 .iter()
2142 .map(|t| {
2143 (
2144 t["Key"].as_str().unwrap().to_string(),
2145 t["Value"].as_str().unwrap().to_string(),
2146 )
2147 })
2148 .collect();
2149
2150 assert_eq!(embedded, vec![("Env".to_string(), "prod".to_string())]);
2151 assert_eq!(
2152 embedded, listed,
2153 "embedded Tags must match ListTagsForResource"
2154 );
2155
2156 s.untag_resource(&ctx(), &apid, &[("tagKeys".to_string(), "Env".to_string())])
2158 .unwrap();
2159 let g = s.state.read();
2160 let ap = g
2161 .get("000000000000")
2162 .unwrap()
2163 .access_points
2164 .get(&apid)
2165 .unwrap();
2166 assert_eq!(ap["Tags"].as_array().unwrap().len(), 0);
2167 }
2168}