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