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_shards(c: &Cluster, show_shards: bool) -> Value {
358 let mut v = cluster_json(c);
359 if !show_shards {
360 if let Some(obj) = v.as_object_mut() {
361 obj.remove("Shards");
362 }
363 }
364 v
365}
366
367fn cluster_json(c: &Cluster) -> Value {
368 json!({
369 "Name": c.name,
370 "Description": c.description,
371 "Status": c.status,
372 "NumberOfShards": c.number_of_shards,
373 "Shards": c.shards.iter().map(shard_json).collect::<Vec<_>>(),
374 "AvailabilityMode": c.availability_mode,
375 "ClusterEndpoint": endpoint_json(&c.cluster_endpoint),
376 "NodeType": c.node_type,
377 "Engine": c.engine,
378 "EngineVersion": c.engine_version,
379 "EnginePatchVersion": c.engine_patch_version,
380 "ParameterGroupName": c.parameter_group_name,
381 "ParameterGroupStatus": c.parameter_group_status,
382 "SecurityGroups": c.security_group_ids.iter().map(|id| json!({ "SecurityGroupId": id, "Status": "active" })).collect::<Vec<_>>(),
383 "SubnetGroupName": c.subnet_group_name,
384 "TLSEnabled": c.tls_enabled,
385 "KmsKeyId": c.kms_key_id,
386 "ARN": c.arn,
387 "SnsTopicArn": c.sns_topic_arn,
388 "SnsTopicStatus": c.sns_topic_arn.as_ref().map(|_| "active"),
389 "SnapshotRetentionLimit": c.snapshot_retention_limit,
390 "MaintenanceWindow": c.maintenance_window,
391 "SnapshotWindow": c.snapshot_window,
392 "ACLName": c.acl_name,
393 "AutoMinorVersionUpgrade": c.auto_minor_version_upgrade,
394 "DataTiering": c.data_tiering,
395 "NetworkType": c.network_type,
396 "IpDiscovery": c.ip_discovery,
397 })
398}
399
400fn acl_json(a: &Acl) -> Value {
401 json!({
402 "Name": a.name,
403 "Status": a.status,
404 "UserNames": a.user_names,
405 "MinimumEngineVersion": a.minimum_engine_version,
406 "Clusters": a.clusters,
407 "ARN": a.arn,
408 })
409}
410
411fn user_json(u: &User) -> Value {
412 json!({
413 "Name": u.name,
414 "Status": u.status,
415 "AccessString": u.access_string,
416 "ACLNames": u.acl_names,
417 "MinimumEngineVersion": u.minimum_engine_version,
418 "Authentication": {
419 "Type": u.authentication.type_,
420 "PasswordCount": u.authentication.password_count,
421 },
422 "ARN": u.arn,
423 })
424}
425
426fn pg_json(p: &ParameterGroup) -> Value {
427 json!({
428 "Name": p.name,
429 "Family": p.family,
430 "Description": p.description,
431 "ARN": p.arn,
432 })
433}
434
435fn sg_json(g: &SubnetGroup) -> Value {
436 json!({
437 "Name": g.name,
438 "Description": g.description,
439 "VpcId": g.vpc_id,
440 "Subnets": g.subnet_ids.iter().map(|id| json!({
441 "Identifier": id,
442 "AvailabilityZone": { "Name": "us-east-1a" },
443 "SupportedNetworkTypes": ["ipv4"],
444 })).collect::<Vec<_>>(),
445 "ARN": g.arn,
446 "SupportedNetworkTypes": g.supported_network_types,
447 })
448}
449
450fn snapshot_json(s: &Snapshot) -> Value {
451 snapshot_json_detail(s, true)
452}
453
454fn snapshot_json_detail(s: &Snapshot, show_detail: bool) -> Value {
457 let mut v = json!({
458 "Name": s.name,
459 "Status": s.status,
460 "Source": s.source,
461 "KmsKeyId": s.kms_key_id,
462 "ARN": s.arn,
463 "ClusterConfiguration": s.cluster_configuration,
464 "DataTiering": s.data_tiering,
465 });
466 if !show_detail {
467 if let Some(obj) = v.as_object_mut() {
468 obj.remove("ClusterConfiguration");
469 }
470 }
471 v
472}
473
474fn mrc_json(m: &MultiRegionCluster) -> Value {
475 json!({
476 "MultiRegionClusterName": m.name,
477 "Description": m.description,
478 "Status": m.status,
479 "NodeType": m.node_type,
480 "Engine": m.engine,
481 "EngineVersion": m.engine_version,
482 "NumberOfShards": m.number_of_shards,
483 "Clusters": m.clusters,
484 "MultiRegionParameterGroupName": m.multi_region_parameter_group_name,
485 "TLSEnabled": m.tls_enabled,
486 "ARN": m.arn,
487 })
488}
489
490fn reserved_node_json(r: &ReservedNode) -> Value {
491 json!({
492 "ReservationId": r.reservation_id,
493 "ReservedNodesOfferingId": r.reserved_nodes_offering_id,
494 "NodeType": r.node_type,
495 "StartTime": r.start_time.timestamp(),
496 "Duration": r.duration,
497 "FixedPrice": r.fixed_price,
498 "NodeCount": r.node_count,
499 "OfferingType": r.offering_type,
500 "State": r.state,
501 "ARN": r.arn,
502 })
503}
504
505impl MemoryDbService {
508 fn create_cluster(&self, req: &AwsRequest) -> Result<AwsResponse, AwsServiceError> {
509 let b = parse(req)?;
510 let name = req_str(&b, "ClusterName")?.to_string();
511 let node_type = req_str(&b, "NodeType")?.to_string();
512 let acl_name = req_str(&b, "ACLName")?.to_string();
513 let mut accounts = self.state.write();
514 let st = accounts.get_or_create(&req.account_id);
515 if st.clusters.contains_key(&name) {
516 return Err(fault(
517 "ClusterAlreadyExistsFault",
518 &format!("Cluster {name} already exists."),
519 ));
520 }
521 let num_shards = b.get("NumShards").and_then(Value::as_i64).unwrap_or(1);
525 if !(1..=500).contains(&num_shards) {
526 return Err(fault(
527 "InvalidParameterValueException",
528 "NumShards must be between 1 and 500.",
529 ));
530 }
531 let num_shards = num_shards as i32;
532 let replicas = b
533 .get("NumReplicasPerShard")
534 .and_then(Value::as_i64)
535 .unwrap_or(1);
536 if !(0..=5).contains(&replicas) {
537 return Err(fault(
538 "InvalidParameterValueException",
539 "NumReplicasPerShard must be between 0 and 5.",
540 ));
541 }
542 let replicas = replicas as i32;
543 let snapshot_retention_limit = int_field(&b, "SnapshotRetentionLimit", 0, 0, 35)?;
545 let port = b.get("Port").and_then(Value::as_i64).unwrap_or(6379) as i32;
546 let engine = opt_str(&b, "Engine").unwrap_or_else(|| DEFAULT_ENGINE.to_string());
547 let engine_version =
548 opt_str(&b, "EngineVersion").unwrap_or_else(|| DEFAULT_ENGINE_VERSION.to_string());
549 let cluster_arn = arn("cluster", &req.region, &req.account_id, &name);
550 let endpoint = Endpoint {
551 address: format!(
552 "clustercfg.{name}.abc123.memorydb.{}.amazonaws.com",
553 req.region
554 ),
555 port,
556 };
557 let shards = (0..num_shards)
558 .map(|i| {
559 let sname = format!("{:04}", i + 1);
560 let nodes = (0..=replicas)
561 .map(|j| Node {
562 name: format!("{name}-{sname}-{:03}", j + 1),
563 status: "creating".to_string(),
564 availability_zone: format!("{}a", req.region),
565 create_time: Utc::now(),
566 endpoint: Endpoint {
567 address: format!(
568 "{name}-{sname}-{:03}.{name}.abc123.memorydb.{}.amazonaws.com",
569 j + 1,
570 req.region
571 ),
572 port,
573 },
574 })
575 .collect::<Vec<_>>();
576 let start = (i as i64 * 16384 / num_shards as i64) as i32;
581 let end = ((i as i64 + 1) * 16384 / num_shards as i64) as i32 - 1;
582 Shard {
583 name: sname,
584 status: "creating".to_string(),
585 slots: format!("{start}-{end}"),
586 number_of_nodes: nodes.len() as i32,
587 nodes,
588 }
589 })
590 .collect();
591 let cluster = Cluster {
592 name: name.clone(),
593 description: opt_str(&b, "Description"),
594 status: "creating".to_string(),
595 number_of_shards: num_shards,
596 shards,
597 node_type,
598 engine,
599 engine_version,
600 engine_patch_version: DEFAULT_PATCH_VERSION.to_string(),
601 parameter_group_name: opt_str(&b, "ParameterGroupName")
602 .unwrap_or_else(|| "default.memorydb-redis7".to_string()),
603 parameter_group_status: "in-sync".to_string(),
604 security_group_ids: str_list(&b, "SecurityGroupIds"),
605 subnet_group_name: opt_str(&b, "SubnetGroupName")
606 .unwrap_or_else(|| "default".to_string()),
607 tls_enabled: b.get("TLSEnabled").and_then(Value::as_bool).unwrap_or(true),
608 kms_key_id: opt_str(&b, "KmsKeyId"),
609 arn: cluster_arn.clone(),
610 sns_topic_arn: opt_str(&b, "SnsTopicArn"),
611 snapshot_retention_limit,
612 maintenance_window: opt_str(&b, "MaintenanceWindow")
613 .unwrap_or_else(|| "wed:08:00-wed:09:00".to_string()),
614 snapshot_window: opt_str(&b, "SnapshotWindow")
615 .unwrap_or_else(|| "05:00-06:00".to_string()),
616 acl_name,
617 auto_minor_version_upgrade: b
618 .get("AutoMinorVersionUpgrade")
619 .and_then(Value::as_bool)
620 .unwrap_or(true),
621 data_tiering: opt_str(&b, "DataTiering")
622 .map(|d| {
623 if d == "true" {
624 "true".to_string()
625 } else {
626 "false".to_string()
627 }
628 })
629 .unwrap_or_else(|| "false".to_string()),
630 availability_mode: if replicas > 0 {
631 "MultiAZ".to_string()
632 } else {
633 "SingleAZ".to_string()
634 },
635 cluster_endpoint: endpoint,
636 network_type: opt_str(&b, "NetworkType").unwrap_or_else(|| "ipv4".to_string()),
637 ip_discovery: opt_str(&b, "IpDiscovery").unwrap_or_else(|| "ipv4".to_string()),
638 port,
639 container_id: None,
640 created_at: Utc::now(),
641 };
642 let tags = parse_tags(&b);
643 if !tags.is_empty() {
644 st.tags.insert(cluster_arn, tags);
645 }
646 if let Some(acl) = st.acls.get_mut(&cluster.acl_name) {
649 if !acl.clusters.contains(&name) {
650 acl.clusters.push(name.clone());
651 }
652 }
653 st.clusters.insert(name, cluster.clone());
654 ok(json!({ "Cluster": cluster_json(&cluster) }))
655 }
656
657 fn describe_clusters(&self, req: &AwsRequest) -> Result<AwsResponse, AwsServiceError> {
658 let b = parse(req)?;
659 let show_shards = b
662 .get("ShowShardDetails")
663 .and_then(Value::as_bool)
664 .unwrap_or(true);
665 let mut accounts = self.state.write();
666 let st = accounts.get_or_create(&req.account_id);
667 for c in st.clusters.values_mut() {
672 if c.status == "creating" {
673 c.status = "available".to_string();
674 for sh in &mut c.shards {
675 sh.status = "available".to_string();
676 for n in &mut sh.nodes {
677 n.status = "available".to_string();
678 }
679 }
680 }
681 }
682 if let Some(name) = opt_str(&b, "ClusterName") {
683 let Some(c) = st.clusters.get(&name) else {
684 return Err(fault(
685 "ClusterNotFoundFault",
686 &format!("Cluster {name} not found."),
687 ));
688 };
689 return ok(json!({ "Clusters": [cluster_json_shards(c, show_shards)] }));
690 }
691 let items: Vec<Value> = st
692 .clusters
693 .values()
694 .map(|c| cluster_json_shards(c, show_shards))
695 .collect();
696 let (page, next) = paginate(items, &b, 100);
697 ok(json!({ "Clusters": page, "NextToken": next }))
698 }
699
700 fn update_cluster(&self, req: &AwsRequest) -> Result<AwsResponse, AwsServiceError> {
701 let b = parse(req)?;
702 let name = req_str(&b, "ClusterName")?.to_string();
703 let mut accounts = self.state.write();
704 let st = accounts.get_or_create(&req.account_id);
705 let Some(c) = st.clusters.get_mut(&name) else {
706 return Err(fault(
707 "ClusterNotFoundFault",
708 &format!("Cluster {name} not found."),
709 ));
710 };
711 if let Some(d) = opt_str(&b, "Description") {
712 c.description = Some(d);
713 }
714 if let Some(v) = opt_str(&b, "EngineVersion") {
715 c.engine_version = v;
716 }
717 if let Some(v) = opt_str(&b, "Engine") {
718 c.engine = v;
719 }
720 if let Some(v) = opt_str(&b, "NodeType") {
721 c.node_type = v;
722 }
723 if let Some(v) = opt_str(&b, "ParameterGroupName") {
724 c.parameter_group_name = v;
725 }
726 let acl_change = opt_str(&b, "ACLName").map(|new_acl| {
729 let old_acl = c.acl_name.clone();
730 c.acl_name = new_acl.clone();
731 (old_acl, new_acl)
732 });
733 if let Some(v) = opt_str(&b, "MaintenanceWindow") {
734 c.maintenance_window = v;
735 }
736 if let Some(v) = opt_str(&b, "SnapshotWindow") {
737 c.snapshot_window = v;
738 }
739 if b.get("SnapshotRetentionLimit").is_some() {
740 c.snapshot_retention_limit = int_field(&b, "SnapshotRetentionLimit", 0, 0, 35)?;
741 }
742 if let Some(sg) = b.get("SecurityGroupIds").and_then(Value::as_array) {
743 c.security_group_ids = sg
744 .iter()
745 .filter_map(|x| x.as_str().map(str::to_string))
746 .collect();
747 }
748 let out = cluster_json(c);
749 if let Some((old_acl, new_acl)) = acl_change {
751 if old_acl != new_acl {
752 if let Some(a) = st.acls.get_mut(&old_acl) {
753 a.clusters.retain(|cn| cn != &name);
754 }
755 if let Some(a) = st.acls.get_mut(&new_acl) {
756 if !a.clusters.contains(&name) {
757 a.clusters.push(name.clone());
758 }
759 }
760 }
761 }
762 ok(json!({ "Cluster": out }))
763 }
764
765 fn delete_cluster(&self, req: &AwsRequest) -> Result<AwsResponse, AwsServiceError> {
766 let b = parse(req)?;
767 let name = req_str(&b, "ClusterName")?.to_string();
768 let mut accounts = self.state.write();
769 let st = accounts.get_or_create(&req.account_id);
770 let Some(mut c) = st.clusters.remove(&name) else {
771 return Err(fault(
772 "ClusterNotFoundFault",
773 &format!("Cluster {name} not found."),
774 ));
775 };
776 c.status = "deleting".to_string();
777 st.tags.remove(&c.arn);
778 if let Some(a) = st.acls.get_mut(&c.acl_name) {
780 a.clusters.retain(|cn| cn != &name);
781 }
782 ok(json!({ "Cluster": cluster_json(&c) }))
783 }
784
785 fn batch_update_cluster(&self, req: &AwsRequest) -> Result<AwsResponse, AwsServiceError> {
786 let b = parse(req)?;
787 let names = str_list(&b, "ClusterNames");
788 let accounts = self.state.read();
789 let st = accounts.get(&req.account_id);
790 let mut processed = Vec::new();
791 let mut unprocessed = Vec::new();
792 for n in &names {
793 match st.and_then(|s| s.clusters.get(n)) {
794 Some(c) => processed.push(cluster_json(c)),
795 None => unprocessed.push(json!({
798 "ClusterName": n,
799 "ErrorType": "ClusterNotFoundFault",
800 "ErrorMessage": format!("Cluster {n} not found."),
801 })),
802 }
803 }
804 ok(json!({ "ProcessedClusters": processed, "UnprocessedClusters": unprocessed }))
805 }
806
807 fn failover_shard(&self, req: &AwsRequest) -> Result<AwsResponse, AwsServiceError> {
808 let b = parse(req)?;
809 let name = req_str(&b, "ClusterName")?.to_string();
810 let shard_name = req_str(&b, "ShardName")?.to_string();
812 let accounts = self.state.read();
813 let st = accounts.get(&req.account_id);
814 let Some(c) = st.and_then(|s| s.clusters.get(&name)) else {
815 return Err(fault(
816 "ClusterNotFoundFault",
817 &format!("Cluster {name} not found."),
818 ));
819 };
820 if !c.shards.iter().any(|s| s.name == shard_name) {
821 return Err(fault(
822 "ShardNotFoundFault",
823 &format!("Shard {shard_name} not found in cluster {name}."),
824 ));
825 }
826 ok(json!({ "Cluster": cluster_json(c) }))
827 }
828
829 fn create_acl(&self, req: &AwsRequest) -> Result<AwsResponse, AwsServiceError> {
830 let b = parse(req)?;
831 let name = req_str(&b, "ACLName")?.to_string();
832 let mut accounts = self.state.write();
833 let st = accounts.get_or_create(&req.account_id);
834 if st.acls.contains_key(&name) {
835 return Err(fault(
836 "ACLAlreadyExistsFault",
837 &format!("ACL {name} already exists."),
838 ));
839 }
840 let acl_arn = arn("acl", &req.region, &req.account_id, &name);
841 let acl = Acl {
842 name: name.clone(),
843 status: "active".to_string(),
844 user_names: str_list(&b, "UserNames"),
845 minimum_engine_version: "6.2".to_string(),
846 clusters: vec![],
847 arn: acl_arn.clone(),
848 };
849 let tags = parse_tags(&b);
850 if !tags.is_empty() {
851 st.tags.insert(acl_arn, tags);
852 }
853 st.acls.insert(name.clone(), acl.clone());
854 for u in &acl.user_names {
856 if let Some(user) = st.users.get_mut(u) {
857 if !user.acl_names.contains(&name) {
858 user.acl_names.push(name.clone());
859 }
860 }
861 }
862 ok(json!({ "ACL": acl_json(&acl) }))
863 }
864
865 fn describe_acls(&self, req: &AwsRequest) -> Result<AwsResponse, AwsServiceError> {
866 let b = parse(req)?;
867 let accounts = self.state.read();
868 let st = accounts.get(&req.account_id);
869 let Some(st) = st else {
870 return ok(json!({ "ACLs": [] }));
871 };
872 if let Some(name) = opt_str(&b, "ACLName") {
873 let Some(a) = st.acls.get(&name) else {
874 return Err(fault("ACLNotFoundFault", &format!("ACL {name} not found.")));
875 };
876 return ok(json!({ "ACLs": [acl_json(a)] }));
877 }
878 let items: Vec<Value> = st.acls.values().map(acl_json).collect();
879 let (page, next) = paginate(items, &b, 100);
880 ok(json!({ "ACLs": page, "NextToken": next }))
881 }
882
883 fn update_acl(&self, req: &AwsRequest) -> Result<AwsResponse, AwsServiceError> {
884 let b = parse(req)?;
885 let name = req_str(&b, "ACLName")?.to_string();
886 let mut accounts = self.state.write();
887 let st = accounts.get_or_create(&req.account_id);
888 let Some(a) = st.acls.get_mut(&name) else {
889 return Err(fault("ACLNotFoundFault", &format!("ACL {name} not found.")));
890 };
891 let mut added = Vec::new();
892 for u in str_list(&b, "UserNamesToAdd") {
893 if !a.user_names.contains(&u) {
894 a.user_names.push(u.clone());
895 added.push(u);
896 }
897 }
898 let remove = str_list(&b, "UserNamesToRemove");
899 let removed: Vec<String> = a
900 .user_names
901 .iter()
902 .filter(|u| remove.contains(u))
903 .cloned()
904 .collect();
905 a.user_names.retain(|u| !remove.contains(u));
906 let out = acl_json(a);
907 for u in &added {
909 if let Some(user) = st.users.get_mut(u) {
910 if !user.acl_names.contains(&name) {
911 user.acl_names.push(name.clone());
912 }
913 }
914 }
915 for u in &removed {
916 if let Some(user) = st.users.get_mut(u) {
917 user.acl_names.retain(|an| an != &name);
918 }
919 }
920 ok(json!({ "ACL": out }))
921 }
922
923 fn delete_acl(&self, req: &AwsRequest) -> Result<AwsResponse, AwsServiceError> {
924 let b = parse(req)?;
925 let name = req_str(&b, "ACLName")?.to_string();
926 let mut accounts = self.state.write();
927 let st = accounts.get_or_create(&req.account_id);
928 let Some(mut a) = st.acls.remove(&name) else {
929 return Err(fault("ACLNotFoundFault", &format!("ACL {name} not found.")));
930 };
931 a.status = "deleting".to_string();
932 st.tags.remove(&a.arn);
933 for u in &a.user_names {
935 if let Some(user) = st.users.get_mut(u) {
936 user.acl_names.retain(|an| an != &name);
937 }
938 }
939 ok(json!({ "ACL": acl_json(&a) }))
940 }
941
942 fn create_user(&self, req: &AwsRequest) -> Result<AwsResponse, AwsServiceError> {
943 let b = parse(req)?;
944 let name = req_str(&b, "UserName")?.to_string();
945 validate_user_name(&name)?;
946 let access_string = req_str(&b, "AccessString")?.to_string();
947 let auth = b.get("AuthenticationMode").ok_or_else(|| {
951 fault(
952 "InvalidParameterValueException",
953 "AuthenticationMode is required.",
954 )
955 })?;
956 let auth_type = auth
957 .get("Type")
958 .and_then(Value::as_str)
959 .ok_or_else(|| {
960 fault(
961 "InvalidParameterValueException",
962 "AuthenticationMode.Type is required.",
963 )
964 })?
965 .to_string();
966 if !matches!(auth_type.as_str(), "password" | "iam") {
967 return Err(fault(
968 "InvalidParameterValueException",
969 &format!(
970 "Invalid AuthenticationMode.Type: {auth_type}. Valid values: password, iam."
971 ),
972 ));
973 }
974 let pw_count = auth
975 .get("Passwords")
976 .and_then(Value::as_array)
977 .map(|a| a.len() as i32)
978 .unwrap_or(0);
979 if auth_type == "password" && pw_count == 0 {
980 return Err(fault(
981 "InvalidParameterValueException",
982 "A password-authenticated user requires at least one password.",
983 ));
984 }
985 let mut accounts = self.state.write();
986 let st = accounts.get_or_create(&req.account_id);
987 if st.users.contains_key(&name) {
988 return Err(fault(
989 "UserAlreadyExistsFault",
990 &format!("User {name} already exists."),
991 ));
992 }
993 let user_arn = arn("user", &req.region, &req.account_id, &name);
994 let user = User {
995 name: name.clone(),
996 status: "active".to_string(),
997 access_string,
998 acl_names: vec![],
999 minimum_engine_version: "6.2".to_string(),
1000 authentication: UserAuthentication {
1001 type_: if auth_type == "iam" {
1002 "iam".to_string()
1003 } else {
1004 auth_type
1005 },
1006 password_count: pw_count,
1007 },
1008 arn: user_arn.clone(),
1009 };
1010 let tags = parse_tags(&b);
1011 if !tags.is_empty() {
1012 st.tags.insert(user_arn, tags);
1013 }
1014 st.users.insert(name, user.clone());
1015 ok(json!({ "User": user_json(&user) }))
1016 }
1017
1018 fn describe_users(&self, req: &AwsRequest) -> Result<AwsResponse, AwsServiceError> {
1019 let b = parse(req)?;
1020 let accounts = self.state.read();
1021 let st = accounts.get(&req.account_id);
1022 let Some(st) = st else {
1023 return ok(json!({ "Users": [] }));
1024 };
1025 if let Some(name) = opt_str(&b, "UserName") {
1026 let Some(u) = st.users.get(&name) else {
1027 return Err(fault(
1028 "UserNotFoundFault",
1029 &format!("User {name} not found."),
1030 ));
1031 };
1032 return ok(json!({ "Users": [user_json(u)] }));
1033 }
1034 let items: Vec<Value> = st.users.values().map(user_json).collect();
1035 let (page, next) = paginate(items, &b, 100);
1036 ok(json!({ "Users": page, "NextToken": next }))
1037 }
1038
1039 fn update_user(&self, req: &AwsRequest) -> Result<AwsResponse, AwsServiceError> {
1040 let b = parse(req)?;
1041 let name = req_str(&b, "UserName")?.to_string();
1042 let mut accounts = self.state.write();
1043 let st = accounts.get_or_create(&req.account_id);
1044 let Some(u) = st.users.get_mut(&name) else {
1045 return Err(fault(
1046 "UserNotFoundFault",
1047 &format!("User {name} not found."),
1048 ));
1049 };
1050 if let Some(a) = opt_str(&b, "AccessString") {
1051 u.access_string = a;
1052 }
1053 if let Some(auth) = b.get("AuthenticationMode") {
1054 if let Some(t) = auth.get("Type").and_then(Value::as_str) {
1055 u.authentication.type_ = t.to_string();
1056 }
1057 if let Some(p) = auth.get("Passwords").and_then(Value::as_array) {
1058 u.authentication.password_count = p.len() as i32;
1059 }
1060 }
1061 let out = user_json(u);
1062 ok(json!({ "User": out }))
1063 }
1064
1065 fn delete_user(&self, req: &AwsRequest) -> Result<AwsResponse, AwsServiceError> {
1066 let b = parse(req)?;
1067 let name = req_str(&b, "UserName")?.to_string();
1068 validate_user_name(&name)?;
1069 let mut accounts = self.state.write();
1070 let st = accounts.get_or_create(&req.account_id);
1071 let Some(mut u) = st.users.remove(&name) else {
1072 return Err(fault(
1073 "UserNotFoundFault",
1074 &format!("User {name} not found."),
1075 ));
1076 };
1077 u.status = "deleting".to_string();
1078 st.tags.remove(&u.arn);
1079 ok(json!({ "User": user_json(&u) }))
1080 }
1081
1082 fn create_parameter_group(&self, req: &AwsRequest) -> Result<AwsResponse, AwsServiceError> {
1083 let b = parse(req)?;
1084 let name = req_str(&b, "ParameterGroupName")?.to_string();
1085 let family = req_str(&b, "Family")?.to_string();
1086 let mut accounts = self.state.write();
1087 let st = accounts.get_or_create(&req.account_id);
1088 if st.parameter_groups.contains_key(&name) {
1089 return Err(fault(
1090 "ParameterGroupAlreadyExistsFault",
1091 &format!("Parameter group {name} already exists."),
1092 ));
1093 }
1094 let pg_arn = arn("parametergroup", &req.region, &req.account_id, &name);
1095 let pg = ParameterGroup {
1096 name: name.clone(),
1097 family,
1098 description: opt_str(&b, "Description").unwrap_or_default(),
1099 arn: pg_arn.clone(),
1100 parameters: BTreeMap::new(),
1101 };
1102 let tags = parse_tags(&b);
1103 if !tags.is_empty() {
1104 st.tags.insert(pg_arn, tags);
1105 }
1106 st.parameter_groups.insert(name, pg.clone());
1107 ok(json!({ "ParameterGroup": pg_json(&pg) }))
1108 }
1109
1110 fn describe_parameter_groups(&self, req: &AwsRequest) -> Result<AwsResponse, AwsServiceError> {
1111 let b = parse(req)?;
1112 let accounts = self.state.read();
1113 let st = accounts.get(&req.account_id);
1114 let Some(st) = st else {
1115 return ok(json!({ "ParameterGroups": [] }));
1116 };
1117 if let Some(name) = opt_str(&b, "ParameterGroupName") {
1118 let Some(p) = st.parameter_groups.get(&name) else {
1119 return Err(fault(
1120 "ParameterGroupNotFoundFault",
1121 &format!("Parameter group {name} not found."),
1122 ));
1123 };
1124 return ok(json!({ "ParameterGroups": [pg_json(p)] }));
1125 }
1126 let items: Vec<Value> = st.parameter_groups.values().map(pg_json).collect();
1127 let (page, next) = paginate(items, &b, 100);
1128 ok(json!({ "ParameterGroups": page, "NextToken": next }))
1129 }
1130
1131 fn update_parameter_group(&self, req: &AwsRequest) -> Result<AwsResponse, AwsServiceError> {
1132 let b = parse(req)?;
1133 let name = req_str(&b, "ParameterGroupName")?.to_string();
1134 let mut accounts = self.state.write();
1135 let st = accounts.get_or_create(&req.account_id);
1136 let Some(p) = st.parameter_groups.get_mut(&name) else {
1137 return Err(fault(
1138 "ParameterGroupNotFoundFault",
1139 &format!("Parameter group {name} not found."),
1140 ));
1141 };
1142 if let Some(arr) = b.get("ParameterNameValues").and_then(Value::as_array) {
1143 for pv in arr {
1144 if let (Some(pn), Some(pval)) = (
1145 pv.get("ParameterName").and_then(Value::as_str),
1146 pv.get("ParameterValue").and_then(Value::as_str),
1147 ) {
1148 p.parameters.insert(pn.to_string(), pval.to_string());
1149 }
1150 }
1151 }
1152 let out = pg_json(p);
1153 ok(json!({ "ParameterGroup": out }))
1154 }
1155
1156 fn reset_parameter_group(&self, req: &AwsRequest) -> Result<AwsResponse, AwsServiceError> {
1157 let b = parse(req)?;
1158 let name = req_str(&b, "ParameterGroupName")?.to_string();
1159 let mut accounts = self.state.write();
1160 let st = accounts.get_or_create(&req.account_id);
1161 let Some(p) = st.parameter_groups.get_mut(&name) else {
1162 return Err(fault(
1163 "ParameterGroupNotFoundFault",
1164 &format!("Parameter group {name} not found."),
1165 ));
1166 };
1167 let all = b
1168 .get("AllParameters")
1169 .and_then(Value::as_bool)
1170 .unwrap_or(false);
1171 if all {
1172 p.parameters.clear();
1173 } else {
1174 for pv in b
1175 .get("ParameterNames")
1176 .and_then(Value::as_array)
1177 .into_iter()
1178 .flatten()
1179 {
1180 if let Some(pn) = pv.as_str() {
1181 p.parameters.remove(pn);
1182 }
1183 }
1184 }
1185 let out = pg_json(p);
1186 ok(json!({ "ParameterGroup": out }))
1187 }
1188
1189 fn delete_parameter_group(&self, req: &AwsRequest) -> Result<AwsResponse, AwsServiceError> {
1190 let b = parse(req)?;
1191 let name = req_str(&b, "ParameterGroupName")?.to_string();
1192 let mut accounts = self.state.write();
1193 let st = accounts.get_or_create(&req.account_id);
1194 let Some(p) = st.parameter_groups.remove(&name) else {
1195 return Err(fault(
1196 "ParameterGroupNotFoundFault",
1197 &format!("Parameter group {name} not found."),
1198 ));
1199 };
1200 st.tags.remove(&p.arn);
1201 ok(json!({ "ParameterGroup": pg_json(&p) }))
1202 }
1203
1204 fn describe_parameters(&self, req: &AwsRequest) -> Result<AwsResponse, AwsServiceError> {
1205 let b = parse(req)?;
1206 let name = req_str(&b, "ParameterGroupName")?.to_string();
1207 let accounts = self.state.read();
1208 let st = accounts.get(&req.account_id);
1209 let Some(p) = st.and_then(|s| s.parameter_groups.get(&name)) else {
1210 return Err(fault(
1211 "ParameterGroupNotFoundFault",
1212 &format!("Parameter group {name} not found."),
1213 ));
1214 };
1215 let params: Vec<Value> = p
1216 .parameters
1217 .iter()
1218 .map(|(k, v)| {
1219 json!({
1220 "Name": k, "Value": v, "Description": "", "DataType": "string",
1221 "AllowedValues": "", "MinimumEngineVersion": "6.2",
1222 })
1223 })
1224 .collect();
1225 let (page, next) = paginate(params, &b, 100);
1226 ok(json!({ "Parameters": page, "NextToken": next }))
1227 }
1228
1229 fn create_subnet_group(&self, req: &AwsRequest) -> Result<AwsResponse, AwsServiceError> {
1230 let b = parse(req)?;
1231 let name = req_str(&b, "SubnetGroupName")?.to_string();
1232 let subnet_ids = str_list(&b, "SubnetIds");
1233 let mut accounts = self.state.write();
1234 let st = accounts.get_or_create(&req.account_id);
1235 if st.subnet_groups.contains_key(&name) {
1236 return Err(fault(
1237 "SubnetGroupAlreadyExistsFault",
1238 &format!("Subnet group {name} already exists."),
1239 ));
1240 }
1241 let sg_arn = arn("subnetgroup", &req.region, &req.account_id, &name);
1242 let sg = SubnetGroup {
1243 name: name.clone(),
1244 description: opt_str(&b, "Description").unwrap_or_default(),
1245 vpc_id: "vpc-00000000".to_string(),
1246 subnet_ids,
1247 arn: sg_arn.clone(),
1248 supported_network_types: vec!["ipv4".to_string()],
1249 };
1250 let tags = parse_tags(&b);
1251 if !tags.is_empty() {
1252 st.tags.insert(sg_arn, tags);
1253 }
1254 st.subnet_groups.insert(name, sg.clone());
1255 ok(json!({ "SubnetGroup": sg_json(&sg) }))
1256 }
1257
1258 fn describe_subnet_groups(&self, req: &AwsRequest) -> Result<AwsResponse, AwsServiceError> {
1259 let b = parse(req)?;
1260 let accounts = self.state.read();
1261 let st = accounts.get(&req.account_id);
1262 let Some(st) = st else {
1263 return ok(json!({ "SubnetGroups": [] }));
1264 };
1265 if let Some(name) = opt_str(&b, "SubnetGroupName") {
1266 let Some(g) = st.subnet_groups.get(&name) else {
1267 return Err(fault(
1268 "SubnetGroupNotFoundFault",
1269 &format!("Subnet group {name} not found."),
1270 ));
1271 };
1272 return ok(json!({ "SubnetGroups": [sg_json(g)] }));
1273 }
1274 let items: Vec<Value> = st.subnet_groups.values().map(sg_json).collect();
1275 let (page, next) = paginate(items, &b, 100);
1276 ok(json!({ "SubnetGroups": page, "NextToken": next }))
1277 }
1278
1279 fn update_subnet_group(&self, req: &AwsRequest) -> Result<AwsResponse, AwsServiceError> {
1280 let b = parse(req)?;
1281 let name = req_str(&b, "SubnetGroupName")?.to_string();
1282 let mut accounts = self.state.write();
1283 let st = accounts.get_or_create(&req.account_id);
1284 let Some(g) = st.subnet_groups.get_mut(&name) else {
1285 return Err(fault(
1286 "SubnetGroupNotFoundFault",
1287 &format!("Subnet group {name} not found."),
1288 ));
1289 };
1290 if let Some(d) = opt_str(&b, "Description") {
1291 g.description = d;
1292 }
1293 let ids = str_list(&b, "SubnetIds");
1294 if !ids.is_empty() {
1295 g.subnet_ids = ids;
1296 }
1297 let out = sg_json(g);
1298 ok(json!({ "SubnetGroup": out }))
1299 }
1300
1301 fn delete_subnet_group(&self, req: &AwsRequest) -> Result<AwsResponse, AwsServiceError> {
1302 let b = parse(req)?;
1303 let name = req_str(&b, "SubnetGroupName")?.to_string();
1304 let mut accounts = self.state.write();
1305 let st = accounts.get_or_create(&req.account_id);
1306 let Some(g) = st.subnet_groups.remove(&name) else {
1307 return Err(fault(
1308 "SubnetGroupNotFoundFault",
1309 &format!("Subnet group {name} not found."),
1310 ));
1311 };
1312 st.tags.remove(&g.arn);
1313 ok(json!({ "SubnetGroup": sg_json(&g) }))
1314 }
1315
1316 fn create_snapshot(&self, req: &AwsRequest) -> Result<AwsResponse, AwsServiceError> {
1317 let b = parse(req)?;
1318 let cluster_name = req_str(&b, "ClusterName")?.to_string();
1319 let snap_name = req_str(&b, "SnapshotName")?.to_string();
1320 let mut accounts = self.state.write();
1321 let st = accounts.get_or_create(&req.account_id);
1322 if st.snapshots.contains_key(&snap_name) {
1323 return Err(fault(
1324 "SnapshotAlreadyExistsFault",
1325 &format!("Snapshot {snap_name} already exists."),
1326 ));
1327 }
1328 let Some(c) = st.clusters.get(&cluster_name) else {
1329 return Err(fault(
1330 "ClusterNotFoundFault",
1331 &format!("Cluster {cluster_name} not found."),
1332 ));
1333 };
1334 let cfg = json!({
1335 "Name": c.name, "Description": c.description, "NodeType": c.node_type,
1336 "Engine": c.engine, "EngineVersion": c.engine_version, "MaintenanceWindow": c.maintenance_window,
1337 "TopicArn": c.sns_topic_arn, "Port": c.port, "ParameterGroupName": c.parameter_group_name,
1338 "SubnetGroupName": c.subnet_group_name, "VpcId": "vpc-00000000",
1339 "SnapshotRetentionLimit": c.snapshot_retention_limit, "SnapshotWindow": c.snapshot_window,
1340 "NumShards": c.number_of_shards,
1341 });
1342 let snap_arn = arn("snapshot", &req.region, &req.account_id, &snap_name);
1343 let snap = Snapshot {
1344 name: snap_name.clone(),
1345 status: "available".to_string(),
1346 source: "manual".to_string(),
1347 kms_key_id: opt_str(&b, "KmsKeyId"),
1348 arn: snap_arn.clone(),
1349 data_tiering: c.data_tiering.clone(),
1350 cluster_configuration: cfg,
1351 };
1352 let tags = parse_tags(&b);
1353 if !tags.is_empty() {
1354 st.tags.insert(snap_arn, tags);
1355 }
1356 st.snapshots.insert(snap_name, snap.clone());
1357 ok(json!({ "Snapshot": snapshot_json(&snap) }))
1358 }
1359
1360 fn copy_snapshot(&self, req: &AwsRequest) -> Result<AwsResponse, AwsServiceError> {
1361 let b = parse(req)?;
1362 let source = req_str(&b, "SourceSnapshotName")?.to_string();
1363 let target = req_str(&b, "TargetSnapshotName")?.to_string();
1364 let mut accounts = self.state.write();
1365 let st = accounts.get_or_create(&req.account_id);
1366 let Some(src) = st.snapshots.get(&source).cloned() else {
1367 return Err(fault(
1368 "SnapshotNotFoundFault",
1369 &format!("Snapshot {source} not found."),
1370 ));
1371 };
1372 if st.snapshots.contains_key(&target) {
1373 return Err(fault(
1374 "SnapshotAlreadyExistsFault",
1375 &format!("Snapshot {target} already exists."),
1376 ));
1377 }
1378 let snap_arn = arn("snapshot", &req.region, &req.account_id, &target);
1379 let snap = Snapshot {
1380 name: target.clone(),
1381 status: "available".to_string(),
1382 source: "manual".to_string(),
1383 kms_key_id: opt_str(&b, "KmsKeyId").or(src.kms_key_id),
1384 arn: snap_arn,
1385 data_tiering: src.data_tiering,
1386 cluster_configuration: src.cluster_configuration,
1387 };
1388 st.snapshots.insert(target, snap.clone());
1389 ok(json!({ "Snapshot": snapshot_json(&snap) }))
1390 }
1391
1392 fn describe_snapshots(&self, req: &AwsRequest) -> Result<AwsResponse, AwsServiceError> {
1393 let b = parse(req)?;
1394 let accounts = self.state.read();
1395 let st = accounts.get(&req.account_id);
1396 let Some(st) = st else {
1397 return ok(json!({ "Snapshots": [] }));
1398 };
1399 let show_detail = b.get("ShowDetail").and_then(Value::as_bool).unwrap_or(true);
1401 if let Some(name) = opt_str(&b, "SnapshotName") {
1402 let Some(s) = st.snapshots.get(&name) else {
1403 return Err(fault(
1404 "SnapshotNotFoundFault",
1405 &format!("Snapshot {name} not found."),
1406 ));
1407 };
1408 return ok(json!({ "Snapshots": [snapshot_json_detail(s, show_detail)] }));
1409 }
1410 let cluster_filter = opt_str(&b, "ClusterName");
1411 let source_filter = opt_str(&b, "Source").filter(|s| s != "all");
1413 let items: Vec<Value> = st
1414 .snapshots
1415 .values()
1416 .filter(|s| {
1417 cluster_filter.as_ref().is_none_or(|cn| {
1418 s.cluster_configuration.get("Name").and_then(Value::as_str) == Some(cn)
1419 })
1420 })
1421 .filter(|s| source_filter.as_ref().is_none_or(|src| &s.source == src))
1422 .map(|s| snapshot_json_detail(s, show_detail))
1423 .collect();
1424 let (page, next) = paginate(items, &b, 50);
1425 ok(json!({ "Snapshots": page, "NextToken": next }))
1426 }
1427
1428 fn delete_snapshot(&self, req: &AwsRequest) -> Result<AwsResponse, AwsServiceError> {
1429 let b = parse(req)?;
1430 let name = req_str(&b, "SnapshotName")?.to_string();
1431 let mut accounts = self.state.write();
1432 let st = accounts.get_or_create(&req.account_id);
1433 let Some(mut s) = st.snapshots.remove(&name) else {
1434 return Err(fault(
1435 "SnapshotNotFoundFault",
1436 &format!("Snapshot {name} not found."),
1437 ));
1438 };
1439 s.status = "deleting".to_string();
1440 st.tags.remove(&s.arn);
1441 ok(json!({ "Snapshot": snapshot_json(&s) }))
1442 }
1443
1444 fn create_multi_region_cluster(
1445 &self,
1446 req: &AwsRequest,
1447 ) -> Result<AwsResponse, AwsServiceError> {
1448 let b = parse(req)?;
1449 let suffix = req_str(&b, "MultiRegionClusterNameSuffix")?.to_string();
1450 let node_type = req_str(&b, "NodeType")?.to_string();
1451 let name = format!("virxk-{suffix}");
1452 let mut accounts = self.state.write();
1453 let st = accounts.get_or_create(&req.account_id);
1454 if st.multi_region_clusters.contains_key(&name) {
1455 return Err(fault(
1456 "MultiRegionClusterAlreadyExistsFault",
1457 &format!("Multi-region cluster {name} already exists."),
1458 ));
1459 }
1460 let mrc_arn = arn("multiregioncluster", &req.region, &req.account_id, &name);
1461 let mrc = MultiRegionCluster {
1462 name: name.clone(),
1463 description: opt_str(&b, "Description"),
1464 status: "creating".to_string(),
1465 node_type,
1466 engine: opt_str(&b, "Engine").unwrap_or_else(|| DEFAULT_ENGINE.to_string()),
1467 engine_version: opt_str(&b, "EngineVersion")
1468 .unwrap_or_else(|| DEFAULT_ENGINE_VERSION.to_string()),
1469 number_of_shards: int_field(&b, "NumShards", 1, 1, 500)?,
1470 multi_region_parameter_group_name: opt_str(&b, "MultiRegionParameterGroupName"),
1471 tls_enabled: b.get("TLSEnabled").and_then(Value::as_bool).unwrap_or(true),
1472 arn: mrc_arn.clone(),
1473 clusters: json!([]),
1474 };
1475 let tags = parse_tags(&b);
1476 if !tags.is_empty() {
1477 st.tags.insert(mrc_arn, tags);
1478 }
1479 st.multi_region_clusters.insert(name, mrc.clone());
1480 ok(json!({ "MultiRegionCluster": mrc_json(&mrc) }))
1481 }
1482
1483 fn describe_multi_region_clusters(
1484 &self,
1485 req: &AwsRequest,
1486 ) -> Result<AwsResponse, AwsServiceError> {
1487 let b = parse(req)?;
1488 let mut accounts = self.state.write();
1489 let st = accounts.get_or_create(&req.account_id);
1490 for m in st.multi_region_clusters.values_mut() {
1491 if m.status == "creating" {
1492 m.status = "available".to_string();
1493 }
1494 }
1495 if let Some(name) = opt_str(&b, "MultiRegionClusterName") {
1496 let Some(m) = st.multi_region_clusters.get(&name) else {
1497 return Err(fault(
1498 "MultiRegionClusterNotFoundFault",
1499 &format!("Multi-region cluster {name} not found."),
1500 ));
1501 };
1502 return ok(json!({ "MultiRegionClusters": [mrc_json(m)] }));
1503 }
1504 let items: Vec<Value> = st.multi_region_clusters.values().map(mrc_json).collect();
1505 let (page, next) = paginate(items, &b, 100);
1506 ok(json!({ "MultiRegionClusters": page, "NextToken": next }))
1507 }
1508
1509 fn update_multi_region_cluster(
1510 &self,
1511 req: &AwsRequest,
1512 ) -> Result<AwsResponse, AwsServiceError> {
1513 let b = parse(req)?;
1514 let name = req_str(&b, "MultiRegionClusterName")?.to_string();
1515 let mut accounts = self.state.write();
1516 let st = accounts.get_or_create(&req.account_id);
1517 let Some(m) = st.multi_region_clusters.get_mut(&name) else {
1518 return Err(fault(
1519 "MultiRegionClusterNotFoundFault",
1520 &format!("Multi-region cluster {name} not found."),
1521 ));
1522 };
1523 if let Some(d) = opt_str(&b, "Description") {
1524 m.description = Some(d);
1525 }
1526 if let Some(v) = opt_str(&b, "EngineVersion") {
1527 m.engine_version = v;
1528 }
1529 if let Some(v) = opt_str(&b, "NodeType") {
1530 m.node_type = v;
1531 }
1532 let out = mrc_json(m);
1533 ok(json!({ "MultiRegionCluster": out }))
1534 }
1535
1536 fn delete_multi_region_cluster(
1537 &self,
1538 req: &AwsRequest,
1539 ) -> Result<AwsResponse, AwsServiceError> {
1540 let b = parse(req)?;
1541 let name = req_str(&b, "MultiRegionClusterName")?.to_string();
1542 let mut accounts = self.state.write();
1543 let st = accounts.get_or_create(&req.account_id);
1544 let Some(mut m) = st.multi_region_clusters.remove(&name) else {
1545 return Err(fault(
1546 "MultiRegionClusterNotFoundFault",
1547 &format!("Multi-region cluster {name} not found."),
1548 ));
1549 };
1550 m.status = "deleting".to_string();
1551 st.tags.remove(&m.arn);
1552 ok(json!({ "MultiRegionCluster": mrc_json(&m) }))
1553 }
1554
1555 fn list_allowed_mr_updates(&self, req: &AwsRequest) -> Result<AwsResponse, AwsServiceError> {
1556 let b = parse(req)?;
1557 let name = req_str(&b, "MultiRegionClusterName")?.to_string();
1558 let accounts = self.state.read();
1559 if accounts
1560 .get(&req.account_id)
1561 .and_then(|s| s.multi_region_clusters.get(&name))
1562 .is_none()
1563 {
1564 return Err(fault(
1565 "MultiRegionClusterNotFoundFault",
1566 &format!("Multi-region cluster {name} not found."),
1567 ));
1568 }
1569 ok(
1570 json!({ "ScaleUpNodeTypes": ["db.r7g.large", "db.r7g.xlarge"], "ScaleDownNodeTypes": [] }),
1571 )
1572 }
1573
1574 fn describe_mr_parameter_groups(
1575 &self,
1576 req: &AwsRequest,
1577 ) -> Result<AwsResponse, AwsServiceError> {
1578 let _ = parse(req)?;
1579 ok(json!({ "MultiRegionParameterGroups": [], "NextToken": Value::Null }))
1580 }
1581
1582 fn describe_mr_parameters(&self, req: &AwsRequest) -> Result<AwsResponse, AwsServiceError> {
1583 let b = parse(req)?;
1584 req_str(&b, "MultiRegionParameterGroupName")?;
1585 ok(json!({ "MultiRegionParameters": [], "NextToken": Value::Null }))
1586 }
1587
1588 fn tag_resource(&self, req: &AwsRequest) -> Result<AwsResponse, AwsServiceError> {
1589 let b = parse(req)?;
1590 let resource_arn = req_str(&b, "ResourceArn")?.to_string();
1591 let mut accounts = self.state.write();
1592 let st = accounts.get_or_create(&req.account_id);
1593 if !arn_exists(st, &resource_arn) {
1594 return Err(fault(
1595 "InvalidARNFault",
1596 &format!("{resource_arn} does not refer to a known resource."),
1597 ));
1598 }
1599 let entry = st.tags.entry(resource_arn).or_default();
1600 for (k, v) in parse_tags(&b) {
1601 entry.insert(k, v);
1602 }
1603 let all = entry.clone();
1604 ok(json!({ "TagList": tags_json(&all) }))
1605 }
1606
1607 fn untag_resource(&self, req: &AwsRequest) -> Result<AwsResponse, AwsServiceError> {
1608 let b = parse(req)?;
1609 let resource_arn = req_str(&b, "ResourceArn")?.to_string();
1610 let mut accounts = self.state.write();
1611 let st = accounts.get_or_create(&req.account_id);
1612 if !arn_exists(st, &resource_arn) {
1613 return Err(fault(
1614 "InvalidARNFault",
1615 &format!("{resource_arn} does not refer to a known resource."),
1616 ));
1617 }
1618 let keys = str_list(&b, "TagKeys");
1619 let entry = st.tags.entry(resource_arn).or_default();
1620 for k in &keys {
1621 entry.remove(k);
1622 }
1623 let all = entry.clone();
1624 ok(json!({ "TagList": tags_json(&all) }))
1625 }
1626
1627 fn list_tags(&self, req: &AwsRequest) -> Result<AwsResponse, AwsServiceError> {
1628 let b = parse(req)?;
1629 let resource_arn = req_str(&b, "ResourceArn")?.to_string();
1630 let accounts = self.state.read();
1631 let st = accounts.get(&req.account_id);
1632 let Some(st) = st else {
1633 return ok(json!({ "TagList": [] }));
1634 };
1635 if !arn_exists(st, &resource_arn) {
1636 return Err(fault(
1637 "InvalidARNFault",
1638 &format!("{resource_arn} does not refer to a known resource."),
1639 ));
1640 }
1641 let tags = st.tags.get(&resource_arn).cloned().unwrap_or_default();
1642 ok(json!({ "TagList": tags_json(&tags) }))
1643 }
1644
1645 fn describe_engine_versions(&self, req: &AwsRequest) -> Result<AwsResponse, AwsServiceError> {
1646 let _ = parse(req)?;
1647 let versions = json!([
1648 { "Engine": "redis", "EngineVersion": "7.1", "EnginePatchVersion": "7.1.1", "ParameterGroupFamily": "memorydb_redis7" },
1649 { "Engine": "redis", "EngineVersion": "6.2", "EnginePatchVersion": "6.2.6", "ParameterGroupFamily": "memorydb_redis6" },
1650 { "Engine": "valkey", "EngineVersion": "7.2", "EnginePatchVersion": "7.2.0", "ParameterGroupFamily": "memorydb_valkey7" },
1651 ]);
1652 ok(json!({ "EngineVersions": versions, "NextToken": Value::Null }))
1653 }
1654
1655 fn describe_events(&self, req: &AwsRequest) -> Result<AwsResponse, AwsServiceError> {
1656 let b = parse(req)?;
1657 if let Some(st) = opt_str(&b, "SourceType") {
1658 const VALID: &[&str] = &[
1659 "node",
1660 "parameter_group",
1661 "subnet_group",
1662 "cluster",
1663 "user",
1664 "acl",
1665 ];
1666 if !VALID.contains(&st.as_str()) {
1667 return Err(fault(
1668 "InvalidParameterValueException",
1669 &format!("Invalid SourceType: {st}"),
1670 ));
1671 }
1672 }
1673 ok(json!({ "Events": [], "NextToken": Value::Null }))
1674 }
1675
1676 fn describe_service_updates(&self, req: &AwsRequest) -> Result<AwsResponse, AwsServiceError> {
1677 let _ = parse(req)?;
1678 ok(json!({ "ServiceUpdates": [], "NextToken": Value::Null }))
1679 }
1680
1681 fn describe_reserved_nodes(&self, req: &AwsRequest) -> Result<AwsResponse, AwsServiceError> {
1682 let b = parse(req)?;
1683 let accounts = self.state.read();
1684 let st = accounts.get(&req.account_id);
1685 let Some(st) = st else {
1686 return ok(json!({ "ReservedNodes": [] }));
1687 };
1688 let items: Vec<Value> = st.reserved_nodes.values().map(reserved_node_json).collect();
1689 let (page, next) = paginate(items, &b, 100);
1690 ok(json!({ "ReservedNodes": page, "NextToken": next }))
1691 }
1692
1693 fn describe_reserved_nodes_offerings(
1694 &self,
1695 req: &AwsRequest,
1696 ) -> Result<AwsResponse, AwsServiceError> {
1697 let _ = parse(req)?;
1698 let offerings = json!([
1699 {
1700 "ReservedNodesOfferingId": "00000000-1111-2222-3333-444444444444",
1701 "NodeType": "db.r6g.large", "Duration": 31536000, "FixedPrice": 1200.0,
1702 "OfferingType": "All Upfront", "RecurringCharges": [],
1703 }
1704 ]);
1705 ok(json!({ "ReservedNodesOfferings": offerings, "NextToken": Value::Null }))
1706 }
1707
1708 fn purchase_reserved_nodes_offering(
1709 &self,
1710 req: &AwsRequest,
1711 ) -> Result<AwsResponse, AwsServiceError> {
1712 let b = parse(req)?;
1713 let offering_id = req_str(&b, "ReservedNodesOfferingId")?.to_string();
1714 let reservation_id = opt_str(&b, "ReservationId")
1715 .unwrap_or_else(|| format!("ri-{}", uuid::Uuid::new_v4().simple()));
1716 let count = int_field(&b, "NodeCount", 1, 1, i32::MAX as i64)?;
1717 let mut accounts = self.state.write();
1718 let st = accounts.get_or_create(&req.account_id);
1719 let node_arn = arn(
1720 "reservednode",
1721 &req.region,
1722 &req.account_id,
1723 &reservation_id,
1724 );
1725 let rn = ReservedNode {
1726 reservation_id: reservation_id.clone(),
1727 reserved_nodes_offering_id: offering_id,
1728 node_type: "db.r6g.large".to_string(),
1729 start_time: Utc::now(),
1730 duration: 31536000,
1731 fixed_price: 1200.0,
1732 node_count: count,
1733 offering_type: "All Upfront".to_string(),
1734 state: "active".to_string(),
1735 arn: node_arn,
1736 };
1737 st.reserved_nodes.insert(reservation_id, rn.clone());
1738 ok(json!({ "ReservedNode": reserved_node_json(&rn) }))
1739 }
1740
1741 fn list_allowed_node_type_updates(
1742 &self,
1743 req: &AwsRequest,
1744 ) -> Result<AwsResponse, AwsServiceError> {
1745 let b = parse(req)?;
1746 let name = req_str(&b, "ClusterName")?.to_string();
1747 let accounts = self.state.read();
1748 if accounts
1749 .get(&req.account_id)
1750 .and_then(|s| s.clusters.get(&name))
1751 .is_none()
1752 {
1753 return Err(fault(
1754 "ClusterNotFoundFault",
1755 &format!("Cluster {name} not found."),
1756 ));
1757 }
1758 ok(json!({
1759 "ScaleUpNodeTypes": ["db.r6g.xlarge", "db.r6g.2xlarge"],
1760 "ScaleDownNodeTypes": ["db.r6g.large"],
1761 }))
1762 }
1763}
1764
1765fn validate_user_name(name: &str) -> Result<(), AwsServiceError> {
1768 let valid = !name.is_empty()
1769 && name.starts_with(|c: char| c.is_ascii_alphabetic())
1770 && name.chars().all(|c| c.is_ascii_alphanumeric() || c == '-');
1771 if valid {
1772 Ok(())
1773 } else {
1774 Err(fault(
1775 "InvalidParameterValueException",
1776 &format!("Invalid UserName: {name}"),
1777 ))
1778 }
1779}
1780
1781fn arn_exists(st: &MemoryDbState, resource_arn: &str) -> bool {
1783 if resource_arn.is_empty() {
1786 return false;
1787 }
1788 st.clusters.values().any(|c| c.arn == resource_arn)
1789 || st.acls.values().any(|a| a.arn == resource_arn)
1790 || st.users.values().any(|u| u.arn == resource_arn)
1791 || st.parameter_groups.values().any(|p| p.arn == resource_arn)
1792 || st.subnet_groups.values().any(|g| g.arn == resource_arn)
1793 || st.snapshots.values().any(|s| s.arn == resource_arn)
1794 || st
1795 .multi_region_clusters
1796 .values()
1797 .any(|m| m.arn == resource_arn)
1798 || st.reserved_nodes.values().any(|r| r.arn == resource_arn)
1799}
1800
1801#[cfg(test)]
1802mod tests {
1803 use super::*;
1804 use fakecloud_core::multi_account::MultiAccountState;
1805
1806 fn service() -> MemoryDbService {
1807 let state = Arc::new(parking_lot::RwLock::new(MultiAccountState::new(
1808 "123456789012",
1809 "us-east-1",
1810 "http://localhost:4566",
1811 )));
1812 MemoryDbService::new(state)
1813 }
1814
1815 fn req(action: &str, body: Value) -> AwsRequest {
1816 AwsRequest {
1817 service: "memorydb".to_string(),
1818 action: action.to_string(),
1819 region: "us-east-1".to_string(),
1820 account_id: "123456789012".to_string(),
1821 request_id: "rid".to_string(),
1822 headers: http::HeaderMap::new(),
1823 query_params: std::collections::HashMap::new(),
1824 body: bytes::Bytes::from(serde_json::to_vec(&body).unwrap()),
1825 body_stream: parking_lot::Mutex::new(None),
1826 path_segments: vec![],
1827 raw_path: "/".to_string(),
1828 raw_query: String::new(),
1829 method: http::Method::POST,
1830 is_query_protocol: false,
1831 access_key_id: None,
1832 principal: None,
1833 }
1834 }
1835
1836 fn call(s: &MemoryDbService, action: &str, body: Value) -> Result<Value, AwsServiceError> {
1837 let resp = dispatch(s, &req(action, body))?;
1838 Ok(serde_json::from_slice(resp.body.expect_bytes()).unwrap())
1839 }
1840
1841 #[test]
1842 fn create_cluster_round_trips_and_describes() {
1843 let s = service();
1844 let out = call(
1845 &s,
1846 "CreateCluster",
1847 json!({"ClusterName": "app", "NodeType": "db.r6g.large", "ACLName": "open-access"}),
1848 )
1849 .unwrap();
1850 assert_eq!(out["Cluster"]["Name"], "app");
1851
1852 let desc = call(&s, "DescribeClusters", json!({"ClusterName": "app"})).unwrap();
1853 let clusters = desc["Clusters"].as_array().unwrap();
1854 assert_eq!(clusters.len(), 1);
1855 assert_eq!(clusters[0]["Status"], "available");
1857 }
1858
1859 #[test]
1860 fn create_cluster_rejects_out_of_range_shard_count() {
1861 let s = service();
1862 let err = call(
1864 &s,
1865 "CreateCluster",
1866 json!({"ClusterName": "big", "NodeType": "db.r6g.large", "ACLName": "open-access", "NumShards": 2_000_000_000i64}),
1867 )
1868 .unwrap_err();
1869 assert!(
1870 err.message().contains("NumShards"),
1871 "got: {}",
1872 err.message()
1873 );
1874 }
1875
1876 #[test]
1877 fn create_cluster_rejects_out_of_range_replica_count() {
1878 let s = service();
1879 let err = call(
1880 &s,
1881 "CreateCluster",
1882 json!({"ClusterName": "r", "NodeType": "db.r6g.large", "ACLName": "open-access", "NumReplicasPerShard": 99}),
1883 )
1884 .unwrap_err();
1885 assert!(
1886 err.message().contains("NumReplicasPerShard"),
1887 "got: {}",
1888 err.message()
1889 );
1890 }
1891
1892 #[test]
1893 fn create_user_rejects_invalid_name() {
1894 let s = service();
1895 let err = call(
1897 &s,
1898 "CreateUser",
1899 json!({"UserName": "1bad", "AccessString": "on ~* &* +@all", "AuthenticationMode": {"Type": "no-password-required"}}),
1900 )
1901 .unwrap_err();
1902 assert!(err.message().contains("UserName"), "got: {}", err.message());
1903 }
1904
1905 #[test]
1906 fn default_acl_and_user_are_seeded() {
1907 let s = service();
1908 let acls = call(&s, "DescribeACLs", json!({})).unwrap();
1909 assert!(acls["ACLs"]
1910 .as_array()
1911 .unwrap()
1912 .iter()
1913 .any(|a| a["Name"] == "open-access"));
1914 let users = call(&s, "DescribeUsers", json!({})).unwrap();
1915 assert!(users["Users"]
1916 .as_array()
1917 .unwrap()
1918 .iter()
1919 .any(|u| u["Name"] == "default"));
1920 }
1921
1922 #[test]
1923 fn seeded_default_acl_and_user_have_populated_arns() {
1924 let s = service();
1925 let acls = call(&s, "DescribeACLs", json!({"ACLName": "open-access"})).unwrap();
1926 assert_eq!(
1927 acls["ACLs"][0]["ARN"],
1928 "arn:aws:memorydb:us-east-1:123456789012:acl/open-access"
1929 );
1930 let users = call(&s, "DescribeUsers", json!({"UserName": "default"})).unwrap();
1931 assert_eq!(
1932 users["Users"][0]["ARN"],
1933 "arn:aws:memorydb:us-east-1:123456789012:user/default"
1934 );
1935 }
1936
1937 #[test]
1938 fn not_found_faults_return_http_404() {
1939 let s = service();
1940 let err = call(&s, "DescribeClusters", json!({"ClusterName": "nope"})).unwrap_err();
1941 assert_eq!(err.status(), http::StatusCode::NOT_FOUND);
1942 let bad = call(
1944 &s,
1945 "CreateCluster",
1946 json!({"ClusterName": "c", "NodeType": "db.r6g.large", "ACLName": "open-access", "NumShards": 9999}),
1947 )
1948 .unwrap_err();
1949 assert_eq!(bad.status(), http::StatusCode::BAD_REQUEST);
1950 }
1951
1952 #[test]
1953 fn create_cluster_populates_acl_clusters_backref() {
1954 let s = service();
1955 call(
1956 &s,
1957 "CreateCluster",
1958 json!({"ClusterName": "c1", "NodeType": "db.r6g.large", "ACLName": "open-access"}),
1959 )
1960 .unwrap();
1961 let acls = call(&s, "DescribeACLs", json!({"ACLName": "open-access"})).unwrap();
1962 let clusters = acls["ACLs"][0]["Clusters"].as_array().unwrap();
1963 assert!(clusters.iter().any(|c| c == "c1"), "got: {clusters:?}");
1964
1965 call(&s, "DeleteCluster", json!({"ClusterName": "c1"})).unwrap();
1967 let acls = call(&s, "DescribeACLs", json!({"ACLName": "open-access"})).unwrap();
1968 assert!(acls["ACLs"][0]["Clusters"].as_array().unwrap().is_empty());
1969 }
1970
1971 #[test]
1972 fn acl_membership_syncs_user_acl_names_backref() {
1973 let s = service();
1974 call(
1975 &s,
1976 "CreateUser",
1977 json!({"UserName": "bob", "AccessString": "on ~* &* +@all", "AuthenticationMode": {"Type": "iam"}}),
1978 )
1979 .unwrap();
1980 call(
1981 &s,
1982 "CreateACL",
1983 json!({"ACLName": "team", "UserNames": ["bob"]}),
1984 )
1985 .unwrap();
1986 let users = call(&s, "DescribeUsers", json!({"UserName": "bob"})).unwrap();
1987 let acl_names = users["Users"][0]["ACLNames"].as_array().unwrap();
1988 assert!(acl_names.iter().any(|a| a == "team"), "got: {acl_names:?}");
1989
1990 call(
1992 &s,
1993 "UpdateACL",
1994 json!({"ACLName": "team", "UserNamesToRemove": ["bob"]}),
1995 )
1996 .unwrap();
1997 let users = call(&s, "DescribeUsers", json!({"UserName": "bob"})).unwrap();
1998 assert!(users["Users"][0]["ACLNames"]
1999 .as_array()
2000 .unwrap()
2001 .iter()
2002 .all(|a| a != "team"));
2003 }
2004
2005 #[test]
2006 fn tag_resource_rejects_empty_arn() {
2007 let s = service();
2008 let err = call(
2009 &s,
2010 "TagResource",
2011 json!({"ResourceArn": "", "Tags": [{"Key": "k", "Value": "v"}]}),
2012 )
2013 .unwrap_err();
2014 assert_eq!(err.status(), http::StatusCode::BAD_REQUEST);
2015 assert!(
2016 err.message().contains("known resource") || err.message().contains("ARN"),
2017 "got: {}",
2018 err.message()
2019 );
2020 }
2021
2022 #[test]
2023 fn create_user_requires_valid_authentication_mode() {
2024 let s = service();
2025 let e1 = call(
2027 &s,
2028 "CreateUser",
2029 json!({"UserName": "u1", "AccessString": "on ~* &* +@all"}),
2030 )
2031 .unwrap_err();
2032 assert!(
2033 e1.message().contains("AuthenticationMode"),
2034 "got: {}",
2035 e1.message()
2036 );
2037 let e2 = call(
2039 &s,
2040 "CreateUser",
2041 json!({"UserName": "u2", "AccessString": "on ~* &* +@all", "AuthenticationMode": {"Type": "password"}}),
2042 )
2043 .unwrap_err();
2044 assert!(e2.message().contains("password"), "got: {}", e2.message());
2045 let e3 = call(
2047 &s,
2048 "CreateUser",
2049 json!({"UserName": "u3", "AccessString": "on ~* &* +@all", "AuthenticationMode": {"Type": "kerberos"}}),
2050 )
2051 .unwrap_err();
2052 assert!(
2053 e3.message().contains("kerberos") || e3.message().contains("Type"),
2054 "got: {}",
2055 e3.message()
2056 );
2057 call(
2059 &s,
2060 "CreateUser",
2061 json!({"UserName": "u4", "AccessString": "on ~* &* +@all", "AuthenticationMode": {"Type": "iam"}}),
2062 )
2063 .unwrap();
2064 }
2065
2066 #[test]
2067 fn failover_shard_validates_shard_name() {
2068 let s = service();
2069 call(
2070 &s,
2071 "CreateCluster",
2072 json!({"ClusterName": "fc", "NodeType": "db.r6g.large", "ACLName": "open-access", "NumShards": 1}),
2073 )
2074 .unwrap();
2075 let err = call(
2077 &s,
2078 "FailoverShard",
2079 json!({"ClusterName": "fc", "ShardName": "9999"}),
2080 )
2081 .unwrap_err();
2082 assert_eq!(err.status(), http::StatusCode::NOT_FOUND);
2083 call(
2085 &s,
2086 "FailoverShard",
2087 json!({"ClusterName": "fc", "ShardName": "0001"}),
2088 )
2089 .unwrap();
2090 }
2091
2092 #[test]
2093 fn batch_update_reports_unknown_clusters_as_unprocessed() {
2094 let s = service();
2095 call(
2096 &s,
2097 "CreateCluster",
2098 json!({"ClusterName": "known", "NodeType": "db.r6g.large", "ACLName": "open-access"}),
2099 )
2100 .unwrap();
2101 let out = call(
2102 &s,
2103 "BatchUpdateCluster",
2104 json!({"ClusterNames": ["known", "ghost"]}),
2105 )
2106 .unwrap();
2107 assert_eq!(out["ProcessedClusters"].as_array().unwrap().len(), 1);
2108 let un = out["UnprocessedClusters"].as_array().unwrap();
2109 assert_eq!(un.len(), 1);
2110 assert_eq!(un[0]["ClusterName"], "ghost");
2111 }
2112
2113 #[test]
2114 fn snapshot_retention_limit_out_of_range_rejected() {
2115 let s = service();
2116 let err = call(
2117 &s,
2118 "CreateCluster",
2119 json!({"ClusterName": "srl", "NodeType": "db.r6g.large", "ACLName": "open-access", "SnapshotRetentionLimit": 4294967297i64}),
2120 )
2121 .unwrap_err();
2122 assert_eq!(err.status(), http::StatusCode::BAD_REQUEST);
2123 assert!(
2124 err.message().contains("SnapshotRetentionLimit"),
2125 "got: {}",
2126 err.message()
2127 );
2128 }
2129
2130 #[test]
2131 fn shard_slots_are_partitioned_across_shards() {
2132 let s = service();
2133 call(
2134 &s,
2135 "CreateCluster",
2136 json!({"ClusterName": "sh", "NodeType": "db.r6g.large", "ACLName": "open-access", "NumShards": 2}),
2137 )
2138 .unwrap();
2139 let desc = call(&s, "DescribeClusters", json!({"ClusterName": "sh"})).unwrap();
2140 let shards = desc["Clusters"][0]["Shards"].as_array().unwrap();
2141 assert_eq!(shards.len(), 2);
2142 assert_eq!(shards[0]["Slots"], "0-8191");
2143 assert_eq!(shards[1]["Slots"], "8192-16383");
2144 }
2145
2146 #[test]
2147 fn describe_clusters_show_shard_details_false_omits_shards() {
2148 let s = service();
2149 call(
2150 &s,
2151 "CreateCluster",
2152 json!({"ClusterName": "hide", "NodeType": "db.r6g.large", "ACLName": "open-access"}),
2153 )
2154 .unwrap();
2155 let with = call(&s, "DescribeClusters", json!({"ClusterName": "hide"})).unwrap();
2156 assert!(with["Clusters"][0].get("Shards").is_some());
2157 let without = call(
2158 &s,
2159 "DescribeClusters",
2160 json!({"ClusterName": "hide", "ShowShardDetails": false}),
2161 )
2162 .unwrap();
2163 assert!(without["Clusters"][0].get("Shards").is_none());
2164 }
2165
2166 #[test]
2167 fn describe_snapshots_honors_show_detail_and_source() {
2168 let s = service();
2169 call(
2170 &s,
2171 "CreateCluster",
2172 json!({"ClusterName": "snapc", "NodeType": "db.r6g.large", "ACLName": "open-access"}),
2173 )
2174 .unwrap();
2175 call(
2176 &s,
2177 "CreateSnapshot",
2178 json!({"ClusterName": "snapc", "SnapshotName": "s1"}),
2179 )
2180 .unwrap();
2181 let brief = call(
2183 &s,
2184 "DescribeSnapshots",
2185 json!({"SnapshotName": "s1", "ShowDetail": false}),
2186 )
2187 .unwrap();
2188 assert!(brief["Snapshots"][0].get("ClusterConfiguration").is_none());
2189 let sys = call(&s, "DescribeSnapshots", json!({"Source": "system"})).unwrap();
2191 assert!(sys["Snapshots"].as_array().unwrap().is_empty());
2192 }
2193
2194 #[test]
2195 fn default_parameter_groups_are_seeded() {
2196 let s = service();
2197 let groups = call(&s, "DescribeParameterGroups", json!({})).unwrap();
2198 let names: Vec<&str> = groups["ParameterGroups"]
2199 .as_array()
2200 .unwrap()
2201 .iter()
2202 .filter_map(|g| g["Name"].as_str())
2203 .collect();
2204 for expected in [
2205 "default.memorydb-redis7",
2206 "default.memorydb-redis6",
2207 "default.memorydb-valkey7",
2208 ] {
2209 assert!(
2210 names.contains(&expected),
2211 "missing {expected}; got {names:?}"
2212 );
2213 }
2214 let one = call(
2216 &s,
2217 "DescribeParameterGroups",
2218 json!({"ParameterGroupName": "default.memorydb-redis7"}),
2219 )
2220 .unwrap();
2221 assert_eq!(one["ParameterGroups"][0]["Family"], "memorydb_redis7");
2222 }
2223}