1use std::collections::BTreeMap;
9use std::sync::Arc;
10
11use async_trait::async_trait;
12use chrono::Utc;
13use http::StatusCode;
14use serde_json::{json, Value};
15use tokio::sync::Mutex as AsyncMutex;
16
17use fakecloud_core::service::{AwsRequest, AwsResponse, AwsService, AwsServiceError};
18use fakecloud_persistence::SnapshotStore;
19
20use crate::persistence::save_snapshot;
21use crate::state::{
22 Acl, Cluster, Endpoint, MemoryDbState, MultiRegionCluster, Node, ParameterGroup, ReservedNode,
23 Shard, SharedMemoryDbState, Snapshot, SubnetGroup, User, UserAuthentication,
24};
25
26pub const MEMORYDB_ACTIONS: &[&str] = &[
28 "BatchUpdateCluster",
29 "CopySnapshot",
30 "CreateACL",
31 "CreateCluster",
32 "CreateMultiRegionCluster",
33 "CreateParameterGroup",
34 "CreateSnapshot",
35 "CreateSubnetGroup",
36 "CreateUser",
37 "DeleteACL",
38 "DeleteCluster",
39 "DeleteMultiRegionCluster",
40 "DeleteParameterGroup",
41 "DeleteSnapshot",
42 "DeleteSubnetGroup",
43 "DeleteUser",
44 "DescribeACLs",
45 "DescribeClusters",
46 "DescribeEngineVersions",
47 "DescribeEvents",
48 "DescribeMultiRegionClusters",
49 "DescribeMultiRegionParameterGroups",
50 "DescribeMultiRegionParameters",
51 "DescribeParameterGroups",
52 "DescribeParameters",
53 "DescribeReservedNodes",
54 "DescribeReservedNodesOfferings",
55 "DescribeServiceUpdates",
56 "DescribeSnapshots",
57 "DescribeSubnetGroups",
58 "DescribeUsers",
59 "FailoverShard",
60 "ListAllowedMultiRegionClusterUpdates",
61 "ListAllowedNodeTypeUpdates",
62 "ListTags",
63 "PurchaseReservedNodesOffering",
64 "ResetParameterGroup",
65 "TagResource",
66 "UntagResource",
67 "UpdateACL",
68 "UpdateCluster",
69 "UpdateMultiRegionCluster",
70 "UpdateParameterGroup",
71 "UpdateSubnetGroup",
72 "UpdateUser",
73];
74
75const DEFAULT_ENGINE: &str = "redis";
76const DEFAULT_ENGINE_VERSION: &str = "7.1";
77const DEFAULT_PATCH_VERSION: &str = "7.1.1";
78
79pub struct MemoryDbService {
80 state: SharedMemoryDbState,
81 snapshot_store: Option<Arc<dyn SnapshotStore>>,
82 snapshot_lock: Arc<AsyncMutex<()>>,
83}
84
85impl MemoryDbService {
86 pub fn new(state: SharedMemoryDbState) -> Self {
87 Self {
88 state,
89 snapshot_store: None,
90 snapshot_lock: Arc::new(AsyncMutex::new(())),
91 }
92 }
93
94 pub fn with_snapshot_store(mut self, store: Arc<dyn SnapshotStore>) -> Self {
95 self.snapshot_store = Some(store);
96 self
97 }
98
99 async fn save(&self) {
100 save_snapshot(
101 &self.state,
102 self.snapshot_store.clone(),
103 &self.snapshot_lock,
104 )
105 .await;
106 }
107}
108
109#[async_trait]
110impl AwsService for MemoryDbService {
111 fn service_name(&self) -> &str {
112 "memorydb"
113 }
114
115 async fn handle(&self, request: AwsRequest) -> Result<AwsResponse, AwsServiceError> {
116 let mutates = is_mutating(request.action.as_str());
117 let result = dispatch(self, &request);
118 if mutates && matches!(result.as_ref(), Ok(resp) if resp.status.is_success()) {
119 self.save().await;
120 }
121 result
122 }
123
124 fn supported_actions(&self) -> &[&str] {
125 MEMORYDB_ACTIONS
126 }
127}
128
129fn is_mutating(action: &str) -> bool {
130 action.starts_with("Create")
134 || action.starts_with("Delete")
135 || action.starts_with("Update")
136 || action.starts_with("Tag")
137 || action.starts_with("Untag")
138 || action.starts_with("Copy")
139 || action.starts_with("Reset")
140 || action.starts_with("Purchase")
141}
142
143fn dispatch(s: &MemoryDbService, req: &AwsRequest) -> Result<AwsResponse, AwsServiceError> {
144 match req.action.as_str() {
145 "CreateCluster" => s.create_cluster(req),
146 "DescribeClusters" => s.describe_clusters(req),
147 "UpdateCluster" => s.update_cluster(req),
148 "DeleteCluster" => s.delete_cluster(req),
149 "BatchUpdateCluster" => s.batch_update_cluster(req),
150 "FailoverShard" => s.failover_shard(req),
151 "CreateACL" => s.create_acl(req),
152 "DescribeACLs" => s.describe_acls(req),
153 "UpdateACL" => s.update_acl(req),
154 "DeleteACL" => s.delete_acl(req),
155 "CreateUser" => s.create_user(req),
156 "DescribeUsers" => s.describe_users(req),
157 "UpdateUser" => s.update_user(req),
158 "DeleteUser" => s.delete_user(req),
159 "CreateParameterGroup" => s.create_parameter_group(req),
160 "DescribeParameterGroups" => s.describe_parameter_groups(req),
161 "UpdateParameterGroup" => s.update_parameter_group(req),
162 "ResetParameterGroup" => s.reset_parameter_group(req),
163 "DeleteParameterGroup" => s.delete_parameter_group(req),
164 "DescribeParameters" => s.describe_parameters(req),
165 "CreateSubnetGroup" => s.create_subnet_group(req),
166 "DescribeSubnetGroups" => s.describe_subnet_groups(req),
167 "UpdateSubnetGroup" => s.update_subnet_group(req),
168 "DeleteSubnetGroup" => s.delete_subnet_group(req),
169 "CreateSnapshot" => s.create_snapshot(req),
170 "CopySnapshot" => s.copy_snapshot(req),
171 "DescribeSnapshots" => s.describe_snapshots(req),
172 "DeleteSnapshot" => s.delete_snapshot(req),
173 "CreateMultiRegionCluster" => s.create_multi_region_cluster(req),
174 "DescribeMultiRegionClusters" => s.describe_multi_region_clusters(req),
175 "UpdateMultiRegionCluster" => s.update_multi_region_cluster(req),
176 "DeleteMultiRegionCluster" => s.delete_multi_region_cluster(req),
177 "ListAllowedMultiRegionClusterUpdates" => s.list_allowed_mr_updates(req),
178 "DescribeMultiRegionParameterGroups" => s.describe_mr_parameter_groups(req),
179 "DescribeMultiRegionParameters" => s.describe_mr_parameters(req),
180 "TagResource" => s.tag_resource(req),
181 "UntagResource" => s.untag_resource(req),
182 "ListTags" => s.list_tags(req),
183 "DescribeEngineVersions" => s.describe_engine_versions(req),
184 "DescribeEvents" => s.describe_events(req),
185 "DescribeServiceUpdates" => s.describe_service_updates(req),
186 "DescribeReservedNodes" => s.describe_reserved_nodes(req),
187 "DescribeReservedNodesOfferings" => s.describe_reserved_nodes_offerings(req),
188 "PurchaseReservedNodesOffering" => s.purchase_reserved_nodes_offering(req),
189 "ListAllowedNodeTypeUpdates" => s.list_allowed_node_type_updates(req),
190 _ => Err(AwsServiceError::action_not_implemented(
191 s.service_name(),
192 &req.action,
193 )),
194 }
195}
196
197fn ok(v: Value) -> Result<AwsResponse, AwsServiceError> {
200 Ok(AwsResponse::json_value(StatusCode::OK, v))
201}
202
203fn parse(req: &AwsRequest) -> Result<Value, AwsServiceError> {
204 if req.body.is_empty() {
205 return Ok(json!({}));
206 }
207 serde_json::from_slice(&req.body).map_err(|e| {
208 fault(
209 "InvalidParameterValueException",
210 &format!("malformed body: {e}"),
211 )
212 })
213}
214
215fn fault(code: &str, msg: &str) -> AwsServiceError {
216 let status = if code.ends_with("NotFoundFault") && code != "ServiceLinkedRoleNotFoundFault" {
222 StatusCode::NOT_FOUND
223 } else {
224 StatusCode::BAD_REQUEST
225 };
226 AwsServiceError::aws_error(status, code, msg)
227}
228
229fn req_str<'a>(b: &'a Value, f: &str) -> Result<&'a str, AwsServiceError> {
230 b.get(f).and_then(Value::as_str).ok_or_else(|| {
231 fault(
232 "InvalidParameterValueException",
233 &format!("{f} is required."),
234 )
235 })
236}
237
238fn opt_str(b: &Value, f: &str) -> Option<String> {
239 b.get(f).and_then(Value::as_str).map(str::to_string)
240}
241
242fn str_list(b: &Value, f: &str) -> Vec<String> {
243 b.get(f)
244 .and_then(Value::as_array)
245 .map(|a| {
246 a.iter()
247 .filter_map(|x| x.as_str().map(str::to_string))
248 .collect()
249 })
250 .unwrap_or_default()
251}
252
253fn arn(kind: &str, region: &str, account: &str, name: &str) -> String {
254 format!("arn:aws:memorydb:{region}:{account}:{kind}/{name}")
255}
256
257fn int_field(
261 b: &Value,
262 field: &str,
263 default: i64,
264 min: i64,
265 max: i64,
266) -> Result<i32, AwsServiceError> {
267 let v = b.get(field).and_then(Value::as_i64).unwrap_or(default);
268 if !(min..=max).contains(&v) {
269 return Err(fault(
270 "InvalidParameterValueException",
271 &format!("{field} must be between {min} and {max}."),
272 ));
273 }
274 Ok(v as i32)
275}
276
277fn parse_tags(b: &Value) -> BTreeMap<String, String> {
278 let mut out = BTreeMap::new();
279 if let Some(arr) = b.get("Tags").and_then(Value::as_array) {
280 for t in arr {
281 if let (Some(k), Some(v)) = (
282 t.get("Key").and_then(Value::as_str),
283 t.get("Value").and_then(Value::as_str),
284 ) {
285 out.insert(k.to_string(), v.to_string());
286 }
287 }
288 }
289 out
290}
291
292fn tags_json(tags: &BTreeMap<String, String>) -> Vec<Value> {
293 tags.iter()
294 .map(|(k, v)| json!({ "Key": k, "Value": v }))
295 .collect()
296}
297
298fn page_start(b: &Value) -> Option<usize> {
301 match b.get("NextToken").and_then(Value::as_str) {
302 None | Some("") => Some(0),
303 Some(t) => t.parse::<usize>().ok(),
304 }
305}
306
307fn max_results(b: &Value, default: usize) -> usize {
308 b.get("MaxResults")
309 .and_then(Value::as_i64)
310 .map(|n| n.max(1) as usize)
311 .unwrap_or(default)
312}
313
314fn paginate(items: Vec<Value>, b: &Value, default_max: usize) -> (Vec<Value>, Option<String>) {
316 let Some(start) = page_start(b) else {
317 return (Vec::new(), None);
318 };
319 let max = max_results(b, default_max);
320 let total = items.len();
321 if start >= total {
322 return (Vec::new(), None);
323 }
324 let end = start.saturating_add(max).min(total);
325 let next = (end < total).then(|| end.to_string());
326 (items[start..end].to_vec(), next)
327}
328
329fn endpoint_json(e: &Endpoint) -> Value {
332 json!({ "Address": e.address, "Port": e.port })
333}
334
335fn node_json(n: &Node) -> Value {
336 json!({
337 "Name": n.name,
338 "Status": n.status,
339 "AvailabilityZone": n.availability_zone,
340 "CreateTime": n.create_time.timestamp(),
341 "Endpoint": endpoint_json(&n.endpoint),
342 })
343}
344
345fn shard_json(sh: &Shard) -> Value {
346 json!({
347 "Name": sh.name,
348 "Status": sh.status,
349 "Slots": sh.slots,
350 "Nodes": sh.nodes.iter().map(node_json).collect::<Vec<_>>(),
351 "NumberOfNodes": sh.number_of_nodes,
352 })
353}
354
355fn cluster_json(c: &Cluster) -> Value {
356 json!({
357 "Name": c.name,
358 "Description": c.description,
359 "Status": c.status,
360 "NumberOfShards": c.number_of_shards,
361 "Shards": c.shards.iter().map(shard_json).collect::<Vec<_>>(),
362 "AvailabilityMode": c.availability_mode,
363 "ClusterEndpoint": endpoint_json(&c.cluster_endpoint),
364 "NodeType": c.node_type,
365 "Engine": c.engine,
366 "EngineVersion": c.engine_version,
367 "EnginePatchVersion": c.engine_patch_version,
368 "ParameterGroupName": c.parameter_group_name,
369 "ParameterGroupStatus": c.parameter_group_status,
370 "SecurityGroups": c.security_group_ids.iter().map(|id| json!({ "SecurityGroupId": id, "Status": "active" })).collect::<Vec<_>>(),
371 "SubnetGroupName": c.subnet_group_name,
372 "TLSEnabled": c.tls_enabled,
373 "KmsKeyId": c.kms_key_id,
374 "ARN": c.arn,
375 "SnsTopicArn": c.sns_topic_arn,
376 "SnsTopicStatus": c.sns_topic_arn.as_ref().map(|_| "active"),
377 "SnapshotRetentionLimit": c.snapshot_retention_limit,
378 "MaintenanceWindow": c.maintenance_window,
379 "SnapshotWindow": c.snapshot_window,
380 "ACLName": c.acl_name,
381 "AutoMinorVersionUpgrade": c.auto_minor_version_upgrade,
382 "DataTiering": c.data_tiering,
383 "NetworkType": c.network_type,
384 "IpDiscovery": c.ip_discovery,
385 })
386}
387
388fn acl_json(a: &Acl) -> Value {
389 json!({
390 "Name": a.name,
391 "Status": a.status,
392 "UserNames": a.user_names,
393 "MinimumEngineVersion": a.minimum_engine_version,
394 "Clusters": a.clusters,
395 "ARN": a.arn,
396 })
397}
398
399fn user_json(u: &User) -> Value {
400 json!({
401 "Name": u.name,
402 "Status": u.status,
403 "AccessString": u.access_string,
404 "ACLNames": u.acl_names,
405 "MinimumEngineVersion": u.minimum_engine_version,
406 "Authentication": {
407 "Type": u.authentication.type_,
408 "PasswordCount": u.authentication.password_count,
409 },
410 "ARN": u.arn,
411 })
412}
413
414fn pg_json(p: &ParameterGroup) -> Value {
415 json!({
416 "Name": p.name,
417 "Family": p.family,
418 "Description": p.description,
419 "ARN": p.arn,
420 })
421}
422
423fn sg_json(g: &SubnetGroup) -> Value {
424 json!({
425 "Name": g.name,
426 "Description": g.description,
427 "VpcId": g.vpc_id,
428 "Subnets": g.subnet_ids.iter().map(|id| json!({
429 "Identifier": id,
430 "AvailabilityZone": { "Name": "us-east-1a" },
431 "SupportedNetworkTypes": ["ipv4"],
432 })).collect::<Vec<_>>(),
433 "ARN": g.arn,
434 "SupportedNetworkTypes": g.supported_network_types,
435 })
436}
437
438fn snapshot_json(s: &Snapshot) -> Value {
439 json!({
440 "Name": s.name,
441 "Status": s.status,
442 "Source": s.source,
443 "KmsKeyId": s.kms_key_id,
444 "ARN": s.arn,
445 "ClusterConfiguration": s.cluster_configuration,
446 "DataTiering": s.data_tiering,
447 })
448}
449
450fn mrc_json(m: &MultiRegionCluster) -> Value {
451 json!({
452 "MultiRegionClusterName": m.name,
453 "Description": m.description,
454 "Status": m.status,
455 "NodeType": m.node_type,
456 "Engine": m.engine,
457 "EngineVersion": m.engine_version,
458 "NumberOfShards": m.number_of_shards,
459 "Clusters": m.clusters,
460 "MultiRegionParameterGroupName": m.multi_region_parameter_group_name,
461 "TLSEnabled": m.tls_enabled,
462 "ARN": m.arn,
463 })
464}
465
466fn reserved_node_json(r: &ReservedNode) -> Value {
467 json!({
468 "ReservationId": r.reservation_id,
469 "ReservedNodesOfferingId": r.reserved_nodes_offering_id,
470 "NodeType": r.node_type,
471 "StartTime": r.start_time.timestamp(),
472 "Duration": r.duration,
473 "FixedPrice": r.fixed_price,
474 "NodeCount": r.node_count,
475 "OfferingType": r.offering_type,
476 "State": r.state,
477 "ARN": r.arn,
478 })
479}
480
481impl MemoryDbService {
484 fn create_cluster(&self, req: &AwsRequest) -> Result<AwsResponse, AwsServiceError> {
485 let b = parse(req)?;
486 let name = req_str(&b, "ClusterName")?.to_string();
487 let node_type = req_str(&b, "NodeType")?.to_string();
488 let acl_name = req_str(&b, "ACLName")?.to_string();
489 let mut accounts = self.state.write();
490 let st = accounts.get_or_create(&req.account_id);
491 if st.clusters.contains_key(&name) {
492 return Err(fault(
493 "ClusterAlreadyExistsFault",
494 &format!("Cluster {name} already exists."),
495 ));
496 }
497 let num_shards = b.get("NumShards").and_then(Value::as_i64).unwrap_or(1);
501 if !(1..=500).contains(&num_shards) {
502 return Err(fault(
503 "InvalidParameterValueException",
504 "NumShards must be between 1 and 500.",
505 ));
506 }
507 let num_shards = num_shards as i32;
508 let replicas = b
509 .get("NumReplicasPerShard")
510 .and_then(Value::as_i64)
511 .unwrap_or(1);
512 if !(0..=5).contains(&replicas) {
513 return Err(fault(
514 "InvalidParameterValueException",
515 "NumReplicasPerShard must be between 0 and 5.",
516 ));
517 }
518 let replicas = replicas as i32;
519 let snapshot_retention_limit = int_field(&b, "SnapshotRetentionLimit", 0, 0, 35)?;
521 let port = b.get("Port").and_then(Value::as_i64).unwrap_or(6379) as i32;
522 let engine = opt_str(&b, "Engine").unwrap_or_else(|| DEFAULT_ENGINE.to_string());
523 let engine_version =
524 opt_str(&b, "EngineVersion").unwrap_or_else(|| DEFAULT_ENGINE_VERSION.to_string());
525 let cluster_arn = arn("cluster", &req.region, &req.account_id, &name);
526 let endpoint = Endpoint {
527 address: format!(
528 "clustercfg.{name}.abc123.memorydb.{}.amazonaws.com",
529 req.region
530 ),
531 port,
532 };
533 let shards = (0..num_shards)
534 .map(|i| {
535 let sname = format!("{:04}", i + 1);
536 let nodes = (0..=replicas)
537 .map(|j| Node {
538 name: format!("{name}-{sname}-{:03}", j + 1),
539 status: "creating".to_string(),
540 availability_zone: format!("{}a", req.region),
541 create_time: Utc::now(),
542 endpoint: Endpoint {
543 address: format!(
544 "{name}-{sname}-{:03}.{name}.abc123.memorydb.{}.amazonaws.com",
545 j + 1,
546 req.region
547 ),
548 port,
549 },
550 })
551 .collect::<Vec<_>>();
552 Shard {
553 name: sname,
554 status: "creating".to_string(),
555 slots: "0-16383".to_string(),
556 number_of_nodes: nodes.len() as i32,
557 nodes,
558 }
559 })
560 .collect();
561 let cluster = Cluster {
562 name: name.clone(),
563 description: opt_str(&b, "Description"),
564 status: "creating".to_string(),
565 number_of_shards: num_shards,
566 shards,
567 node_type,
568 engine,
569 engine_version,
570 engine_patch_version: DEFAULT_PATCH_VERSION.to_string(),
571 parameter_group_name: opt_str(&b, "ParameterGroupName")
572 .unwrap_or_else(|| "default.memorydb-redis7".to_string()),
573 parameter_group_status: "in-sync".to_string(),
574 security_group_ids: str_list(&b, "SecurityGroupIds"),
575 subnet_group_name: opt_str(&b, "SubnetGroupName")
576 .unwrap_or_else(|| "default".to_string()),
577 tls_enabled: b.get("TLSEnabled").and_then(Value::as_bool).unwrap_or(true),
578 kms_key_id: opt_str(&b, "KmsKeyId"),
579 arn: cluster_arn.clone(),
580 sns_topic_arn: opt_str(&b, "SnsTopicArn"),
581 snapshot_retention_limit,
582 maintenance_window: opt_str(&b, "MaintenanceWindow")
583 .unwrap_or_else(|| "wed:08:00-wed:09:00".to_string()),
584 snapshot_window: opt_str(&b, "SnapshotWindow")
585 .unwrap_or_else(|| "05:00-06:00".to_string()),
586 acl_name,
587 auto_minor_version_upgrade: b
588 .get("AutoMinorVersionUpgrade")
589 .and_then(Value::as_bool)
590 .unwrap_or(true),
591 data_tiering: opt_str(&b, "DataTiering")
592 .map(|d| {
593 if d == "true" {
594 "true".to_string()
595 } else {
596 "false".to_string()
597 }
598 })
599 .unwrap_or_else(|| "false".to_string()),
600 availability_mode: if replicas > 0 {
601 "MultiAZ".to_string()
602 } else {
603 "SingleAZ".to_string()
604 },
605 cluster_endpoint: endpoint,
606 network_type: opt_str(&b, "NetworkType").unwrap_or_else(|| "ipv4".to_string()),
607 ip_discovery: opt_str(&b, "IpDiscovery").unwrap_or_else(|| "ipv4".to_string()),
608 port,
609 container_id: None,
610 created_at: Utc::now(),
611 };
612 let tags = parse_tags(&b);
613 if !tags.is_empty() {
614 st.tags.insert(cluster_arn, tags);
615 }
616 if let Some(acl) = st.acls.get_mut(&cluster.acl_name) {
619 if !acl.clusters.contains(&name) {
620 acl.clusters.push(name.clone());
621 }
622 }
623 st.clusters.insert(name, cluster.clone());
624 ok(json!({ "Cluster": cluster_json(&cluster) }))
625 }
626
627 fn describe_clusters(&self, req: &AwsRequest) -> Result<AwsResponse, AwsServiceError> {
628 let b = parse(req)?;
629 let mut accounts = self.state.write();
630 let st = accounts.get_or_create(&req.account_id);
631 for c in st.clusters.values_mut() {
633 if c.status == "creating" {
634 c.status = "available".to_string();
635 for sh in &mut c.shards {
636 sh.status = "available".to_string();
637 for n in &mut sh.nodes {
638 n.status = "available".to_string();
639 }
640 }
641 }
642 }
643 if let Some(name) = opt_str(&b, "ClusterName") {
644 let Some(c) = st.clusters.get(&name) else {
645 return Err(fault(
646 "ClusterNotFoundFault",
647 &format!("Cluster {name} not found."),
648 ));
649 };
650 return ok(json!({ "Clusters": [cluster_json(c)] }));
651 }
652 let items: Vec<Value> = st.clusters.values().map(cluster_json).collect();
653 let (page, next) = paginate(items, &b, 100);
654 ok(json!({ "Clusters": page, "NextToken": next }))
655 }
656
657 fn update_cluster(&self, req: &AwsRequest) -> Result<AwsResponse, AwsServiceError> {
658 let b = parse(req)?;
659 let name = req_str(&b, "ClusterName")?.to_string();
660 let mut accounts = self.state.write();
661 let st = accounts.get_or_create(&req.account_id);
662 let Some(c) = st.clusters.get_mut(&name) else {
663 return Err(fault(
664 "ClusterNotFoundFault",
665 &format!("Cluster {name} not found."),
666 ));
667 };
668 if let Some(d) = opt_str(&b, "Description") {
669 c.description = Some(d);
670 }
671 if let Some(v) = opt_str(&b, "EngineVersion") {
672 c.engine_version = v;
673 }
674 if let Some(v) = opt_str(&b, "Engine") {
675 c.engine = v;
676 }
677 if let Some(v) = opt_str(&b, "NodeType") {
678 c.node_type = v;
679 }
680 if let Some(v) = opt_str(&b, "ParameterGroupName") {
681 c.parameter_group_name = v;
682 }
683 let acl_change = opt_str(&b, "ACLName").map(|new_acl| {
686 let old_acl = c.acl_name.clone();
687 c.acl_name = new_acl.clone();
688 (old_acl, new_acl)
689 });
690 if let Some(v) = opt_str(&b, "MaintenanceWindow") {
691 c.maintenance_window = v;
692 }
693 if let Some(v) = opt_str(&b, "SnapshotWindow") {
694 c.snapshot_window = v;
695 }
696 if b.get("SnapshotRetentionLimit").is_some() {
697 c.snapshot_retention_limit = int_field(&b, "SnapshotRetentionLimit", 0, 0, 35)?;
698 }
699 if let Some(sg) = b.get("SecurityGroupIds").and_then(Value::as_array) {
700 c.security_group_ids = sg
701 .iter()
702 .filter_map(|x| x.as_str().map(str::to_string))
703 .collect();
704 }
705 let out = cluster_json(c);
706 if let Some((old_acl, new_acl)) = acl_change {
708 if old_acl != new_acl {
709 if let Some(a) = st.acls.get_mut(&old_acl) {
710 a.clusters.retain(|cn| cn != &name);
711 }
712 if let Some(a) = st.acls.get_mut(&new_acl) {
713 if !a.clusters.contains(&name) {
714 a.clusters.push(name.clone());
715 }
716 }
717 }
718 }
719 ok(json!({ "Cluster": out }))
720 }
721
722 fn delete_cluster(&self, req: &AwsRequest) -> Result<AwsResponse, AwsServiceError> {
723 let b = parse(req)?;
724 let name = req_str(&b, "ClusterName")?.to_string();
725 let mut accounts = self.state.write();
726 let st = accounts.get_or_create(&req.account_id);
727 let Some(mut c) = st.clusters.remove(&name) else {
728 return Err(fault(
729 "ClusterNotFoundFault",
730 &format!("Cluster {name} not found."),
731 ));
732 };
733 c.status = "deleting".to_string();
734 st.tags.remove(&c.arn);
735 if let Some(a) = st.acls.get_mut(&c.acl_name) {
737 a.clusters.retain(|cn| cn != &name);
738 }
739 ok(json!({ "Cluster": cluster_json(&c) }))
740 }
741
742 fn batch_update_cluster(&self, req: &AwsRequest) -> Result<AwsResponse, AwsServiceError> {
743 let b = parse(req)?;
744 let names = str_list(&b, "ClusterNames");
745 let accounts = self.state.read();
746 let st = accounts.get(&req.account_id);
747 let mut processed = Vec::new();
748 let mut unprocessed = Vec::new();
749 for n in &names {
750 match st.and_then(|s| s.clusters.get(n)) {
751 Some(c) => processed.push(cluster_json(c)),
752 None => unprocessed.push(json!({
755 "ClusterName": n,
756 "ErrorType": "ClusterNotFoundFault",
757 "ErrorMessage": format!("Cluster {n} not found."),
758 })),
759 }
760 }
761 ok(json!({ "ProcessedClusters": processed, "UnprocessedClusters": unprocessed }))
762 }
763
764 fn failover_shard(&self, req: &AwsRequest) -> Result<AwsResponse, AwsServiceError> {
765 let b = parse(req)?;
766 let name = req_str(&b, "ClusterName")?.to_string();
767 let shard_name = req_str(&b, "ShardName")?.to_string();
769 let accounts = self.state.read();
770 let st = accounts.get(&req.account_id);
771 let Some(c) = st.and_then(|s| s.clusters.get(&name)) else {
772 return Err(fault(
773 "ClusterNotFoundFault",
774 &format!("Cluster {name} not found."),
775 ));
776 };
777 if !c.shards.iter().any(|s| s.name == shard_name) {
778 return Err(fault(
779 "ShardNotFoundFault",
780 &format!("Shard {shard_name} not found in cluster {name}."),
781 ));
782 }
783 ok(json!({ "Cluster": cluster_json(c) }))
784 }
785
786 fn create_acl(&self, req: &AwsRequest) -> Result<AwsResponse, AwsServiceError> {
787 let b = parse(req)?;
788 let name = req_str(&b, "ACLName")?.to_string();
789 let mut accounts = self.state.write();
790 let st = accounts.get_or_create(&req.account_id);
791 if st.acls.contains_key(&name) {
792 return Err(fault(
793 "ACLAlreadyExistsFault",
794 &format!("ACL {name} already exists."),
795 ));
796 }
797 let acl_arn = arn("acl", &req.region, &req.account_id, &name);
798 let acl = Acl {
799 name: name.clone(),
800 status: "active".to_string(),
801 user_names: str_list(&b, "UserNames"),
802 minimum_engine_version: "6.2".to_string(),
803 clusters: vec![],
804 arn: acl_arn.clone(),
805 };
806 let tags = parse_tags(&b);
807 if !tags.is_empty() {
808 st.tags.insert(acl_arn, tags);
809 }
810 st.acls.insert(name.clone(), acl.clone());
811 for u in &acl.user_names {
813 if let Some(user) = st.users.get_mut(u) {
814 if !user.acl_names.contains(&name) {
815 user.acl_names.push(name.clone());
816 }
817 }
818 }
819 ok(json!({ "ACL": acl_json(&acl) }))
820 }
821
822 fn describe_acls(&self, req: &AwsRequest) -> Result<AwsResponse, AwsServiceError> {
823 let b = parse(req)?;
824 let accounts = self.state.read();
825 let st = accounts.get(&req.account_id);
826 let Some(st) = st else {
827 return ok(json!({ "ACLs": [] }));
828 };
829 if let Some(name) = opt_str(&b, "ACLName") {
830 let Some(a) = st.acls.get(&name) else {
831 return Err(fault("ACLNotFoundFault", &format!("ACL {name} not found.")));
832 };
833 return ok(json!({ "ACLs": [acl_json(a)] }));
834 }
835 let items: Vec<Value> = st.acls.values().map(acl_json).collect();
836 let (page, next) = paginate(items, &b, 100);
837 ok(json!({ "ACLs": page, "NextToken": next }))
838 }
839
840 fn update_acl(&self, req: &AwsRequest) -> Result<AwsResponse, AwsServiceError> {
841 let b = parse(req)?;
842 let name = req_str(&b, "ACLName")?.to_string();
843 let mut accounts = self.state.write();
844 let st = accounts.get_or_create(&req.account_id);
845 let Some(a) = st.acls.get_mut(&name) else {
846 return Err(fault("ACLNotFoundFault", &format!("ACL {name} not found.")));
847 };
848 let mut added = Vec::new();
849 for u in str_list(&b, "UserNamesToAdd") {
850 if !a.user_names.contains(&u) {
851 a.user_names.push(u.clone());
852 added.push(u);
853 }
854 }
855 let remove = str_list(&b, "UserNamesToRemove");
856 let removed: Vec<String> = a
857 .user_names
858 .iter()
859 .filter(|u| remove.contains(u))
860 .cloned()
861 .collect();
862 a.user_names.retain(|u| !remove.contains(u));
863 let out = acl_json(a);
864 for u in &added {
866 if let Some(user) = st.users.get_mut(u) {
867 if !user.acl_names.contains(&name) {
868 user.acl_names.push(name.clone());
869 }
870 }
871 }
872 for u in &removed {
873 if let Some(user) = st.users.get_mut(u) {
874 user.acl_names.retain(|an| an != &name);
875 }
876 }
877 ok(json!({ "ACL": out }))
878 }
879
880 fn delete_acl(&self, req: &AwsRequest) -> Result<AwsResponse, AwsServiceError> {
881 let b = parse(req)?;
882 let name = req_str(&b, "ACLName")?.to_string();
883 let mut accounts = self.state.write();
884 let st = accounts.get_or_create(&req.account_id);
885 let Some(mut a) = st.acls.remove(&name) else {
886 return Err(fault("ACLNotFoundFault", &format!("ACL {name} not found.")));
887 };
888 a.status = "deleting".to_string();
889 st.tags.remove(&a.arn);
890 for u in &a.user_names {
892 if let Some(user) = st.users.get_mut(u) {
893 user.acl_names.retain(|an| an != &name);
894 }
895 }
896 ok(json!({ "ACL": acl_json(&a) }))
897 }
898
899 fn create_user(&self, req: &AwsRequest) -> Result<AwsResponse, AwsServiceError> {
900 let b = parse(req)?;
901 let name = req_str(&b, "UserName")?.to_string();
902 validate_user_name(&name)?;
903 let access_string = req_str(&b, "AccessString")?.to_string();
904 let auth = b.get("AuthenticationMode").ok_or_else(|| {
908 fault(
909 "InvalidParameterValueException",
910 "AuthenticationMode is required.",
911 )
912 })?;
913 let auth_type = auth
914 .get("Type")
915 .and_then(Value::as_str)
916 .ok_or_else(|| {
917 fault(
918 "InvalidParameterValueException",
919 "AuthenticationMode.Type is required.",
920 )
921 })?
922 .to_string();
923 if !matches!(auth_type.as_str(), "password" | "iam") {
924 return Err(fault(
925 "InvalidParameterValueException",
926 &format!(
927 "Invalid AuthenticationMode.Type: {auth_type}. Valid values: password, iam."
928 ),
929 ));
930 }
931 let pw_count = auth
932 .get("Passwords")
933 .and_then(Value::as_array)
934 .map(|a| a.len() as i32)
935 .unwrap_or(0);
936 if auth_type == "password" && pw_count == 0 {
937 return Err(fault(
938 "InvalidParameterValueException",
939 "A password-authenticated user requires at least one password.",
940 ));
941 }
942 let mut accounts = self.state.write();
943 let st = accounts.get_or_create(&req.account_id);
944 if st.users.contains_key(&name) {
945 return Err(fault(
946 "UserAlreadyExistsFault",
947 &format!("User {name} already exists."),
948 ));
949 }
950 let user_arn = arn("user", &req.region, &req.account_id, &name);
951 let user = User {
952 name: name.clone(),
953 status: "active".to_string(),
954 access_string,
955 acl_names: vec![],
956 minimum_engine_version: "6.2".to_string(),
957 authentication: UserAuthentication {
958 type_: if auth_type == "iam" {
959 "iam".to_string()
960 } else {
961 auth_type
962 },
963 password_count: pw_count,
964 },
965 arn: user_arn.clone(),
966 };
967 let tags = parse_tags(&b);
968 if !tags.is_empty() {
969 st.tags.insert(user_arn, tags);
970 }
971 st.users.insert(name, user.clone());
972 ok(json!({ "User": user_json(&user) }))
973 }
974
975 fn describe_users(&self, req: &AwsRequest) -> Result<AwsResponse, AwsServiceError> {
976 let b = parse(req)?;
977 let accounts = self.state.read();
978 let st = accounts.get(&req.account_id);
979 let Some(st) = st else {
980 return ok(json!({ "Users": [] }));
981 };
982 if let Some(name) = opt_str(&b, "UserName") {
983 let Some(u) = st.users.get(&name) else {
984 return Err(fault(
985 "UserNotFoundFault",
986 &format!("User {name} not found."),
987 ));
988 };
989 return ok(json!({ "Users": [user_json(u)] }));
990 }
991 let items: Vec<Value> = st.users.values().map(user_json).collect();
992 let (page, next) = paginate(items, &b, 100);
993 ok(json!({ "Users": page, "NextToken": next }))
994 }
995
996 fn update_user(&self, req: &AwsRequest) -> Result<AwsResponse, AwsServiceError> {
997 let b = parse(req)?;
998 let name = req_str(&b, "UserName")?.to_string();
999 let mut accounts = self.state.write();
1000 let st = accounts.get_or_create(&req.account_id);
1001 let Some(u) = st.users.get_mut(&name) else {
1002 return Err(fault(
1003 "UserNotFoundFault",
1004 &format!("User {name} not found."),
1005 ));
1006 };
1007 if let Some(a) = opt_str(&b, "AccessString") {
1008 u.access_string = a;
1009 }
1010 if let Some(auth) = b.get("AuthenticationMode") {
1011 if let Some(t) = auth.get("Type").and_then(Value::as_str) {
1012 u.authentication.type_ = t.to_string();
1013 }
1014 if let Some(p) = auth.get("Passwords").and_then(Value::as_array) {
1015 u.authentication.password_count = p.len() as i32;
1016 }
1017 }
1018 let out = user_json(u);
1019 ok(json!({ "User": out }))
1020 }
1021
1022 fn delete_user(&self, req: &AwsRequest) -> Result<AwsResponse, AwsServiceError> {
1023 let b = parse(req)?;
1024 let name = req_str(&b, "UserName")?.to_string();
1025 validate_user_name(&name)?;
1026 let mut accounts = self.state.write();
1027 let st = accounts.get_or_create(&req.account_id);
1028 let Some(mut u) = st.users.remove(&name) else {
1029 return Err(fault(
1030 "UserNotFoundFault",
1031 &format!("User {name} not found."),
1032 ));
1033 };
1034 u.status = "deleting".to_string();
1035 st.tags.remove(&u.arn);
1036 ok(json!({ "User": user_json(&u) }))
1037 }
1038
1039 fn create_parameter_group(&self, req: &AwsRequest) -> Result<AwsResponse, AwsServiceError> {
1040 let b = parse(req)?;
1041 let name = req_str(&b, "ParameterGroupName")?.to_string();
1042 let family = req_str(&b, "Family")?.to_string();
1043 let mut accounts = self.state.write();
1044 let st = accounts.get_or_create(&req.account_id);
1045 if st.parameter_groups.contains_key(&name) {
1046 return Err(fault(
1047 "ParameterGroupAlreadyExistsFault",
1048 &format!("Parameter group {name} already exists."),
1049 ));
1050 }
1051 let pg_arn = arn("parametergroup", &req.region, &req.account_id, &name);
1052 let pg = ParameterGroup {
1053 name: name.clone(),
1054 family,
1055 description: opt_str(&b, "Description").unwrap_or_default(),
1056 arn: pg_arn.clone(),
1057 parameters: BTreeMap::new(),
1058 };
1059 let tags = parse_tags(&b);
1060 if !tags.is_empty() {
1061 st.tags.insert(pg_arn, tags);
1062 }
1063 st.parameter_groups.insert(name, pg.clone());
1064 ok(json!({ "ParameterGroup": pg_json(&pg) }))
1065 }
1066
1067 fn describe_parameter_groups(&self, req: &AwsRequest) -> Result<AwsResponse, AwsServiceError> {
1068 let b = parse(req)?;
1069 let accounts = self.state.read();
1070 let st = accounts.get(&req.account_id);
1071 let Some(st) = st else {
1072 return ok(json!({ "ParameterGroups": [] }));
1073 };
1074 if let Some(name) = opt_str(&b, "ParameterGroupName") {
1075 let Some(p) = st.parameter_groups.get(&name) else {
1076 return Err(fault(
1077 "ParameterGroupNotFoundFault",
1078 &format!("Parameter group {name} not found."),
1079 ));
1080 };
1081 return ok(json!({ "ParameterGroups": [pg_json(p)] }));
1082 }
1083 let items: Vec<Value> = st.parameter_groups.values().map(pg_json).collect();
1084 let (page, next) = paginate(items, &b, 100);
1085 ok(json!({ "ParameterGroups": page, "NextToken": next }))
1086 }
1087
1088 fn update_parameter_group(&self, req: &AwsRequest) -> Result<AwsResponse, AwsServiceError> {
1089 let b = parse(req)?;
1090 let name = req_str(&b, "ParameterGroupName")?.to_string();
1091 let mut accounts = self.state.write();
1092 let st = accounts.get_or_create(&req.account_id);
1093 let Some(p) = st.parameter_groups.get_mut(&name) else {
1094 return Err(fault(
1095 "ParameterGroupNotFoundFault",
1096 &format!("Parameter group {name} not found."),
1097 ));
1098 };
1099 if let Some(arr) = b.get("ParameterNameValues").and_then(Value::as_array) {
1100 for pv in arr {
1101 if let (Some(pn), Some(pval)) = (
1102 pv.get("ParameterName").and_then(Value::as_str),
1103 pv.get("ParameterValue").and_then(Value::as_str),
1104 ) {
1105 p.parameters.insert(pn.to_string(), pval.to_string());
1106 }
1107 }
1108 }
1109 let out = pg_json(p);
1110 ok(json!({ "ParameterGroup": out }))
1111 }
1112
1113 fn reset_parameter_group(&self, req: &AwsRequest) -> Result<AwsResponse, AwsServiceError> {
1114 let b = parse(req)?;
1115 let name = req_str(&b, "ParameterGroupName")?.to_string();
1116 let mut accounts = self.state.write();
1117 let st = accounts.get_or_create(&req.account_id);
1118 let Some(p) = st.parameter_groups.get_mut(&name) else {
1119 return Err(fault(
1120 "ParameterGroupNotFoundFault",
1121 &format!("Parameter group {name} not found."),
1122 ));
1123 };
1124 let all = b
1125 .get("AllParameters")
1126 .and_then(Value::as_bool)
1127 .unwrap_or(false);
1128 if all {
1129 p.parameters.clear();
1130 } else {
1131 for pv in b
1132 .get("ParameterNames")
1133 .and_then(Value::as_array)
1134 .into_iter()
1135 .flatten()
1136 {
1137 if let Some(pn) = pv.as_str() {
1138 p.parameters.remove(pn);
1139 }
1140 }
1141 }
1142 let out = pg_json(p);
1143 ok(json!({ "ParameterGroup": out }))
1144 }
1145
1146 fn delete_parameter_group(&self, req: &AwsRequest) -> Result<AwsResponse, AwsServiceError> {
1147 let b = parse(req)?;
1148 let name = req_str(&b, "ParameterGroupName")?.to_string();
1149 let mut accounts = self.state.write();
1150 let st = accounts.get_or_create(&req.account_id);
1151 let Some(p) = st.parameter_groups.remove(&name) else {
1152 return Err(fault(
1153 "ParameterGroupNotFoundFault",
1154 &format!("Parameter group {name} not found."),
1155 ));
1156 };
1157 st.tags.remove(&p.arn);
1158 ok(json!({ "ParameterGroup": pg_json(&p) }))
1159 }
1160
1161 fn describe_parameters(&self, req: &AwsRequest) -> Result<AwsResponse, AwsServiceError> {
1162 let b = parse(req)?;
1163 let name = req_str(&b, "ParameterGroupName")?.to_string();
1164 let accounts = self.state.read();
1165 let st = accounts.get(&req.account_id);
1166 let Some(p) = st.and_then(|s| s.parameter_groups.get(&name)) else {
1167 return Err(fault(
1168 "ParameterGroupNotFoundFault",
1169 &format!("Parameter group {name} not found."),
1170 ));
1171 };
1172 let params: Vec<Value> = p
1173 .parameters
1174 .iter()
1175 .map(|(k, v)| {
1176 json!({
1177 "Name": k, "Value": v, "Description": "", "DataType": "string",
1178 "AllowedValues": "", "MinimumEngineVersion": "6.2",
1179 })
1180 })
1181 .collect();
1182 ok(json!({ "Parameters": params, "NextToken": Value::Null }))
1183 }
1184
1185 fn create_subnet_group(&self, req: &AwsRequest) -> Result<AwsResponse, AwsServiceError> {
1186 let b = parse(req)?;
1187 let name = req_str(&b, "SubnetGroupName")?.to_string();
1188 let subnet_ids = str_list(&b, "SubnetIds");
1189 let mut accounts = self.state.write();
1190 let st = accounts.get_or_create(&req.account_id);
1191 if st.subnet_groups.contains_key(&name) {
1192 return Err(fault(
1193 "SubnetGroupAlreadyExistsFault",
1194 &format!("Subnet group {name} already exists."),
1195 ));
1196 }
1197 let sg_arn = arn("subnetgroup", &req.region, &req.account_id, &name);
1198 let sg = SubnetGroup {
1199 name: name.clone(),
1200 description: opt_str(&b, "Description").unwrap_or_default(),
1201 vpc_id: "vpc-00000000".to_string(),
1202 subnet_ids,
1203 arn: sg_arn.clone(),
1204 supported_network_types: vec!["ipv4".to_string()],
1205 };
1206 let tags = parse_tags(&b);
1207 if !tags.is_empty() {
1208 st.tags.insert(sg_arn, tags);
1209 }
1210 st.subnet_groups.insert(name, sg.clone());
1211 ok(json!({ "SubnetGroup": sg_json(&sg) }))
1212 }
1213
1214 fn describe_subnet_groups(&self, req: &AwsRequest) -> Result<AwsResponse, AwsServiceError> {
1215 let b = parse(req)?;
1216 let accounts = self.state.read();
1217 let st = accounts.get(&req.account_id);
1218 let Some(st) = st else {
1219 return ok(json!({ "SubnetGroups": [] }));
1220 };
1221 if let Some(name) = opt_str(&b, "SubnetGroupName") {
1222 let Some(g) = st.subnet_groups.get(&name) else {
1223 return Err(fault(
1224 "SubnetGroupNotFoundFault",
1225 &format!("Subnet group {name} not found."),
1226 ));
1227 };
1228 return ok(json!({ "SubnetGroups": [sg_json(g)] }));
1229 }
1230 let items: Vec<Value> = st.subnet_groups.values().map(sg_json).collect();
1231 let (page, next) = paginate(items, &b, 100);
1232 ok(json!({ "SubnetGroups": page, "NextToken": next }))
1233 }
1234
1235 fn update_subnet_group(&self, req: &AwsRequest) -> Result<AwsResponse, AwsServiceError> {
1236 let b = parse(req)?;
1237 let name = req_str(&b, "SubnetGroupName")?.to_string();
1238 let mut accounts = self.state.write();
1239 let st = accounts.get_or_create(&req.account_id);
1240 let Some(g) = st.subnet_groups.get_mut(&name) else {
1241 return Err(fault(
1242 "SubnetGroupNotFoundFault",
1243 &format!("Subnet group {name} not found."),
1244 ));
1245 };
1246 if let Some(d) = opt_str(&b, "Description") {
1247 g.description = d;
1248 }
1249 let ids = str_list(&b, "SubnetIds");
1250 if !ids.is_empty() {
1251 g.subnet_ids = ids;
1252 }
1253 let out = sg_json(g);
1254 ok(json!({ "SubnetGroup": out }))
1255 }
1256
1257 fn delete_subnet_group(&self, req: &AwsRequest) -> Result<AwsResponse, AwsServiceError> {
1258 let b = parse(req)?;
1259 let name = req_str(&b, "SubnetGroupName")?.to_string();
1260 let mut accounts = self.state.write();
1261 let st = accounts.get_or_create(&req.account_id);
1262 let Some(g) = st.subnet_groups.remove(&name) else {
1263 return Err(fault(
1264 "SubnetGroupNotFoundFault",
1265 &format!("Subnet group {name} not found."),
1266 ));
1267 };
1268 st.tags.remove(&g.arn);
1269 ok(json!({ "SubnetGroup": sg_json(&g) }))
1270 }
1271
1272 fn create_snapshot(&self, req: &AwsRequest) -> Result<AwsResponse, AwsServiceError> {
1273 let b = parse(req)?;
1274 let cluster_name = req_str(&b, "ClusterName")?.to_string();
1275 let snap_name = req_str(&b, "SnapshotName")?.to_string();
1276 let mut accounts = self.state.write();
1277 let st = accounts.get_or_create(&req.account_id);
1278 if st.snapshots.contains_key(&snap_name) {
1279 return Err(fault(
1280 "SnapshotAlreadyExistsFault",
1281 &format!("Snapshot {snap_name} already exists."),
1282 ));
1283 }
1284 let Some(c) = st.clusters.get(&cluster_name) else {
1285 return Err(fault(
1286 "ClusterNotFoundFault",
1287 &format!("Cluster {cluster_name} not found."),
1288 ));
1289 };
1290 let cfg = json!({
1291 "Name": c.name, "Description": c.description, "NodeType": c.node_type,
1292 "Engine": c.engine, "EngineVersion": c.engine_version, "MaintenanceWindow": c.maintenance_window,
1293 "TopicArn": c.sns_topic_arn, "Port": c.port, "ParameterGroupName": c.parameter_group_name,
1294 "SubnetGroupName": c.subnet_group_name, "VpcId": "vpc-00000000",
1295 "SnapshotRetentionLimit": c.snapshot_retention_limit, "SnapshotWindow": c.snapshot_window,
1296 "NumShards": c.number_of_shards,
1297 });
1298 let snap_arn = arn("snapshot", &req.region, &req.account_id, &snap_name);
1299 let snap = Snapshot {
1300 name: snap_name.clone(),
1301 status: "available".to_string(),
1302 source: "manual".to_string(),
1303 kms_key_id: opt_str(&b, "KmsKeyId"),
1304 arn: snap_arn.clone(),
1305 data_tiering: c.data_tiering.clone(),
1306 cluster_configuration: cfg,
1307 };
1308 let tags = parse_tags(&b);
1309 if !tags.is_empty() {
1310 st.tags.insert(snap_arn, tags);
1311 }
1312 st.snapshots.insert(snap_name, snap.clone());
1313 ok(json!({ "Snapshot": snapshot_json(&snap) }))
1314 }
1315
1316 fn copy_snapshot(&self, req: &AwsRequest) -> Result<AwsResponse, AwsServiceError> {
1317 let b = parse(req)?;
1318 let source = req_str(&b, "SourceSnapshotName")?.to_string();
1319 let target = req_str(&b, "TargetSnapshotName")?.to_string();
1320 let mut accounts = self.state.write();
1321 let st = accounts.get_or_create(&req.account_id);
1322 let Some(src) = st.snapshots.get(&source).cloned() else {
1323 return Err(fault(
1324 "SnapshotNotFoundFault",
1325 &format!("Snapshot {source} not found."),
1326 ));
1327 };
1328 if st.snapshots.contains_key(&target) {
1329 return Err(fault(
1330 "SnapshotAlreadyExistsFault",
1331 &format!("Snapshot {target} already exists."),
1332 ));
1333 }
1334 let snap_arn = arn("snapshot", &req.region, &req.account_id, &target);
1335 let snap = Snapshot {
1336 name: target.clone(),
1337 status: "available".to_string(),
1338 source: "manual".to_string(),
1339 kms_key_id: opt_str(&b, "KmsKeyId").or(src.kms_key_id),
1340 arn: snap_arn,
1341 data_tiering: src.data_tiering,
1342 cluster_configuration: src.cluster_configuration,
1343 };
1344 st.snapshots.insert(target, snap.clone());
1345 ok(json!({ "Snapshot": snapshot_json(&snap) }))
1346 }
1347
1348 fn describe_snapshots(&self, req: &AwsRequest) -> Result<AwsResponse, AwsServiceError> {
1349 let b = parse(req)?;
1350 let accounts = self.state.read();
1351 let st = accounts.get(&req.account_id);
1352 let Some(st) = st else {
1353 return ok(json!({ "Snapshots": [] }));
1354 };
1355 if let Some(name) = opt_str(&b, "SnapshotName") {
1356 let Some(s) = st.snapshots.get(&name) else {
1357 return Err(fault(
1358 "SnapshotNotFoundFault",
1359 &format!("Snapshot {name} not found."),
1360 ));
1361 };
1362 return ok(json!({ "Snapshots": [snapshot_json(s)] }));
1363 }
1364 let cluster_filter = opt_str(&b, "ClusterName");
1365 let items: Vec<Value> = st
1366 .snapshots
1367 .values()
1368 .filter(|s| {
1369 cluster_filter.as_ref().is_none_or(|cn| {
1370 s.cluster_configuration.get("Name").and_then(Value::as_str) == Some(cn)
1371 })
1372 })
1373 .map(snapshot_json)
1374 .collect();
1375 let (page, next) = paginate(items, &b, 50);
1376 ok(json!({ "Snapshots": page, "NextToken": next }))
1377 }
1378
1379 fn delete_snapshot(&self, req: &AwsRequest) -> Result<AwsResponse, AwsServiceError> {
1380 let b = parse(req)?;
1381 let name = req_str(&b, "SnapshotName")?.to_string();
1382 let mut accounts = self.state.write();
1383 let st = accounts.get_or_create(&req.account_id);
1384 let Some(mut s) = st.snapshots.remove(&name) else {
1385 return Err(fault(
1386 "SnapshotNotFoundFault",
1387 &format!("Snapshot {name} not found."),
1388 ));
1389 };
1390 s.status = "deleting".to_string();
1391 st.tags.remove(&s.arn);
1392 ok(json!({ "Snapshot": snapshot_json(&s) }))
1393 }
1394
1395 fn create_multi_region_cluster(
1396 &self,
1397 req: &AwsRequest,
1398 ) -> Result<AwsResponse, AwsServiceError> {
1399 let b = parse(req)?;
1400 let suffix = req_str(&b, "MultiRegionClusterNameSuffix")?.to_string();
1401 let node_type = req_str(&b, "NodeType")?.to_string();
1402 let name = format!("virxk-{suffix}");
1403 let mut accounts = self.state.write();
1404 let st = accounts.get_or_create(&req.account_id);
1405 if st.multi_region_clusters.contains_key(&name) {
1406 return Err(fault(
1407 "MultiRegionClusterAlreadyExistsFault",
1408 &format!("Multi-region cluster {name} already exists."),
1409 ));
1410 }
1411 let mrc_arn = arn("multiregioncluster", &req.region, &req.account_id, &name);
1412 let mrc = MultiRegionCluster {
1413 name: name.clone(),
1414 description: opt_str(&b, "Description"),
1415 status: "creating".to_string(),
1416 node_type,
1417 engine: opt_str(&b, "Engine").unwrap_or_else(|| DEFAULT_ENGINE.to_string()),
1418 engine_version: opt_str(&b, "EngineVersion")
1419 .unwrap_or_else(|| DEFAULT_ENGINE_VERSION.to_string()),
1420 number_of_shards: int_field(&b, "NumShards", 1, 1, 500)?,
1421 multi_region_parameter_group_name: opt_str(&b, "MultiRegionParameterGroupName"),
1422 tls_enabled: b.get("TLSEnabled").and_then(Value::as_bool).unwrap_or(true),
1423 arn: mrc_arn.clone(),
1424 clusters: json!([]),
1425 };
1426 let tags = parse_tags(&b);
1427 if !tags.is_empty() {
1428 st.tags.insert(mrc_arn, tags);
1429 }
1430 st.multi_region_clusters.insert(name, mrc.clone());
1431 ok(json!({ "MultiRegionCluster": mrc_json(&mrc) }))
1432 }
1433
1434 fn describe_multi_region_clusters(
1435 &self,
1436 req: &AwsRequest,
1437 ) -> Result<AwsResponse, AwsServiceError> {
1438 let b = parse(req)?;
1439 let mut accounts = self.state.write();
1440 let st = accounts.get_or_create(&req.account_id);
1441 for m in st.multi_region_clusters.values_mut() {
1442 if m.status == "creating" {
1443 m.status = "available".to_string();
1444 }
1445 }
1446 if let Some(name) = opt_str(&b, "MultiRegionClusterName") {
1447 let Some(m) = st.multi_region_clusters.get(&name) else {
1448 return Err(fault(
1449 "MultiRegionClusterNotFoundFault",
1450 &format!("Multi-region cluster {name} not found."),
1451 ));
1452 };
1453 return ok(json!({ "MultiRegionClusters": [mrc_json(m)] }));
1454 }
1455 let items: Vec<Value> = st.multi_region_clusters.values().map(mrc_json).collect();
1456 let (page, next) = paginate(items, &b, 100);
1457 ok(json!({ "MultiRegionClusters": page, "NextToken": next }))
1458 }
1459
1460 fn update_multi_region_cluster(
1461 &self,
1462 req: &AwsRequest,
1463 ) -> Result<AwsResponse, AwsServiceError> {
1464 let b = parse(req)?;
1465 let name = req_str(&b, "MultiRegionClusterName")?.to_string();
1466 let mut accounts = self.state.write();
1467 let st = accounts.get_or_create(&req.account_id);
1468 let Some(m) = st.multi_region_clusters.get_mut(&name) else {
1469 return Err(fault(
1470 "MultiRegionClusterNotFoundFault",
1471 &format!("Multi-region cluster {name} not found."),
1472 ));
1473 };
1474 if let Some(d) = opt_str(&b, "Description") {
1475 m.description = Some(d);
1476 }
1477 if let Some(v) = opt_str(&b, "EngineVersion") {
1478 m.engine_version = v;
1479 }
1480 if let Some(v) = opt_str(&b, "NodeType") {
1481 m.node_type = v;
1482 }
1483 let out = mrc_json(m);
1484 ok(json!({ "MultiRegionCluster": out }))
1485 }
1486
1487 fn delete_multi_region_cluster(
1488 &self,
1489 req: &AwsRequest,
1490 ) -> Result<AwsResponse, AwsServiceError> {
1491 let b = parse(req)?;
1492 let name = req_str(&b, "MultiRegionClusterName")?.to_string();
1493 let mut accounts = self.state.write();
1494 let st = accounts.get_or_create(&req.account_id);
1495 let Some(mut m) = st.multi_region_clusters.remove(&name) else {
1496 return Err(fault(
1497 "MultiRegionClusterNotFoundFault",
1498 &format!("Multi-region cluster {name} not found."),
1499 ));
1500 };
1501 m.status = "deleting".to_string();
1502 st.tags.remove(&m.arn);
1503 ok(json!({ "MultiRegionCluster": mrc_json(&m) }))
1504 }
1505
1506 fn list_allowed_mr_updates(&self, req: &AwsRequest) -> Result<AwsResponse, AwsServiceError> {
1507 let b = parse(req)?;
1508 let name = req_str(&b, "MultiRegionClusterName")?.to_string();
1509 let accounts = self.state.read();
1510 if accounts
1511 .get(&req.account_id)
1512 .and_then(|s| s.multi_region_clusters.get(&name))
1513 .is_none()
1514 {
1515 return Err(fault(
1516 "MultiRegionClusterNotFoundFault",
1517 &format!("Multi-region cluster {name} not found."),
1518 ));
1519 }
1520 ok(
1521 json!({ "ScaleUpNodeTypes": ["db.r7g.large", "db.r7g.xlarge"], "ScaleDownNodeTypes": [] }),
1522 )
1523 }
1524
1525 fn describe_mr_parameter_groups(
1526 &self,
1527 req: &AwsRequest,
1528 ) -> Result<AwsResponse, AwsServiceError> {
1529 let _ = parse(req)?;
1530 ok(json!({ "MultiRegionParameterGroups": [], "NextToken": Value::Null }))
1531 }
1532
1533 fn describe_mr_parameters(&self, req: &AwsRequest) -> Result<AwsResponse, AwsServiceError> {
1534 let b = parse(req)?;
1535 req_str(&b, "MultiRegionParameterGroupName")?;
1536 ok(json!({ "MultiRegionParameters": [], "NextToken": Value::Null }))
1537 }
1538
1539 fn tag_resource(&self, req: &AwsRequest) -> Result<AwsResponse, AwsServiceError> {
1540 let b = parse(req)?;
1541 let resource_arn = req_str(&b, "ResourceArn")?.to_string();
1542 let mut accounts = self.state.write();
1543 let st = accounts.get_or_create(&req.account_id);
1544 if !arn_exists(st, &resource_arn) {
1545 return Err(fault(
1546 "InvalidARNFault",
1547 &format!("{resource_arn} does not refer to a known resource."),
1548 ));
1549 }
1550 let entry = st.tags.entry(resource_arn).or_default();
1551 for (k, v) in parse_tags(&b) {
1552 entry.insert(k, v);
1553 }
1554 let all = entry.clone();
1555 ok(json!({ "TagList": tags_json(&all) }))
1556 }
1557
1558 fn untag_resource(&self, req: &AwsRequest) -> Result<AwsResponse, AwsServiceError> {
1559 let b = parse(req)?;
1560 let resource_arn = req_str(&b, "ResourceArn")?.to_string();
1561 let mut accounts = self.state.write();
1562 let st = accounts.get_or_create(&req.account_id);
1563 if !arn_exists(st, &resource_arn) {
1564 return Err(fault(
1565 "InvalidARNFault",
1566 &format!("{resource_arn} does not refer to a known resource."),
1567 ));
1568 }
1569 let keys = str_list(&b, "TagKeys");
1570 let entry = st.tags.entry(resource_arn).or_default();
1571 for k in &keys {
1572 entry.remove(k);
1573 }
1574 let all = entry.clone();
1575 ok(json!({ "TagList": tags_json(&all) }))
1576 }
1577
1578 fn list_tags(&self, req: &AwsRequest) -> Result<AwsResponse, AwsServiceError> {
1579 let b = parse(req)?;
1580 let resource_arn = req_str(&b, "ResourceArn")?.to_string();
1581 let accounts = self.state.read();
1582 let st = accounts.get(&req.account_id);
1583 let Some(st) = st else {
1584 return ok(json!({ "TagList": [] }));
1585 };
1586 if !arn_exists(st, &resource_arn) {
1587 return Err(fault(
1588 "InvalidARNFault",
1589 &format!("{resource_arn} does not refer to a known resource."),
1590 ));
1591 }
1592 let tags = st.tags.get(&resource_arn).cloned().unwrap_or_default();
1593 ok(json!({ "TagList": tags_json(&tags) }))
1594 }
1595
1596 fn describe_engine_versions(&self, req: &AwsRequest) -> Result<AwsResponse, AwsServiceError> {
1597 let _ = parse(req)?;
1598 let versions = json!([
1599 { "Engine": "redis", "EngineVersion": "7.1", "EnginePatchVersion": "7.1.1", "ParameterGroupFamily": "memorydb_redis7" },
1600 { "Engine": "redis", "EngineVersion": "6.2", "EnginePatchVersion": "6.2.6", "ParameterGroupFamily": "memorydb_redis6" },
1601 { "Engine": "valkey", "EngineVersion": "7.2", "EnginePatchVersion": "7.2.0", "ParameterGroupFamily": "memorydb_valkey7" },
1602 ]);
1603 ok(json!({ "EngineVersions": versions, "NextToken": Value::Null }))
1604 }
1605
1606 fn describe_events(&self, req: &AwsRequest) -> Result<AwsResponse, AwsServiceError> {
1607 let b = parse(req)?;
1608 if let Some(st) = opt_str(&b, "SourceType") {
1609 const VALID: &[&str] = &[
1610 "node",
1611 "parameter_group",
1612 "subnet_group",
1613 "cluster",
1614 "user",
1615 "acl",
1616 ];
1617 if !VALID.contains(&st.as_str()) {
1618 return Err(fault(
1619 "InvalidParameterValueException",
1620 &format!("Invalid SourceType: {st}"),
1621 ));
1622 }
1623 }
1624 ok(json!({ "Events": [], "NextToken": Value::Null }))
1625 }
1626
1627 fn describe_service_updates(&self, req: &AwsRequest) -> Result<AwsResponse, AwsServiceError> {
1628 let _ = parse(req)?;
1629 ok(json!({ "ServiceUpdates": [], "NextToken": Value::Null }))
1630 }
1631
1632 fn describe_reserved_nodes(&self, req: &AwsRequest) -> Result<AwsResponse, AwsServiceError> {
1633 let b = parse(req)?;
1634 let accounts = self.state.read();
1635 let st = accounts.get(&req.account_id);
1636 let Some(st) = st else {
1637 return ok(json!({ "ReservedNodes": [] }));
1638 };
1639 let items: Vec<Value> = st.reserved_nodes.values().map(reserved_node_json).collect();
1640 let (page, next) = paginate(items, &b, 100);
1641 ok(json!({ "ReservedNodes": page, "NextToken": next }))
1642 }
1643
1644 fn describe_reserved_nodes_offerings(
1645 &self,
1646 req: &AwsRequest,
1647 ) -> Result<AwsResponse, AwsServiceError> {
1648 let _ = parse(req)?;
1649 let offerings = json!([
1650 {
1651 "ReservedNodesOfferingId": "00000000-1111-2222-3333-444444444444",
1652 "NodeType": "db.r6g.large", "Duration": 31536000, "FixedPrice": 1200.0,
1653 "OfferingType": "All Upfront", "RecurringCharges": [],
1654 }
1655 ]);
1656 ok(json!({ "ReservedNodesOfferings": offerings, "NextToken": Value::Null }))
1657 }
1658
1659 fn purchase_reserved_nodes_offering(
1660 &self,
1661 req: &AwsRequest,
1662 ) -> Result<AwsResponse, AwsServiceError> {
1663 let b = parse(req)?;
1664 let offering_id = req_str(&b, "ReservedNodesOfferingId")?.to_string();
1665 let reservation_id = opt_str(&b, "ReservationId")
1666 .unwrap_or_else(|| format!("ri-{}", uuid::Uuid::new_v4().simple()));
1667 let count = int_field(&b, "NodeCount", 1, 1, i32::MAX as i64)?;
1668 let mut accounts = self.state.write();
1669 let st = accounts.get_or_create(&req.account_id);
1670 let node_arn = arn(
1671 "reservednode",
1672 &req.region,
1673 &req.account_id,
1674 &reservation_id,
1675 );
1676 let rn = ReservedNode {
1677 reservation_id: reservation_id.clone(),
1678 reserved_nodes_offering_id: offering_id,
1679 node_type: "db.r6g.large".to_string(),
1680 start_time: Utc::now(),
1681 duration: 31536000,
1682 fixed_price: 1200.0,
1683 node_count: count,
1684 offering_type: "All Upfront".to_string(),
1685 state: "active".to_string(),
1686 arn: node_arn,
1687 };
1688 st.reserved_nodes.insert(reservation_id, rn.clone());
1689 ok(json!({ "ReservedNode": reserved_node_json(&rn) }))
1690 }
1691
1692 fn list_allowed_node_type_updates(
1693 &self,
1694 req: &AwsRequest,
1695 ) -> Result<AwsResponse, AwsServiceError> {
1696 let b = parse(req)?;
1697 let name = req_str(&b, "ClusterName")?.to_string();
1698 let accounts = self.state.read();
1699 if accounts
1700 .get(&req.account_id)
1701 .and_then(|s| s.clusters.get(&name))
1702 .is_none()
1703 {
1704 return Err(fault(
1705 "ClusterNotFoundFault",
1706 &format!("Cluster {name} not found."),
1707 ));
1708 }
1709 ok(json!({
1710 "ScaleUpNodeTypes": ["db.r6g.xlarge", "db.r6g.2xlarge"],
1711 "ScaleDownNodeTypes": ["db.r6g.large"],
1712 }))
1713 }
1714}
1715
1716fn validate_user_name(name: &str) -> Result<(), AwsServiceError> {
1719 let valid = !name.is_empty()
1720 && name.starts_with(|c: char| c.is_ascii_alphabetic())
1721 && name.chars().all(|c| c.is_ascii_alphanumeric() || c == '-');
1722 if valid {
1723 Ok(())
1724 } else {
1725 Err(fault(
1726 "InvalidParameterValueException",
1727 &format!("Invalid UserName: {name}"),
1728 ))
1729 }
1730}
1731
1732fn arn_exists(st: &MemoryDbState, resource_arn: &str) -> bool {
1734 if resource_arn.is_empty() {
1737 return false;
1738 }
1739 st.clusters.values().any(|c| c.arn == resource_arn)
1740 || st.acls.values().any(|a| a.arn == resource_arn)
1741 || st.users.values().any(|u| u.arn == resource_arn)
1742 || st.parameter_groups.values().any(|p| p.arn == resource_arn)
1743 || st.subnet_groups.values().any(|g| g.arn == resource_arn)
1744 || st.snapshots.values().any(|s| s.arn == resource_arn)
1745 || st
1746 .multi_region_clusters
1747 .values()
1748 .any(|m| m.arn == resource_arn)
1749 || st.reserved_nodes.values().any(|r| r.arn == resource_arn)
1750}
1751
1752#[cfg(test)]
1753mod tests {
1754 use super::*;
1755 use fakecloud_core::multi_account::MultiAccountState;
1756
1757 fn service() -> MemoryDbService {
1758 let state = Arc::new(parking_lot::RwLock::new(MultiAccountState::new(
1759 "123456789012",
1760 "us-east-1",
1761 "http://localhost:4566",
1762 )));
1763 MemoryDbService::new(state)
1764 }
1765
1766 fn req(action: &str, body: Value) -> AwsRequest {
1767 AwsRequest {
1768 service: "memorydb".to_string(),
1769 action: action.to_string(),
1770 region: "us-east-1".to_string(),
1771 account_id: "123456789012".to_string(),
1772 request_id: "rid".to_string(),
1773 headers: http::HeaderMap::new(),
1774 query_params: std::collections::HashMap::new(),
1775 body: bytes::Bytes::from(serde_json::to_vec(&body).unwrap()),
1776 body_stream: parking_lot::Mutex::new(None),
1777 path_segments: vec![],
1778 raw_path: "/".to_string(),
1779 raw_query: String::new(),
1780 method: http::Method::POST,
1781 is_query_protocol: false,
1782 access_key_id: None,
1783 principal: None,
1784 }
1785 }
1786
1787 fn call(s: &MemoryDbService, action: &str, body: Value) -> Result<Value, AwsServiceError> {
1788 let resp = dispatch(s, &req(action, body))?;
1789 Ok(serde_json::from_slice(resp.body.expect_bytes()).unwrap())
1790 }
1791
1792 #[test]
1793 fn create_cluster_round_trips_and_describes() {
1794 let s = service();
1795 let out = call(
1796 &s,
1797 "CreateCluster",
1798 json!({"ClusterName": "app", "NodeType": "db.r6g.large", "ACLName": "open-access"}),
1799 )
1800 .unwrap();
1801 assert_eq!(out["Cluster"]["Name"], "app");
1802
1803 let desc = call(&s, "DescribeClusters", json!({"ClusterName": "app"})).unwrap();
1804 let clusters = desc["Clusters"].as_array().unwrap();
1805 assert_eq!(clusters.len(), 1);
1806 assert_eq!(clusters[0]["Status"], "available");
1808 }
1809
1810 #[test]
1811 fn create_cluster_rejects_out_of_range_shard_count() {
1812 let s = service();
1813 let err = call(
1815 &s,
1816 "CreateCluster",
1817 json!({"ClusterName": "big", "NodeType": "db.r6g.large", "ACLName": "open-access", "NumShards": 2_000_000_000i64}),
1818 )
1819 .unwrap_err();
1820 assert!(
1821 err.message().contains("NumShards"),
1822 "got: {}",
1823 err.message()
1824 );
1825 }
1826
1827 #[test]
1828 fn create_cluster_rejects_out_of_range_replica_count() {
1829 let s = service();
1830 let err = call(
1831 &s,
1832 "CreateCluster",
1833 json!({"ClusterName": "r", "NodeType": "db.r6g.large", "ACLName": "open-access", "NumReplicasPerShard": 99}),
1834 )
1835 .unwrap_err();
1836 assert!(
1837 err.message().contains("NumReplicasPerShard"),
1838 "got: {}",
1839 err.message()
1840 );
1841 }
1842
1843 #[test]
1844 fn create_user_rejects_invalid_name() {
1845 let s = service();
1846 let err = call(
1848 &s,
1849 "CreateUser",
1850 json!({"UserName": "1bad", "AccessString": "on ~* &* +@all", "AuthenticationMode": {"Type": "no-password-required"}}),
1851 )
1852 .unwrap_err();
1853 assert!(err.message().contains("UserName"), "got: {}", err.message());
1854 }
1855
1856 #[test]
1857 fn default_acl_and_user_are_seeded() {
1858 let s = service();
1859 let acls = call(&s, "DescribeACLs", json!({})).unwrap();
1860 assert!(acls["ACLs"]
1861 .as_array()
1862 .unwrap()
1863 .iter()
1864 .any(|a| a["Name"] == "open-access"));
1865 let users = call(&s, "DescribeUsers", json!({})).unwrap();
1866 assert!(users["Users"]
1867 .as_array()
1868 .unwrap()
1869 .iter()
1870 .any(|u| u["Name"] == "default"));
1871 }
1872
1873 #[test]
1874 fn seeded_default_acl_and_user_have_populated_arns() {
1875 let s = service();
1876 let acls = call(&s, "DescribeACLs", json!({"ACLName": "open-access"})).unwrap();
1877 assert_eq!(
1878 acls["ACLs"][0]["ARN"],
1879 "arn:aws:memorydb:us-east-1:123456789012:acl/open-access"
1880 );
1881 let users = call(&s, "DescribeUsers", json!({"UserName": "default"})).unwrap();
1882 assert_eq!(
1883 users["Users"][0]["ARN"],
1884 "arn:aws:memorydb:us-east-1:123456789012:user/default"
1885 );
1886 }
1887
1888 #[test]
1889 fn not_found_faults_return_http_404() {
1890 let s = service();
1891 let err = call(&s, "DescribeClusters", json!({"ClusterName": "nope"})).unwrap_err();
1892 assert_eq!(err.status(), http::StatusCode::NOT_FOUND);
1893 let bad = call(
1895 &s,
1896 "CreateCluster",
1897 json!({"ClusterName": "c", "NodeType": "db.r6g.large", "ACLName": "open-access", "NumShards": 9999}),
1898 )
1899 .unwrap_err();
1900 assert_eq!(bad.status(), http::StatusCode::BAD_REQUEST);
1901 }
1902
1903 #[test]
1904 fn create_cluster_populates_acl_clusters_backref() {
1905 let s = service();
1906 call(
1907 &s,
1908 "CreateCluster",
1909 json!({"ClusterName": "c1", "NodeType": "db.r6g.large", "ACLName": "open-access"}),
1910 )
1911 .unwrap();
1912 let acls = call(&s, "DescribeACLs", json!({"ACLName": "open-access"})).unwrap();
1913 let clusters = acls["ACLs"][0]["Clusters"].as_array().unwrap();
1914 assert!(clusters.iter().any(|c| c == "c1"), "got: {clusters:?}");
1915
1916 call(&s, "DeleteCluster", json!({"ClusterName": "c1"})).unwrap();
1918 let acls = call(&s, "DescribeACLs", json!({"ACLName": "open-access"})).unwrap();
1919 assert!(acls["ACLs"][0]["Clusters"].as_array().unwrap().is_empty());
1920 }
1921
1922 #[test]
1923 fn acl_membership_syncs_user_acl_names_backref() {
1924 let s = service();
1925 call(
1926 &s,
1927 "CreateUser",
1928 json!({"UserName": "bob", "AccessString": "on ~* &* +@all", "AuthenticationMode": {"Type": "iam"}}),
1929 )
1930 .unwrap();
1931 call(
1932 &s,
1933 "CreateACL",
1934 json!({"ACLName": "team", "UserNames": ["bob"]}),
1935 )
1936 .unwrap();
1937 let users = call(&s, "DescribeUsers", json!({"UserName": "bob"})).unwrap();
1938 let acl_names = users["Users"][0]["ACLNames"].as_array().unwrap();
1939 assert!(acl_names.iter().any(|a| a == "team"), "got: {acl_names:?}");
1940
1941 call(
1943 &s,
1944 "UpdateACL",
1945 json!({"ACLName": "team", "UserNamesToRemove": ["bob"]}),
1946 )
1947 .unwrap();
1948 let users = call(&s, "DescribeUsers", json!({"UserName": "bob"})).unwrap();
1949 assert!(users["Users"][0]["ACLNames"]
1950 .as_array()
1951 .unwrap()
1952 .iter()
1953 .all(|a| a != "team"));
1954 }
1955
1956 #[test]
1957 fn tag_resource_rejects_empty_arn() {
1958 let s = service();
1959 let err = call(
1960 &s,
1961 "TagResource",
1962 json!({"ResourceArn": "", "Tags": [{"Key": "k", "Value": "v"}]}),
1963 )
1964 .unwrap_err();
1965 assert_eq!(err.status(), http::StatusCode::BAD_REQUEST);
1966 assert!(
1967 err.message().contains("known resource") || err.message().contains("ARN"),
1968 "got: {}",
1969 err.message()
1970 );
1971 }
1972
1973 #[test]
1974 fn create_user_requires_valid_authentication_mode() {
1975 let s = service();
1976 let e1 = call(
1978 &s,
1979 "CreateUser",
1980 json!({"UserName": "u1", "AccessString": "on ~* &* +@all"}),
1981 )
1982 .unwrap_err();
1983 assert!(
1984 e1.message().contains("AuthenticationMode"),
1985 "got: {}",
1986 e1.message()
1987 );
1988 let e2 = call(
1990 &s,
1991 "CreateUser",
1992 json!({"UserName": "u2", "AccessString": "on ~* &* +@all", "AuthenticationMode": {"Type": "password"}}),
1993 )
1994 .unwrap_err();
1995 assert!(e2.message().contains("password"), "got: {}", e2.message());
1996 let e3 = call(
1998 &s,
1999 "CreateUser",
2000 json!({"UserName": "u3", "AccessString": "on ~* &* +@all", "AuthenticationMode": {"Type": "kerberos"}}),
2001 )
2002 .unwrap_err();
2003 assert!(
2004 e3.message().contains("kerberos") || e3.message().contains("Type"),
2005 "got: {}",
2006 e3.message()
2007 );
2008 call(
2010 &s,
2011 "CreateUser",
2012 json!({"UserName": "u4", "AccessString": "on ~* &* +@all", "AuthenticationMode": {"Type": "iam"}}),
2013 )
2014 .unwrap();
2015 }
2016
2017 #[test]
2018 fn failover_shard_validates_shard_name() {
2019 let s = service();
2020 call(
2021 &s,
2022 "CreateCluster",
2023 json!({"ClusterName": "fc", "NodeType": "db.r6g.large", "ACLName": "open-access", "NumShards": 1}),
2024 )
2025 .unwrap();
2026 let err = call(
2028 &s,
2029 "FailoverShard",
2030 json!({"ClusterName": "fc", "ShardName": "9999"}),
2031 )
2032 .unwrap_err();
2033 assert_eq!(err.status(), http::StatusCode::NOT_FOUND);
2034 call(
2036 &s,
2037 "FailoverShard",
2038 json!({"ClusterName": "fc", "ShardName": "0001"}),
2039 )
2040 .unwrap();
2041 }
2042
2043 #[test]
2044 fn batch_update_reports_unknown_clusters_as_unprocessed() {
2045 let s = service();
2046 call(
2047 &s,
2048 "CreateCluster",
2049 json!({"ClusterName": "known", "NodeType": "db.r6g.large", "ACLName": "open-access"}),
2050 )
2051 .unwrap();
2052 let out = call(
2053 &s,
2054 "BatchUpdateCluster",
2055 json!({"ClusterNames": ["known", "ghost"]}),
2056 )
2057 .unwrap();
2058 assert_eq!(out["ProcessedClusters"].as_array().unwrap().len(), 1);
2059 let un = out["UnprocessedClusters"].as_array().unwrap();
2060 assert_eq!(un.len(), 1);
2061 assert_eq!(un[0]["ClusterName"], "ghost");
2062 }
2063
2064 #[test]
2065 fn snapshot_retention_limit_out_of_range_rejected() {
2066 let s = service();
2067 let err = call(
2068 &s,
2069 "CreateCluster",
2070 json!({"ClusterName": "srl", "NodeType": "db.r6g.large", "ACLName": "open-access", "SnapshotRetentionLimit": 4294967297i64}),
2071 )
2072 .unwrap_err();
2073 assert_eq!(err.status(), http::StatusCode::BAD_REQUEST);
2074 assert!(
2075 err.message().contains("SnapshotRetentionLimit"),
2076 "got: {}",
2077 err.message()
2078 );
2079 }
2080}