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 ]);
1706 ok(json!({ "EngineVersions": versions, "NextToken": Value::Null }))
1707 }
1708
1709 fn describe_events(&self, req: &AwsRequest) -> Result<AwsResponse, AwsServiceError> {
1710 let b = parse(req)?;
1711 if let Some(st) = opt_str(&b, "SourceType") {
1712 const VALID: &[&str] = &[
1713 "node",
1714 "parameter_group",
1715 "subnet_group",
1716 "cluster",
1717 "user",
1718 "acl",
1719 ];
1720 if !VALID.contains(&st.as_str()) {
1721 return Err(fault(
1722 "InvalidParameterValueException",
1723 &format!("Invalid SourceType: {st}"),
1724 ));
1725 }
1726 }
1727 ok(json!({ "Events": [], "NextToken": Value::Null }))
1728 }
1729
1730 fn describe_service_updates(&self, req: &AwsRequest) -> Result<AwsResponse, AwsServiceError> {
1731 let _ = parse(req)?;
1732 ok(json!({ "ServiceUpdates": [], "NextToken": Value::Null }))
1733 }
1734
1735 fn describe_reserved_nodes(&self, req: &AwsRequest) -> Result<AwsResponse, AwsServiceError> {
1736 let b = parse(req)?;
1737 let accounts = self.state.read();
1738 let st = accounts.get(&req.account_id);
1739 let Some(st) = st else {
1740 return ok(json!({ "ReservedNodes": [] }));
1741 };
1742 let items: Vec<Value> = st.reserved_nodes.values().map(reserved_node_json).collect();
1743 let (page, next) = paginate(items, &b, 100);
1744 ok(json!({ "ReservedNodes": page, "NextToken": next }))
1745 }
1746
1747 fn describe_reserved_nodes_offerings(
1748 &self,
1749 req: &AwsRequest,
1750 ) -> Result<AwsResponse, AwsServiceError> {
1751 let _ = parse(req)?;
1752 let offerings = json!([
1753 {
1754 "ReservedNodesOfferingId": "00000000-1111-2222-3333-444444444444",
1755 "NodeType": "db.r6g.large", "Duration": 31536000, "FixedPrice": 1200.0,
1756 "OfferingType": "All Upfront", "RecurringCharges": [],
1757 }
1758 ]);
1759 ok(json!({ "ReservedNodesOfferings": offerings, "NextToken": Value::Null }))
1760 }
1761
1762 fn purchase_reserved_nodes_offering(
1763 &self,
1764 req: &AwsRequest,
1765 ) -> Result<AwsResponse, AwsServiceError> {
1766 let b = parse(req)?;
1767 let offering_id = req_str(&b, "ReservedNodesOfferingId")?.to_string();
1768 let reservation_id = opt_str(&b, "ReservationId")
1769 .unwrap_or_else(|| format!("ri-{}", uuid::Uuid::new_v4().simple()));
1770 let count = int_field(&b, "NodeCount", 1, 1, i32::MAX as i64)?;
1771 let mut accounts = self.state.write();
1772 let st = accounts.get_or_create(&req.account_id);
1773 let node_arn = arn(
1774 "reservednode",
1775 &req.region,
1776 &req.account_id,
1777 &reservation_id,
1778 );
1779 let rn = ReservedNode {
1780 reservation_id: reservation_id.clone(),
1781 reserved_nodes_offering_id: offering_id,
1782 node_type: "db.r6g.large".to_string(),
1783 start_time: Utc::now(),
1784 duration: 31536000,
1785 fixed_price: 1200.0,
1786 node_count: count,
1787 offering_type: "All Upfront".to_string(),
1788 state: "active".to_string(),
1789 arn: node_arn,
1790 };
1791 st.reserved_nodes.insert(reservation_id, rn.clone());
1792 ok(json!({ "ReservedNode": reserved_node_json(&rn) }))
1793 }
1794
1795 fn list_allowed_node_type_updates(
1796 &self,
1797 req: &AwsRequest,
1798 ) -> Result<AwsResponse, AwsServiceError> {
1799 let b = parse(req)?;
1800 let name = req_str(&b, "ClusterName")?.to_string();
1801 let accounts = self.state.read();
1802 if accounts
1803 .get(&req.account_id)
1804 .and_then(|s| s.clusters.get(&name))
1805 .is_none()
1806 {
1807 return Err(fault(
1808 "ClusterNotFoundFault",
1809 &format!("Cluster {name} not found."),
1810 ));
1811 }
1812 ok(json!({
1813 "ScaleUpNodeTypes": ["db.r6g.xlarge", "db.r6g.2xlarge"],
1814 "ScaleDownNodeTypes": ["db.r6g.large"],
1815 }))
1816 }
1817}
1818
1819fn validate_user_name(name: &str) -> Result<(), AwsServiceError> {
1822 let valid = !name.is_empty()
1823 && name.starts_with(|c: char| c.is_ascii_alphabetic())
1824 && name.chars().all(|c| c.is_ascii_alphanumeric() || c == '-');
1825 if valid {
1826 Ok(())
1827 } else {
1828 Err(fault(
1829 "InvalidParameterValueException",
1830 &format!("Invalid UserName: {name}"),
1831 ))
1832 }
1833}
1834
1835fn arn_exists(st: &MemoryDbState, resource_arn: &str) -> bool {
1837 if resource_arn.is_empty() {
1840 return false;
1841 }
1842 st.clusters.values().any(|c| c.arn == resource_arn)
1843 || st.acls.values().any(|a| a.arn == resource_arn)
1844 || st.users.values().any(|u| u.arn == resource_arn)
1845 || st.parameter_groups.values().any(|p| p.arn == resource_arn)
1846 || st.subnet_groups.values().any(|g| g.arn == resource_arn)
1847 || st.snapshots.values().any(|s| s.arn == resource_arn)
1848 || st
1849 .multi_region_clusters
1850 .values()
1851 .any(|m| m.arn == resource_arn)
1852 || st.reserved_nodes.values().any(|r| r.arn == resource_arn)
1853}
1854
1855#[cfg(test)]
1856mod tests {
1857 use super::*;
1858 use fakecloud_core::multi_account::MultiAccountState;
1859
1860 fn service() -> MemoryDbService {
1861 let state = Arc::new(parking_lot::RwLock::new(MultiAccountState::new(
1862 "123456789012",
1863 "us-east-1",
1864 "http://localhost:4566",
1865 )));
1866 MemoryDbService::new(state)
1867 }
1868
1869 fn req(action: &str, body: Value) -> AwsRequest {
1870 AwsRequest {
1871 service: "memorydb".to_string(),
1872 action: action.to_string(),
1873 region: "us-east-1".to_string(),
1874 account_id: "123456789012".to_string(),
1875 request_id: "rid".to_string(),
1876 headers: http::HeaderMap::new(),
1877 query_params: std::collections::HashMap::new(),
1878 body: bytes::Bytes::from(serde_json::to_vec(&body).unwrap()),
1879 body_stream: parking_lot::Mutex::new(None),
1880 path_segments: vec![],
1881 raw_path: "/".to_string(),
1882 raw_query: String::new(),
1883 method: http::Method::POST,
1884 is_query_protocol: false,
1885 access_key_id: None,
1886 principal: None,
1887 }
1888 }
1889
1890 fn call(s: &MemoryDbService, action: &str, body: Value) -> Result<Value, AwsServiceError> {
1891 let resp = dispatch(s, &req(action, body))?;
1892 Ok(serde_json::from_slice(resp.body.expect_bytes()).unwrap())
1893 }
1894
1895 #[test]
1896 fn create_cluster_round_trips_and_describes() {
1897 let s = service();
1898 let out = call(
1899 &s,
1900 "CreateCluster",
1901 json!({"ClusterName": "app", "NodeType": "db.r6g.large", "ACLName": "open-access"}),
1902 )
1903 .unwrap();
1904 assert_eq!(out["Cluster"]["Name"], "app");
1905
1906 let desc = call(&s, "DescribeClusters", json!({"ClusterName": "app"})).unwrap();
1907 let clusters = desc["Clusters"].as_array().unwrap();
1908 assert_eq!(clusters.len(), 1);
1909 assert_eq!(clusters[0]["Status"], "available");
1911 }
1912
1913 #[test]
1914 fn update_cluster_applies_shard_and_replica_scaling() {
1915 let s = service();
1916 call(
1917 &s,
1918 "CreateCluster",
1919 json!({"ClusterName": "scale", "NodeType": "db.r6g.large", "ACLName": "open-access", "NumShards": 2, "NumReplicasPerShard": 1}),
1920 )
1921 .unwrap();
1922
1923 let upd = call(
1925 &s,
1926 "UpdateCluster",
1927 json!({
1928 "ClusterName": "scale",
1929 "ShardConfiguration": {"ShardCount": 4},
1930 "ReplicaConfiguration": {"ReplicaCount": 2}
1931 }),
1932 )
1933 .unwrap();
1934 assert_eq!(upd["Cluster"]["NumberOfShards"], 4);
1935
1936 let desc = call(&s, "DescribeClusters", json!({"ClusterName": "scale"})).unwrap();
1937 let c = &desc["Clusters"][0];
1938 assert_eq!(c["NumberOfShards"], 4);
1939 let shards = c["Shards"].as_array().unwrap();
1940 assert_eq!(shards.len(), 4, "shard vector rebuilt to new count");
1941 assert_eq!(shards[0]["NumberOfNodes"], 3, "2 replicas + 1 primary");
1942 assert_eq!(c["AvailabilityMode"], "MultiAZ");
1943
1944 call(
1946 &s,
1947 "UpdateCluster",
1948 json!({"ClusterName": "scale", "ReplicaConfiguration": {"ReplicaCount": 0}}),
1949 )
1950 .unwrap();
1951 let desc = call(&s, "DescribeClusters", json!({"ClusterName": "scale"})).unwrap();
1952 let c = &desc["Clusters"][0];
1953 assert_eq!(c["NumberOfShards"], 4, "shard count preserved");
1954 assert_eq!(c["Shards"][0]["NumberOfNodes"], 1);
1955 assert_eq!(c["AvailabilityMode"], "SingleAZ");
1956 }
1957
1958 #[test]
1959 fn create_cluster_rejects_out_of_range_shard_count() {
1960 let s = service();
1961 let err = call(
1963 &s,
1964 "CreateCluster",
1965 json!({"ClusterName": "big", "NodeType": "db.r6g.large", "ACLName": "open-access", "NumShards": 2_000_000_000i64}),
1966 )
1967 .unwrap_err();
1968 assert!(
1969 err.message().contains("NumShards"),
1970 "got: {}",
1971 err.message()
1972 );
1973 }
1974
1975 #[test]
1976 fn create_cluster_rejects_out_of_range_replica_count() {
1977 let s = service();
1978 let err = call(
1979 &s,
1980 "CreateCluster",
1981 json!({"ClusterName": "r", "NodeType": "db.r6g.large", "ACLName": "open-access", "NumReplicasPerShard": 99}),
1982 )
1983 .unwrap_err();
1984 assert!(
1985 err.message().contains("NumReplicasPerShard"),
1986 "got: {}",
1987 err.message()
1988 );
1989 }
1990
1991 #[test]
1992 fn create_user_rejects_invalid_name() {
1993 let s = service();
1994 let err = call(
1996 &s,
1997 "CreateUser",
1998 json!({"UserName": "1bad", "AccessString": "on ~* &* +@all", "AuthenticationMode": {"Type": "no-password-required"}}),
1999 )
2000 .unwrap_err();
2001 assert!(err.message().contains("UserName"), "got: {}", err.message());
2002 }
2003
2004 #[test]
2005 fn default_acl_and_user_are_seeded() {
2006 let s = service();
2007 let acls = call(&s, "DescribeACLs", json!({})).unwrap();
2008 assert!(acls["ACLs"]
2009 .as_array()
2010 .unwrap()
2011 .iter()
2012 .any(|a| a["Name"] == "open-access"));
2013 let users = call(&s, "DescribeUsers", json!({})).unwrap();
2014 assert!(users["Users"]
2015 .as_array()
2016 .unwrap()
2017 .iter()
2018 .any(|u| u["Name"] == "default"));
2019 }
2020
2021 #[test]
2022 fn seeded_default_acl_and_user_have_populated_arns() {
2023 let s = service();
2024 let acls = call(&s, "DescribeACLs", json!({"ACLName": "open-access"})).unwrap();
2025 assert_eq!(
2026 acls["ACLs"][0]["ARN"],
2027 "arn:aws:memorydb:us-east-1:123456789012:acl/open-access"
2028 );
2029 let users = call(&s, "DescribeUsers", json!({"UserName": "default"})).unwrap();
2030 assert_eq!(
2031 users["Users"][0]["ARN"],
2032 "arn:aws:memorydb:us-east-1:123456789012:user/default"
2033 );
2034 }
2035
2036 #[test]
2037 fn not_found_faults_return_http_404() {
2038 let s = service();
2039 let err = call(&s, "DescribeClusters", json!({"ClusterName": "nope"})).unwrap_err();
2040 assert_eq!(err.status(), http::StatusCode::NOT_FOUND);
2041 let bad = call(
2043 &s,
2044 "CreateCluster",
2045 json!({"ClusterName": "c", "NodeType": "db.r6g.large", "ACLName": "open-access", "NumShards": 9999}),
2046 )
2047 .unwrap_err();
2048 assert_eq!(bad.status(), http::StatusCode::BAD_REQUEST);
2049 }
2050
2051 #[test]
2052 fn create_cluster_populates_acl_clusters_backref() {
2053 let s = service();
2054 call(
2055 &s,
2056 "CreateCluster",
2057 json!({"ClusterName": "c1", "NodeType": "db.r6g.large", "ACLName": "open-access"}),
2058 )
2059 .unwrap();
2060 let acls = call(&s, "DescribeACLs", json!({"ACLName": "open-access"})).unwrap();
2061 let clusters = acls["ACLs"][0]["Clusters"].as_array().unwrap();
2062 assert!(clusters.iter().any(|c| c == "c1"), "got: {clusters:?}");
2063
2064 call(&s, "DeleteCluster", json!({"ClusterName": "c1"})).unwrap();
2066 let acls = call(&s, "DescribeACLs", json!({"ACLName": "open-access"})).unwrap();
2067 assert!(acls["ACLs"][0]["Clusters"].as_array().unwrap().is_empty());
2068 }
2069
2070 #[test]
2071 fn acl_membership_syncs_user_acl_names_backref() {
2072 let s = service();
2073 call(
2074 &s,
2075 "CreateUser",
2076 json!({"UserName": "bob", "AccessString": "on ~* &* +@all", "AuthenticationMode": {"Type": "iam"}}),
2077 )
2078 .unwrap();
2079 call(
2080 &s,
2081 "CreateACL",
2082 json!({"ACLName": "team", "UserNames": ["bob"]}),
2083 )
2084 .unwrap();
2085 let users = call(&s, "DescribeUsers", json!({"UserName": "bob"})).unwrap();
2086 let acl_names = users["Users"][0]["ACLNames"].as_array().unwrap();
2087 assert!(acl_names.iter().any(|a| a == "team"), "got: {acl_names:?}");
2088
2089 call(
2091 &s,
2092 "UpdateACL",
2093 json!({"ACLName": "team", "UserNamesToRemove": ["bob"]}),
2094 )
2095 .unwrap();
2096 let users = call(&s, "DescribeUsers", json!({"UserName": "bob"})).unwrap();
2097 assert!(users["Users"][0]["ACLNames"]
2098 .as_array()
2099 .unwrap()
2100 .iter()
2101 .all(|a| a != "team"));
2102 }
2103
2104 #[test]
2105 fn tag_resource_rejects_empty_arn() {
2106 let s = service();
2107 let err = call(
2108 &s,
2109 "TagResource",
2110 json!({"ResourceArn": "", "Tags": [{"Key": "k", "Value": "v"}]}),
2111 )
2112 .unwrap_err();
2113 assert_eq!(err.status(), http::StatusCode::BAD_REQUEST);
2114 assert!(
2115 err.message().contains("known resource") || err.message().contains("ARN"),
2116 "got: {}",
2117 err.message()
2118 );
2119 }
2120
2121 #[test]
2122 fn create_user_requires_valid_authentication_mode() {
2123 let s = service();
2124 let e1 = call(
2126 &s,
2127 "CreateUser",
2128 json!({"UserName": "u1", "AccessString": "on ~* &* +@all"}),
2129 )
2130 .unwrap_err();
2131 assert!(
2132 e1.message().contains("AuthenticationMode"),
2133 "got: {}",
2134 e1.message()
2135 );
2136 let e2 = call(
2138 &s,
2139 "CreateUser",
2140 json!({"UserName": "u2", "AccessString": "on ~* &* +@all", "AuthenticationMode": {"Type": "password"}}),
2141 )
2142 .unwrap_err();
2143 assert!(e2.message().contains("password"), "got: {}", e2.message());
2144 let e3 = call(
2146 &s,
2147 "CreateUser",
2148 json!({"UserName": "u3", "AccessString": "on ~* &* +@all", "AuthenticationMode": {"Type": "kerberos"}}),
2149 )
2150 .unwrap_err();
2151 assert!(
2152 e3.message().contains("kerberos") || e3.message().contains("Type"),
2153 "got: {}",
2154 e3.message()
2155 );
2156 call(
2158 &s,
2159 "CreateUser",
2160 json!({"UserName": "u4", "AccessString": "on ~* &* +@all", "AuthenticationMode": {"Type": "iam"}}),
2161 )
2162 .unwrap();
2163 }
2164
2165 #[test]
2166 fn failover_shard_validates_shard_name() {
2167 let s = service();
2168 call(
2169 &s,
2170 "CreateCluster",
2171 json!({"ClusterName": "fc", "NodeType": "db.r6g.large", "ACLName": "open-access", "NumShards": 1}),
2172 )
2173 .unwrap();
2174 let err = call(
2176 &s,
2177 "FailoverShard",
2178 json!({"ClusterName": "fc", "ShardName": "9999"}),
2179 )
2180 .unwrap_err();
2181 assert_eq!(err.status(), http::StatusCode::NOT_FOUND);
2182 call(
2184 &s,
2185 "FailoverShard",
2186 json!({"ClusterName": "fc", "ShardName": "0001"}),
2187 )
2188 .unwrap();
2189 }
2190
2191 #[test]
2192 fn batch_update_reports_unknown_clusters_as_unprocessed() {
2193 let s = service();
2194 call(
2195 &s,
2196 "CreateCluster",
2197 json!({"ClusterName": "known", "NodeType": "db.r6g.large", "ACLName": "open-access"}),
2198 )
2199 .unwrap();
2200 let out = call(
2201 &s,
2202 "BatchUpdateCluster",
2203 json!({"ClusterNames": ["known", "ghost"]}),
2204 )
2205 .unwrap();
2206 assert_eq!(out["ProcessedClusters"].as_array().unwrap().len(), 1);
2207 let un = out["UnprocessedClusters"].as_array().unwrap();
2208 assert_eq!(un.len(), 1);
2209 assert_eq!(un[0]["ClusterName"], "ghost");
2210 }
2211
2212 #[test]
2213 fn snapshot_retention_limit_out_of_range_rejected() {
2214 let s = service();
2215 let err = call(
2216 &s,
2217 "CreateCluster",
2218 json!({"ClusterName": "srl", "NodeType": "db.r6g.large", "ACLName": "open-access", "SnapshotRetentionLimit": 4294967297i64}),
2219 )
2220 .unwrap_err();
2221 assert_eq!(err.status(), http::StatusCode::BAD_REQUEST);
2222 assert!(
2223 err.message().contains("SnapshotRetentionLimit"),
2224 "got: {}",
2225 err.message()
2226 );
2227 }
2228
2229 #[test]
2230 fn shard_slots_are_partitioned_across_shards() {
2231 let s = service();
2232 call(
2233 &s,
2234 "CreateCluster",
2235 json!({"ClusterName": "sh", "NodeType": "db.r6g.large", "ACLName": "open-access", "NumShards": 2}),
2236 )
2237 .unwrap();
2238 let desc = call(&s, "DescribeClusters", json!({"ClusterName": "sh"})).unwrap();
2239 let shards = desc["Clusters"][0]["Shards"].as_array().unwrap();
2240 assert_eq!(shards.len(), 2);
2241 assert_eq!(shards[0]["Slots"], "0-8191");
2242 assert_eq!(shards[1]["Slots"], "8192-16383");
2243 }
2244
2245 #[test]
2246 fn describe_clusters_show_shard_details_false_omits_shards() {
2247 let s = service();
2248 call(
2249 &s,
2250 "CreateCluster",
2251 json!({"ClusterName": "hide", "NodeType": "db.r6g.large", "ACLName": "open-access"}),
2252 )
2253 .unwrap();
2254 let with = call(&s, "DescribeClusters", json!({"ClusterName": "hide"})).unwrap();
2255 assert!(with["Clusters"][0].get("Shards").is_some());
2256 let without = call(
2257 &s,
2258 "DescribeClusters",
2259 json!({"ClusterName": "hide", "ShowShardDetails": false}),
2260 )
2261 .unwrap();
2262 assert!(without["Clusters"][0].get("Shards").is_none());
2263 }
2264
2265 #[test]
2266 fn describe_snapshots_honors_show_detail_and_source() {
2267 let s = service();
2268 call(
2269 &s,
2270 "CreateCluster",
2271 json!({"ClusterName": "snapc", "NodeType": "db.r6g.large", "ACLName": "open-access"}),
2272 )
2273 .unwrap();
2274 call(
2275 &s,
2276 "CreateSnapshot",
2277 json!({"ClusterName": "snapc", "SnapshotName": "s1"}),
2278 )
2279 .unwrap();
2280 let brief = call(
2282 &s,
2283 "DescribeSnapshots",
2284 json!({"SnapshotName": "s1", "ShowDetail": false}),
2285 )
2286 .unwrap();
2287 assert!(brief["Snapshots"][0].get("ClusterConfiguration").is_none());
2288 let sys = call(&s, "DescribeSnapshots", json!({"Source": "system"})).unwrap();
2290 assert!(sys["Snapshots"].as_array().unwrap().is_empty());
2291 }
2292
2293 #[test]
2294 fn default_parameter_groups_are_seeded() {
2295 let s = service();
2296 let groups = call(&s, "DescribeParameterGroups", json!({})).unwrap();
2297 let names: Vec<&str> = groups["ParameterGroups"]
2298 .as_array()
2299 .unwrap()
2300 .iter()
2301 .filter_map(|g| g["Name"].as_str())
2302 .collect();
2303 for expected in [
2304 "default.memorydb-redis7",
2305 "default.memorydb-redis6",
2306 "default.memorydb-valkey7",
2307 ] {
2308 assert!(
2309 names.contains(&expected),
2310 "missing {expected}; got {names:?}"
2311 );
2312 }
2313 let one = call(
2315 &s,
2316 "DescribeParameterGroups",
2317 json!({"ParameterGroupName": "default.memorydb-redis7"}),
2318 )
2319 .unwrap();
2320 assert_eq!(one["ParameterGroups"][0]["Family"], "memorydb_redis7");
2321 }
2322}