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")
131 || action.starts_with("Delete")
132 || action.starts_with("Update")
133 || action.starts_with("Tag")
134 || action.starts_with("Untag")
135 || action.starts_with("Copy")
136 || action.starts_with("Reset")
137 || action.starts_with("Batch")
138 || action.starts_with("Failover")
139 || action.starts_with("Purchase")
140}
141
142fn dispatch(s: &MemoryDbService, req: &AwsRequest) -> Result<AwsResponse, AwsServiceError> {
143 match req.action.as_str() {
144 "CreateCluster" => s.create_cluster(req),
145 "DescribeClusters" => s.describe_clusters(req),
146 "UpdateCluster" => s.update_cluster(req),
147 "DeleteCluster" => s.delete_cluster(req),
148 "BatchUpdateCluster" => s.batch_update_cluster(req),
149 "FailoverShard" => s.failover_shard(req),
150 "CreateACL" => s.create_acl(req),
151 "DescribeACLs" => s.describe_acls(req),
152 "UpdateACL" => s.update_acl(req),
153 "DeleteACL" => s.delete_acl(req),
154 "CreateUser" => s.create_user(req),
155 "DescribeUsers" => s.describe_users(req),
156 "UpdateUser" => s.update_user(req),
157 "DeleteUser" => s.delete_user(req),
158 "CreateParameterGroup" => s.create_parameter_group(req),
159 "DescribeParameterGroups" => s.describe_parameter_groups(req),
160 "UpdateParameterGroup" => s.update_parameter_group(req),
161 "ResetParameterGroup" => s.reset_parameter_group(req),
162 "DeleteParameterGroup" => s.delete_parameter_group(req),
163 "DescribeParameters" => s.describe_parameters(req),
164 "CreateSubnetGroup" => s.create_subnet_group(req),
165 "DescribeSubnetGroups" => s.describe_subnet_groups(req),
166 "UpdateSubnetGroup" => s.update_subnet_group(req),
167 "DeleteSubnetGroup" => s.delete_subnet_group(req),
168 "CreateSnapshot" => s.create_snapshot(req),
169 "CopySnapshot" => s.copy_snapshot(req),
170 "DescribeSnapshots" => s.describe_snapshots(req),
171 "DeleteSnapshot" => s.delete_snapshot(req),
172 "CreateMultiRegionCluster" => s.create_multi_region_cluster(req),
173 "DescribeMultiRegionClusters" => s.describe_multi_region_clusters(req),
174 "UpdateMultiRegionCluster" => s.update_multi_region_cluster(req),
175 "DeleteMultiRegionCluster" => s.delete_multi_region_cluster(req),
176 "ListAllowedMultiRegionClusterUpdates" => s.list_allowed_mr_updates(req),
177 "DescribeMultiRegionParameterGroups" => s.describe_mr_parameter_groups(req),
178 "DescribeMultiRegionParameters" => s.describe_mr_parameters(req),
179 "TagResource" => s.tag_resource(req),
180 "UntagResource" => s.untag_resource(req),
181 "ListTags" => s.list_tags(req),
182 "DescribeEngineVersions" => s.describe_engine_versions(req),
183 "DescribeEvents" => s.describe_events(req),
184 "DescribeServiceUpdates" => s.describe_service_updates(req),
185 "DescribeReservedNodes" => s.describe_reserved_nodes(req),
186 "DescribeReservedNodesOfferings" => s.describe_reserved_nodes_offerings(req),
187 "PurchaseReservedNodesOffering" => s.purchase_reserved_nodes_offering(req),
188 "ListAllowedNodeTypeUpdates" => s.list_allowed_node_type_updates(req),
189 _ => Err(AwsServiceError::action_not_implemented(
190 s.service_name(),
191 &req.action,
192 )),
193 }
194}
195
196fn ok(v: Value) -> Result<AwsResponse, AwsServiceError> {
199 Ok(AwsResponse::json_value(StatusCode::OK, v))
200}
201
202fn parse(req: &AwsRequest) -> Result<Value, AwsServiceError> {
203 if req.body.is_empty() {
204 return Ok(json!({}));
205 }
206 serde_json::from_slice(&req.body).map_err(|e| {
207 fault(
208 "InvalidParameterValueException",
209 &format!("malformed body: {e}"),
210 )
211 })
212}
213
214fn fault(code: &str, msg: &str) -> AwsServiceError {
215 AwsServiceError::aws_error(StatusCode::BAD_REQUEST, code, msg)
216}
217
218fn req_str<'a>(b: &'a Value, f: &str) -> Result<&'a str, AwsServiceError> {
219 b.get(f).and_then(Value::as_str).ok_or_else(|| {
220 fault(
221 "InvalidParameterValueException",
222 &format!("{f} is required."),
223 )
224 })
225}
226
227fn opt_str(b: &Value, f: &str) -> Option<String> {
228 b.get(f).and_then(Value::as_str).map(str::to_string)
229}
230
231fn str_list(b: &Value, f: &str) -> Vec<String> {
232 b.get(f)
233 .and_then(Value::as_array)
234 .map(|a| {
235 a.iter()
236 .filter_map(|x| x.as_str().map(str::to_string))
237 .collect()
238 })
239 .unwrap_or_default()
240}
241
242fn arn(kind: &str, region: &str, account: &str, name: &str) -> String {
243 format!("arn:aws:memorydb:{region}:{account}:{kind}/{name}")
244}
245
246fn parse_tags(b: &Value) -> BTreeMap<String, String> {
247 let mut out = BTreeMap::new();
248 if let Some(arr) = b.get("Tags").and_then(Value::as_array) {
249 for t in arr {
250 if let (Some(k), Some(v)) = (
251 t.get("Key").and_then(Value::as_str),
252 t.get("Value").and_then(Value::as_str),
253 ) {
254 out.insert(k.to_string(), v.to_string());
255 }
256 }
257 }
258 out
259}
260
261fn tags_json(tags: &BTreeMap<String, String>) -> Vec<Value> {
262 tags.iter()
263 .map(|(k, v)| json!({ "Key": k, "Value": v }))
264 .collect()
265}
266
267fn page_start(b: &Value) -> Option<usize> {
270 match b.get("NextToken").and_then(Value::as_str) {
271 None | Some("") => Some(0),
272 Some(t) => t.parse::<usize>().ok(),
273 }
274}
275
276fn max_results(b: &Value, default: usize) -> usize {
277 b.get("MaxResults")
278 .and_then(Value::as_i64)
279 .map(|n| n.max(1) as usize)
280 .unwrap_or(default)
281}
282
283fn paginate(items: Vec<Value>, b: &Value, default_max: usize) -> (Vec<Value>, Option<String>) {
285 let Some(start) = page_start(b) else {
286 return (Vec::new(), None);
287 };
288 let max = max_results(b, default_max);
289 let total = items.len();
290 if start >= total {
291 return (Vec::new(), None);
292 }
293 let end = start.saturating_add(max).min(total);
294 let next = (end < total).then(|| end.to_string());
295 (items[start..end].to_vec(), next)
296}
297
298fn endpoint_json(e: &Endpoint) -> Value {
301 json!({ "Address": e.address, "Port": e.port })
302}
303
304fn node_json(n: &Node) -> Value {
305 json!({
306 "Name": n.name,
307 "Status": n.status,
308 "AvailabilityZone": n.availability_zone,
309 "CreateTime": n.create_time.timestamp(),
310 "Endpoint": endpoint_json(&n.endpoint),
311 })
312}
313
314fn shard_json(sh: &Shard) -> Value {
315 json!({
316 "Name": sh.name,
317 "Status": sh.status,
318 "Slots": sh.slots,
319 "Nodes": sh.nodes.iter().map(node_json).collect::<Vec<_>>(),
320 "NumberOfNodes": sh.number_of_nodes,
321 })
322}
323
324fn cluster_json(c: &Cluster) -> Value {
325 json!({
326 "Name": c.name,
327 "Description": c.description,
328 "Status": c.status,
329 "NumberOfShards": c.number_of_shards,
330 "Shards": c.shards.iter().map(shard_json).collect::<Vec<_>>(),
331 "AvailabilityMode": c.availability_mode,
332 "ClusterEndpoint": endpoint_json(&c.cluster_endpoint),
333 "NodeType": c.node_type,
334 "Engine": c.engine,
335 "EngineVersion": c.engine_version,
336 "EnginePatchVersion": c.engine_patch_version,
337 "ParameterGroupName": c.parameter_group_name,
338 "ParameterGroupStatus": c.parameter_group_status,
339 "SecurityGroups": c.security_group_ids.iter().map(|id| json!({ "SecurityGroupId": id, "Status": "active" })).collect::<Vec<_>>(),
340 "SubnetGroupName": c.subnet_group_name,
341 "TLSEnabled": c.tls_enabled,
342 "KmsKeyId": c.kms_key_id,
343 "ARN": c.arn,
344 "SnsTopicArn": c.sns_topic_arn,
345 "SnsTopicStatus": c.sns_topic_arn.as_ref().map(|_| "active"),
346 "SnapshotRetentionLimit": c.snapshot_retention_limit,
347 "MaintenanceWindow": c.maintenance_window,
348 "SnapshotWindow": c.snapshot_window,
349 "ACLName": c.acl_name,
350 "AutoMinorVersionUpgrade": c.auto_minor_version_upgrade,
351 "DataTiering": c.data_tiering,
352 "NetworkType": c.network_type,
353 "IpDiscovery": c.ip_discovery,
354 })
355}
356
357fn acl_json(a: &Acl) -> Value {
358 json!({
359 "Name": a.name,
360 "Status": a.status,
361 "UserNames": a.user_names,
362 "MinimumEngineVersion": a.minimum_engine_version,
363 "Clusters": a.clusters,
364 "ARN": a.arn,
365 })
366}
367
368fn user_json(u: &User) -> Value {
369 json!({
370 "Name": u.name,
371 "Status": u.status,
372 "AccessString": u.access_string,
373 "ACLNames": u.acl_names,
374 "MinimumEngineVersion": u.minimum_engine_version,
375 "Authentication": {
376 "Type": u.authentication.type_,
377 "PasswordCount": u.authentication.password_count,
378 },
379 "ARN": u.arn,
380 })
381}
382
383fn pg_json(p: &ParameterGroup) -> Value {
384 json!({
385 "Name": p.name,
386 "Family": p.family,
387 "Description": p.description,
388 "ARN": p.arn,
389 })
390}
391
392fn sg_json(g: &SubnetGroup) -> Value {
393 json!({
394 "Name": g.name,
395 "Description": g.description,
396 "VpcId": g.vpc_id,
397 "Subnets": g.subnet_ids.iter().map(|id| json!({
398 "Identifier": id,
399 "AvailabilityZone": { "Name": "us-east-1a" },
400 "SupportedNetworkTypes": ["ipv4"],
401 })).collect::<Vec<_>>(),
402 "ARN": g.arn,
403 "SupportedNetworkTypes": g.supported_network_types,
404 })
405}
406
407fn snapshot_json(s: &Snapshot) -> Value {
408 json!({
409 "Name": s.name,
410 "Status": s.status,
411 "Source": s.source,
412 "KmsKeyId": s.kms_key_id,
413 "ARN": s.arn,
414 "ClusterConfiguration": s.cluster_configuration,
415 "DataTiering": s.data_tiering,
416 })
417}
418
419fn mrc_json(m: &MultiRegionCluster) -> Value {
420 json!({
421 "MultiRegionClusterName": m.name,
422 "Description": m.description,
423 "Status": m.status,
424 "NodeType": m.node_type,
425 "Engine": m.engine,
426 "EngineVersion": m.engine_version,
427 "NumberOfShards": m.number_of_shards,
428 "Clusters": m.clusters,
429 "MultiRegionParameterGroupName": m.multi_region_parameter_group_name,
430 "TLSEnabled": m.tls_enabled,
431 "ARN": m.arn,
432 })
433}
434
435fn reserved_node_json(r: &ReservedNode) -> Value {
436 json!({
437 "ReservationId": r.reservation_id,
438 "ReservedNodesOfferingId": r.reserved_nodes_offering_id,
439 "NodeType": r.node_type,
440 "StartTime": r.start_time.timestamp(),
441 "Duration": r.duration,
442 "FixedPrice": r.fixed_price,
443 "NodeCount": r.node_count,
444 "OfferingType": r.offering_type,
445 "State": r.state,
446 "ARN": r.arn,
447 })
448}
449
450impl MemoryDbService {
453 fn create_cluster(&self, req: &AwsRequest) -> Result<AwsResponse, AwsServiceError> {
454 let b = parse(req)?;
455 let name = req_str(&b, "ClusterName")?.to_string();
456 let node_type = req_str(&b, "NodeType")?.to_string();
457 let acl_name = req_str(&b, "ACLName")?.to_string();
458 let mut accounts = self.state.write();
459 let st = accounts.get_or_create(&req.account_id);
460 if st.clusters.contains_key(&name) {
461 return Err(fault(
462 "ClusterAlreadyExistsFault",
463 &format!("Cluster {name} already exists."),
464 ));
465 }
466 let num_shards = b.get("NumShards").and_then(Value::as_i64).unwrap_or(1);
470 if !(1..=500).contains(&num_shards) {
471 return Err(fault(
472 "InvalidParameterValueException",
473 "NumShards must be between 1 and 500.",
474 ));
475 }
476 let num_shards = num_shards as i32;
477 let replicas = b
478 .get("NumReplicasPerShard")
479 .and_then(Value::as_i64)
480 .unwrap_or(1);
481 if !(0..=5).contains(&replicas) {
482 return Err(fault(
483 "InvalidParameterValueException",
484 "NumReplicasPerShard must be between 0 and 5.",
485 ));
486 }
487 let replicas = replicas as i32;
488 let port = b.get("Port").and_then(Value::as_i64).unwrap_or(6379) as i32;
489 let engine = opt_str(&b, "Engine").unwrap_or_else(|| DEFAULT_ENGINE.to_string());
490 let engine_version =
491 opt_str(&b, "EngineVersion").unwrap_or_else(|| DEFAULT_ENGINE_VERSION.to_string());
492 let cluster_arn = arn("cluster", &req.region, &req.account_id, &name);
493 let endpoint = Endpoint {
494 address: format!(
495 "clustercfg.{name}.abc123.memorydb.{}.amazonaws.com",
496 req.region
497 ),
498 port,
499 };
500 let shards = (0..num_shards)
501 .map(|i| {
502 let sname = format!("{:04}", i + 1);
503 let nodes = (0..=replicas)
504 .map(|j| Node {
505 name: format!("{name}-{sname}-{:03}", j + 1),
506 status: "creating".to_string(),
507 availability_zone: format!("{}a", req.region),
508 create_time: Utc::now(),
509 endpoint: Endpoint {
510 address: format!(
511 "{name}-{sname}-{:03}.{name}.abc123.memorydb.{}.amazonaws.com",
512 j + 1,
513 req.region
514 ),
515 port,
516 },
517 })
518 .collect::<Vec<_>>();
519 Shard {
520 name: sname,
521 status: "creating".to_string(),
522 slots: "0-16383".to_string(),
523 number_of_nodes: nodes.len() as i32,
524 nodes,
525 }
526 })
527 .collect();
528 let cluster = Cluster {
529 name: name.clone(),
530 description: opt_str(&b, "Description"),
531 status: "creating".to_string(),
532 number_of_shards: num_shards,
533 shards,
534 node_type,
535 engine,
536 engine_version,
537 engine_patch_version: DEFAULT_PATCH_VERSION.to_string(),
538 parameter_group_name: opt_str(&b, "ParameterGroupName")
539 .unwrap_or_else(|| "default.memorydb-redis7".to_string()),
540 parameter_group_status: "in-sync".to_string(),
541 security_group_ids: str_list(&b, "SecurityGroupIds"),
542 subnet_group_name: opt_str(&b, "SubnetGroupName")
543 .unwrap_or_else(|| "default".to_string()),
544 tls_enabled: b.get("TLSEnabled").and_then(Value::as_bool).unwrap_or(true),
545 kms_key_id: opt_str(&b, "KmsKeyId"),
546 arn: cluster_arn.clone(),
547 sns_topic_arn: opt_str(&b, "SnsTopicArn"),
548 snapshot_retention_limit: b
549 .get("SnapshotRetentionLimit")
550 .and_then(Value::as_i64)
551 .unwrap_or(0) as i32,
552 maintenance_window: opt_str(&b, "MaintenanceWindow")
553 .unwrap_or_else(|| "wed:08:00-wed:09:00".to_string()),
554 snapshot_window: opt_str(&b, "SnapshotWindow")
555 .unwrap_or_else(|| "05:00-06:00".to_string()),
556 acl_name,
557 auto_minor_version_upgrade: b
558 .get("AutoMinorVersionUpgrade")
559 .and_then(Value::as_bool)
560 .unwrap_or(true),
561 data_tiering: opt_str(&b, "DataTiering")
562 .map(|d| {
563 if d == "true" {
564 "true".to_string()
565 } else {
566 "false".to_string()
567 }
568 })
569 .unwrap_or_else(|| "false".to_string()),
570 availability_mode: if replicas > 0 {
571 "MultiAZ".to_string()
572 } else {
573 "SingleAZ".to_string()
574 },
575 cluster_endpoint: endpoint,
576 network_type: opt_str(&b, "NetworkType").unwrap_or_else(|| "ipv4".to_string()),
577 ip_discovery: opt_str(&b, "IpDiscovery").unwrap_or_else(|| "ipv4".to_string()),
578 port,
579 container_id: None,
580 created_at: Utc::now(),
581 };
582 let tags = parse_tags(&b);
583 if !tags.is_empty() {
584 st.tags.insert(cluster_arn, tags);
585 }
586 st.clusters.insert(name, cluster.clone());
587 ok(json!({ "Cluster": cluster_json(&cluster) }))
588 }
589
590 fn describe_clusters(&self, req: &AwsRequest) -> Result<AwsResponse, AwsServiceError> {
591 let b = parse(req)?;
592 let mut accounts = self.state.write();
593 let st = accounts.get_or_create(&req.account_id);
594 for c in st.clusters.values_mut() {
596 if c.status == "creating" {
597 c.status = "available".to_string();
598 for sh in &mut c.shards {
599 sh.status = "available".to_string();
600 for n in &mut sh.nodes {
601 n.status = "available".to_string();
602 }
603 }
604 }
605 }
606 if let Some(name) = opt_str(&b, "ClusterName") {
607 let Some(c) = st.clusters.get(&name) else {
608 return Err(fault(
609 "ClusterNotFoundFault",
610 &format!("Cluster {name} not found."),
611 ));
612 };
613 return ok(json!({ "Clusters": [cluster_json(c)] }));
614 }
615 let items: Vec<Value> = st.clusters.values().map(cluster_json).collect();
616 let (page, next) = paginate(items, &b, 100);
617 ok(json!({ "Clusters": page, "NextToken": next }))
618 }
619
620 fn update_cluster(&self, req: &AwsRequest) -> Result<AwsResponse, AwsServiceError> {
621 let b = parse(req)?;
622 let name = req_str(&b, "ClusterName")?.to_string();
623 let mut accounts = self.state.write();
624 let st = accounts.get_or_create(&req.account_id);
625 let Some(c) = st.clusters.get_mut(&name) else {
626 return Err(fault(
627 "ClusterNotFoundFault",
628 &format!("Cluster {name} not found."),
629 ));
630 };
631 if let Some(d) = opt_str(&b, "Description") {
632 c.description = Some(d);
633 }
634 if let Some(v) = opt_str(&b, "EngineVersion") {
635 c.engine_version = v;
636 }
637 if let Some(v) = opt_str(&b, "Engine") {
638 c.engine = v;
639 }
640 if let Some(v) = opt_str(&b, "NodeType") {
641 c.node_type = v;
642 }
643 if let Some(v) = opt_str(&b, "ParameterGroupName") {
644 c.parameter_group_name = v;
645 }
646 if let Some(v) = opt_str(&b, "ACLName") {
647 c.acl_name = v;
648 }
649 if let Some(v) = opt_str(&b, "MaintenanceWindow") {
650 c.maintenance_window = v;
651 }
652 if let Some(v) = opt_str(&b, "SnapshotWindow") {
653 c.snapshot_window = v;
654 }
655 if let Some(v) = b.get("SnapshotRetentionLimit").and_then(Value::as_i64) {
656 c.snapshot_retention_limit = v as i32;
657 }
658 if let Some(sg) = b.get("SecurityGroupIds").and_then(Value::as_array) {
659 c.security_group_ids = sg
660 .iter()
661 .filter_map(|x| x.as_str().map(str::to_string))
662 .collect();
663 }
664 let out = cluster_json(c);
665 ok(json!({ "Cluster": out }))
666 }
667
668 fn delete_cluster(&self, req: &AwsRequest) -> Result<AwsResponse, AwsServiceError> {
669 let b = parse(req)?;
670 let name = req_str(&b, "ClusterName")?.to_string();
671 let mut accounts = self.state.write();
672 let st = accounts.get_or_create(&req.account_id);
673 let Some(mut c) = st.clusters.remove(&name) else {
674 return Err(fault(
675 "ClusterNotFoundFault",
676 &format!("Cluster {name} not found."),
677 ));
678 };
679 c.status = "deleting".to_string();
680 st.tags.remove(&c.arn);
681 ok(json!({ "Cluster": cluster_json(&c) }))
682 }
683
684 fn batch_update_cluster(&self, req: &AwsRequest) -> Result<AwsResponse, AwsServiceError> {
685 let b = parse(req)?;
686 let names = str_list(&b, "ClusterNames");
687 let mut accounts = self.state.write();
688 let st = accounts.get_or_create(&req.account_id);
689 let mut processed = Vec::new();
690 for n in &names {
691 if let Some(c) = st.clusters.get(n) {
692 processed.push(cluster_json(c));
693 }
694 }
695 ok(json!({ "ProcessedClusters": processed, "UnprocessedClusters": [] }))
696 }
697
698 fn failover_shard(&self, req: &AwsRequest) -> Result<AwsResponse, AwsServiceError> {
699 let b = parse(req)?;
700 let name = req_str(&b, "ClusterName")?.to_string();
701 let accounts = self.state.read();
702 let st = accounts.get(&req.account_id);
703 let Some(c) = st.and_then(|s| s.clusters.get(&name)) else {
704 return Err(fault(
705 "ClusterNotFoundFault",
706 &format!("Cluster {name} not found."),
707 ));
708 };
709 ok(json!({ "Cluster": cluster_json(c) }))
710 }
711
712 fn create_acl(&self, req: &AwsRequest) -> Result<AwsResponse, AwsServiceError> {
713 let b = parse(req)?;
714 let name = req_str(&b, "ACLName")?.to_string();
715 let mut accounts = self.state.write();
716 let st = accounts.get_or_create(&req.account_id);
717 if st.acls.contains_key(&name) {
718 return Err(fault(
719 "ACLAlreadyExistsFault",
720 &format!("ACL {name} already exists."),
721 ));
722 }
723 let acl_arn = arn("acl", &req.region, &req.account_id, &name);
724 let acl = Acl {
725 name: name.clone(),
726 status: "active".to_string(),
727 user_names: str_list(&b, "UserNames"),
728 minimum_engine_version: "6.2".to_string(),
729 clusters: vec![],
730 arn: acl_arn.clone(),
731 };
732 let tags = parse_tags(&b);
733 if !tags.is_empty() {
734 st.tags.insert(acl_arn, tags);
735 }
736 st.acls.insert(name, acl.clone());
737 ok(json!({ "ACL": acl_json(&acl) }))
738 }
739
740 fn describe_acls(&self, req: &AwsRequest) -> Result<AwsResponse, AwsServiceError> {
741 let b = parse(req)?;
742 let accounts = self.state.read();
743 let st = accounts.get(&req.account_id);
744 let Some(st) = st else {
745 return ok(json!({ "ACLs": [] }));
746 };
747 if let Some(name) = opt_str(&b, "ACLName") {
748 let Some(a) = st.acls.get(&name) else {
749 return Err(fault("ACLNotFoundFault", &format!("ACL {name} not found.")));
750 };
751 return ok(json!({ "ACLs": [acl_json(a)] }));
752 }
753 let items: Vec<Value> = st.acls.values().map(acl_json).collect();
754 let (page, next) = paginate(items, &b, 100);
755 ok(json!({ "ACLs": page, "NextToken": next }))
756 }
757
758 fn update_acl(&self, req: &AwsRequest) -> Result<AwsResponse, AwsServiceError> {
759 let b = parse(req)?;
760 let name = req_str(&b, "ACLName")?.to_string();
761 let mut accounts = self.state.write();
762 let st = accounts.get_or_create(&req.account_id);
763 let Some(a) = st.acls.get_mut(&name) else {
764 return Err(fault("ACLNotFoundFault", &format!("ACL {name} not found.")));
765 };
766 for u in str_list(&b, "UserNamesToAdd") {
767 if !a.user_names.contains(&u) {
768 a.user_names.push(u);
769 }
770 }
771 let remove = str_list(&b, "UserNamesToRemove");
772 a.user_names.retain(|u| !remove.contains(u));
773 let out = acl_json(a);
774 ok(json!({ "ACL": out }))
775 }
776
777 fn delete_acl(&self, req: &AwsRequest) -> Result<AwsResponse, AwsServiceError> {
778 let b = parse(req)?;
779 let name = req_str(&b, "ACLName")?.to_string();
780 let mut accounts = self.state.write();
781 let st = accounts.get_or_create(&req.account_id);
782 let Some(mut a) = st.acls.remove(&name) else {
783 return Err(fault("ACLNotFoundFault", &format!("ACL {name} not found.")));
784 };
785 a.status = "deleting".to_string();
786 st.tags.remove(&a.arn);
787 ok(json!({ "ACL": acl_json(&a) }))
788 }
789
790 fn create_user(&self, req: &AwsRequest) -> Result<AwsResponse, AwsServiceError> {
791 let b = parse(req)?;
792 let name = req_str(&b, "UserName")?.to_string();
793 validate_user_name(&name)?;
794 let access_string = req_str(&b, "AccessString")?.to_string();
795 let auth = b.get("AuthenticationMode").cloned().unwrap_or(json!({}));
796 let auth_type = auth
797 .get("Type")
798 .and_then(Value::as_str)
799 .unwrap_or("password")
800 .to_string();
801 let pw_count = auth
802 .get("Passwords")
803 .and_then(Value::as_array)
804 .map(|a| a.len() as i32)
805 .unwrap_or(0);
806 let mut accounts = self.state.write();
807 let st = accounts.get_or_create(&req.account_id);
808 if st.users.contains_key(&name) {
809 return Err(fault(
810 "UserAlreadyExistsFault",
811 &format!("User {name} already exists."),
812 ));
813 }
814 let user_arn = arn("user", &req.region, &req.account_id, &name);
815 let user = User {
816 name: name.clone(),
817 status: "active".to_string(),
818 access_string,
819 acl_names: vec![],
820 minimum_engine_version: "6.2".to_string(),
821 authentication: UserAuthentication {
822 type_: if auth_type == "iam" {
823 "iam".to_string()
824 } else {
825 auth_type
826 },
827 password_count: pw_count,
828 },
829 arn: user_arn.clone(),
830 };
831 let tags = parse_tags(&b);
832 if !tags.is_empty() {
833 st.tags.insert(user_arn, tags);
834 }
835 st.users.insert(name, user.clone());
836 ok(json!({ "User": user_json(&user) }))
837 }
838
839 fn describe_users(&self, req: &AwsRequest) -> Result<AwsResponse, AwsServiceError> {
840 let b = parse(req)?;
841 let accounts = self.state.read();
842 let st = accounts.get(&req.account_id);
843 let Some(st) = st else {
844 return ok(json!({ "Users": [] }));
845 };
846 if let Some(name) = opt_str(&b, "UserName") {
847 let Some(u) = st.users.get(&name) else {
848 return Err(fault(
849 "UserNotFoundFault",
850 &format!("User {name} not found."),
851 ));
852 };
853 return ok(json!({ "Users": [user_json(u)] }));
854 }
855 let items: Vec<Value> = st.users.values().map(user_json).collect();
856 let (page, next) = paginate(items, &b, 100);
857 ok(json!({ "Users": page, "NextToken": next }))
858 }
859
860 fn update_user(&self, req: &AwsRequest) -> Result<AwsResponse, AwsServiceError> {
861 let b = parse(req)?;
862 let name = req_str(&b, "UserName")?.to_string();
863 let mut accounts = self.state.write();
864 let st = accounts.get_or_create(&req.account_id);
865 let Some(u) = st.users.get_mut(&name) else {
866 return Err(fault(
867 "UserNotFoundFault",
868 &format!("User {name} not found."),
869 ));
870 };
871 if let Some(a) = opt_str(&b, "AccessString") {
872 u.access_string = a;
873 }
874 if let Some(auth) = b.get("AuthenticationMode") {
875 if let Some(t) = auth.get("Type").and_then(Value::as_str) {
876 u.authentication.type_ = t.to_string();
877 }
878 if let Some(p) = auth.get("Passwords").and_then(Value::as_array) {
879 u.authentication.password_count = p.len() as i32;
880 }
881 }
882 let out = user_json(u);
883 ok(json!({ "User": out }))
884 }
885
886 fn delete_user(&self, req: &AwsRequest) -> Result<AwsResponse, AwsServiceError> {
887 let b = parse(req)?;
888 let name = req_str(&b, "UserName")?.to_string();
889 validate_user_name(&name)?;
890 let mut accounts = self.state.write();
891 let st = accounts.get_or_create(&req.account_id);
892 let Some(mut u) = st.users.remove(&name) else {
893 return Err(fault(
894 "UserNotFoundFault",
895 &format!("User {name} not found."),
896 ));
897 };
898 u.status = "deleting".to_string();
899 st.tags.remove(&u.arn);
900 ok(json!({ "User": user_json(&u) }))
901 }
902
903 fn create_parameter_group(&self, req: &AwsRequest) -> Result<AwsResponse, AwsServiceError> {
904 let b = parse(req)?;
905 let name = req_str(&b, "ParameterGroupName")?.to_string();
906 let family = req_str(&b, "Family")?.to_string();
907 let mut accounts = self.state.write();
908 let st = accounts.get_or_create(&req.account_id);
909 if st.parameter_groups.contains_key(&name) {
910 return Err(fault(
911 "ParameterGroupAlreadyExistsFault",
912 &format!("Parameter group {name} already exists."),
913 ));
914 }
915 let pg_arn = arn("parametergroup", &req.region, &req.account_id, &name);
916 let pg = ParameterGroup {
917 name: name.clone(),
918 family,
919 description: opt_str(&b, "Description").unwrap_or_default(),
920 arn: pg_arn.clone(),
921 parameters: BTreeMap::new(),
922 };
923 let tags = parse_tags(&b);
924 if !tags.is_empty() {
925 st.tags.insert(pg_arn, tags);
926 }
927 st.parameter_groups.insert(name, pg.clone());
928 ok(json!({ "ParameterGroup": pg_json(&pg) }))
929 }
930
931 fn describe_parameter_groups(&self, req: &AwsRequest) -> Result<AwsResponse, AwsServiceError> {
932 let b = parse(req)?;
933 let accounts = self.state.read();
934 let st = accounts.get(&req.account_id);
935 let Some(st) = st else {
936 return ok(json!({ "ParameterGroups": [] }));
937 };
938 if let Some(name) = opt_str(&b, "ParameterGroupName") {
939 let Some(p) = st.parameter_groups.get(&name) else {
940 return Err(fault(
941 "ParameterGroupNotFoundFault",
942 &format!("Parameter group {name} not found."),
943 ));
944 };
945 return ok(json!({ "ParameterGroups": [pg_json(p)] }));
946 }
947 let items: Vec<Value> = st.parameter_groups.values().map(pg_json).collect();
948 let (page, next) = paginate(items, &b, 100);
949 ok(json!({ "ParameterGroups": page, "NextToken": next }))
950 }
951
952 fn update_parameter_group(&self, req: &AwsRequest) -> Result<AwsResponse, AwsServiceError> {
953 let b = parse(req)?;
954 let name = req_str(&b, "ParameterGroupName")?.to_string();
955 let mut accounts = self.state.write();
956 let st = accounts.get_or_create(&req.account_id);
957 let Some(p) = st.parameter_groups.get_mut(&name) else {
958 return Err(fault(
959 "ParameterGroupNotFoundFault",
960 &format!("Parameter group {name} not found."),
961 ));
962 };
963 if let Some(arr) = b.get("ParameterNameValues").and_then(Value::as_array) {
964 for pv in arr {
965 if let (Some(pn), Some(pval)) = (
966 pv.get("ParameterName").and_then(Value::as_str),
967 pv.get("ParameterValue").and_then(Value::as_str),
968 ) {
969 p.parameters.insert(pn.to_string(), pval.to_string());
970 }
971 }
972 }
973 let out = pg_json(p);
974 ok(json!({ "ParameterGroup": out }))
975 }
976
977 fn reset_parameter_group(&self, req: &AwsRequest) -> Result<AwsResponse, AwsServiceError> {
978 let b = parse(req)?;
979 let name = req_str(&b, "ParameterGroupName")?.to_string();
980 let mut accounts = self.state.write();
981 let st = accounts.get_or_create(&req.account_id);
982 let Some(p) = st.parameter_groups.get_mut(&name) else {
983 return Err(fault(
984 "ParameterGroupNotFoundFault",
985 &format!("Parameter group {name} not found."),
986 ));
987 };
988 let all = b
989 .get("AllParameters")
990 .and_then(Value::as_bool)
991 .unwrap_or(false);
992 if all {
993 p.parameters.clear();
994 } else {
995 for pv in b
996 .get("ParameterNames")
997 .and_then(Value::as_array)
998 .into_iter()
999 .flatten()
1000 {
1001 if let Some(pn) = pv.as_str() {
1002 p.parameters.remove(pn);
1003 }
1004 }
1005 }
1006 let out = pg_json(p);
1007 ok(json!({ "ParameterGroup": out }))
1008 }
1009
1010 fn delete_parameter_group(&self, req: &AwsRequest) -> Result<AwsResponse, AwsServiceError> {
1011 let b = parse(req)?;
1012 let name = req_str(&b, "ParameterGroupName")?.to_string();
1013 let mut accounts = self.state.write();
1014 let st = accounts.get_or_create(&req.account_id);
1015 let Some(p) = st.parameter_groups.remove(&name) else {
1016 return Err(fault(
1017 "ParameterGroupNotFoundFault",
1018 &format!("Parameter group {name} not found."),
1019 ));
1020 };
1021 st.tags.remove(&p.arn);
1022 ok(json!({ "ParameterGroup": pg_json(&p) }))
1023 }
1024
1025 fn describe_parameters(&self, req: &AwsRequest) -> Result<AwsResponse, AwsServiceError> {
1026 let b = parse(req)?;
1027 let name = req_str(&b, "ParameterGroupName")?.to_string();
1028 let accounts = self.state.read();
1029 let st = accounts.get(&req.account_id);
1030 let Some(p) = st.and_then(|s| s.parameter_groups.get(&name)) else {
1031 return Err(fault(
1032 "ParameterGroupNotFoundFault",
1033 &format!("Parameter group {name} not found."),
1034 ));
1035 };
1036 let params: Vec<Value> = p
1037 .parameters
1038 .iter()
1039 .map(|(k, v)| {
1040 json!({
1041 "Name": k, "Value": v, "Description": "", "DataType": "string",
1042 "AllowedValues": "", "MinimumEngineVersion": "6.2",
1043 })
1044 })
1045 .collect();
1046 ok(json!({ "Parameters": params, "NextToken": Value::Null }))
1047 }
1048
1049 fn create_subnet_group(&self, req: &AwsRequest) -> Result<AwsResponse, AwsServiceError> {
1050 let b = parse(req)?;
1051 let name = req_str(&b, "SubnetGroupName")?.to_string();
1052 let subnet_ids = str_list(&b, "SubnetIds");
1053 let mut accounts = self.state.write();
1054 let st = accounts.get_or_create(&req.account_id);
1055 if st.subnet_groups.contains_key(&name) {
1056 return Err(fault(
1057 "SubnetGroupAlreadyExistsFault",
1058 &format!("Subnet group {name} already exists."),
1059 ));
1060 }
1061 let sg_arn = arn("subnetgroup", &req.region, &req.account_id, &name);
1062 let sg = SubnetGroup {
1063 name: name.clone(),
1064 description: opt_str(&b, "Description").unwrap_or_default(),
1065 vpc_id: "vpc-00000000".to_string(),
1066 subnet_ids,
1067 arn: sg_arn.clone(),
1068 supported_network_types: vec!["ipv4".to_string()],
1069 };
1070 let tags = parse_tags(&b);
1071 if !tags.is_empty() {
1072 st.tags.insert(sg_arn, tags);
1073 }
1074 st.subnet_groups.insert(name, sg.clone());
1075 ok(json!({ "SubnetGroup": sg_json(&sg) }))
1076 }
1077
1078 fn describe_subnet_groups(&self, req: &AwsRequest) -> Result<AwsResponse, AwsServiceError> {
1079 let b = parse(req)?;
1080 let accounts = self.state.read();
1081 let st = accounts.get(&req.account_id);
1082 let Some(st) = st else {
1083 return ok(json!({ "SubnetGroups": [] }));
1084 };
1085 if let Some(name) = opt_str(&b, "SubnetGroupName") {
1086 let Some(g) = st.subnet_groups.get(&name) else {
1087 return Err(fault(
1088 "SubnetGroupNotFoundFault",
1089 &format!("Subnet group {name} not found."),
1090 ));
1091 };
1092 return ok(json!({ "SubnetGroups": [sg_json(g)] }));
1093 }
1094 let items: Vec<Value> = st.subnet_groups.values().map(sg_json).collect();
1095 let (page, next) = paginate(items, &b, 100);
1096 ok(json!({ "SubnetGroups": page, "NextToken": next }))
1097 }
1098
1099 fn update_subnet_group(&self, req: &AwsRequest) -> Result<AwsResponse, AwsServiceError> {
1100 let b = parse(req)?;
1101 let name = req_str(&b, "SubnetGroupName")?.to_string();
1102 let mut accounts = self.state.write();
1103 let st = accounts.get_or_create(&req.account_id);
1104 let Some(g) = st.subnet_groups.get_mut(&name) else {
1105 return Err(fault(
1106 "SubnetGroupNotFoundFault",
1107 &format!("Subnet group {name} not found."),
1108 ));
1109 };
1110 if let Some(d) = opt_str(&b, "Description") {
1111 g.description = d;
1112 }
1113 let ids = str_list(&b, "SubnetIds");
1114 if !ids.is_empty() {
1115 g.subnet_ids = ids;
1116 }
1117 let out = sg_json(g);
1118 ok(json!({ "SubnetGroup": out }))
1119 }
1120
1121 fn delete_subnet_group(&self, req: &AwsRequest) -> Result<AwsResponse, AwsServiceError> {
1122 let b = parse(req)?;
1123 let name = req_str(&b, "SubnetGroupName")?.to_string();
1124 let mut accounts = self.state.write();
1125 let st = accounts.get_or_create(&req.account_id);
1126 let Some(g) = st.subnet_groups.remove(&name) else {
1127 return Err(fault(
1128 "SubnetGroupNotFoundFault",
1129 &format!("Subnet group {name} not found."),
1130 ));
1131 };
1132 st.tags.remove(&g.arn);
1133 ok(json!({ "SubnetGroup": sg_json(&g) }))
1134 }
1135
1136 fn create_snapshot(&self, req: &AwsRequest) -> Result<AwsResponse, AwsServiceError> {
1137 let b = parse(req)?;
1138 let cluster_name = req_str(&b, "ClusterName")?.to_string();
1139 let snap_name = req_str(&b, "SnapshotName")?.to_string();
1140 let mut accounts = self.state.write();
1141 let st = accounts.get_or_create(&req.account_id);
1142 if st.snapshots.contains_key(&snap_name) {
1143 return Err(fault(
1144 "SnapshotAlreadyExistsFault",
1145 &format!("Snapshot {snap_name} already exists."),
1146 ));
1147 }
1148 let Some(c) = st.clusters.get(&cluster_name) else {
1149 return Err(fault(
1150 "ClusterNotFoundFault",
1151 &format!("Cluster {cluster_name} not found."),
1152 ));
1153 };
1154 let cfg = json!({
1155 "Name": c.name, "Description": c.description, "NodeType": c.node_type,
1156 "Engine": c.engine, "EngineVersion": c.engine_version, "MaintenanceWindow": c.maintenance_window,
1157 "TopicArn": c.sns_topic_arn, "Port": c.port, "ParameterGroupName": c.parameter_group_name,
1158 "SubnetGroupName": c.subnet_group_name, "VpcId": "vpc-00000000",
1159 "SnapshotRetentionLimit": c.snapshot_retention_limit, "SnapshotWindow": c.snapshot_window,
1160 "NumShards": c.number_of_shards,
1161 });
1162 let snap_arn = arn("snapshot", &req.region, &req.account_id, &snap_name);
1163 let snap = Snapshot {
1164 name: snap_name.clone(),
1165 status: "available".to_string(),
1166 source: "manual".to_string(),
1167 kms_key_id: opt_str(&b, "KmsKeyId"),
1168 arn: snap_arn.clone(),
1169 data_tiering: c.data_tiering.clone(),
1170 cluster_configuration: cfg,
1171 };
1172 let tags = parse_tags(&b);
1173 if !tags.is_empty() {
1174 st.tags.insert(snap_arn, tags);
1175 }
1176 st.snapshots.insert(snap_name, snap.clone());
1177 ok(json!({ "Snapshot": snapshot_json(&snap) }))
1178 }
1179
1180 fn copy_snapshot(&self, req: &AwsRequest) -> Result<AwsResponse, AwsServiceError> {
1181 let b = parse(req)?;
1182 let source = req_str(&b, "SourceSnapshotName")?.to_string();
1183 let target = req_str(&b, "TargetSnapshotName")?.to_string();
1184 let mut accounts = self.state.write();
1185 let st = accounts.get_or_create(&req.account_id);
1186 let Some(src) = st.snapshots.get(&source).cloned() else {
1187 return Err(fault(
1188 "SnapshotNotFoundFault",
1189 &format!("Snapshot {source} not found."),
1190 ));
1191 };
1192 if st.snapshots.contains_key(&target) {
1193 return Err(fault(
1194 "SnapshotAlreadyExistsFault",
1195 &format!("Snapshot {target} already exists."),
1196 ));
1197 }
1198 let snap_arn = arn("snapshot", &req.region, &req.account_id, &target);
1199 let snap = Snapshot {
1200 name: target.clone(),
1201 status: "available".to_string(),
1202 source: "manual".to_string(),
1203 kms_key_id: opt_str(&b, "KmsKeyId").or(src.kms_key_id),
1204 arn: snap_arn,
1205 data_tiering: src.data_tiering,
1206 cluster_configuration: src.cluster_configuration,
1207 };
1208 st.snapshots.insert(target, snap.clone());
1209 ok(json!({ "Snapshot": snapshot_json(&snap) }))
1210 }
1211
1212 fn describe_snapshots(&self, req: &AwsRequest) -> Result<AwsResponse, AwsServiceError> {
1213 let b = parse(req)?;
1214 let accounts = self.state.read();
1215 let st = accounts.get(&req.account_id);
1216 let Some(st) = st else {
1217 return ok(json!({ "Snapshots": [] }));
1218 };
1219 if let Some(name) = opt_str(&b, "SnapshotName") {
1220 let Some(s) = st.snapshots.get(&name) else {
1221 return Err(fault(
1222 "SnapshotNotFoundFault",
1223 &format!("Snapshot {name} not found."),
1224 ));
1225 };
1226 return ok(json!({ "Snapshots": [snapshot_json(s)] }));
1227 }
1228 let cluster_filter = opt_str(&b, "ClusterName");
1229 let items: Vec<Value> = st
1230 .snapshots
1231 .values()
1232 .filter(|s| {
1233 cluster_filter.as_ref().is_none_or(|cn| {
1234 s.cluster_configuration.get("Name").and_then(Value::as_str) == Some(cn)
1235 })
1236 })
1237 .map(snapshot_json)
1238 .collect();
1239 let (page, next) = paginate(items, &b, 50);
1240 ok(json!({ "Snapshots": page, "NextToken": next }))
1241 }
1242
1243 fn delete_snapshot(&self, req: &AwsRequest) -> Result<AwsResponse, AwsServiceError> {
1244 let b = parse(req)?;
1245 let name = req_str(&b, "SnapshotName")?.to_string();
1246 let mut accounts = self.state.write();
1247 let st = accounts.get_or_create(&req.account_id);
1248 let Some(mut s) = st.snapshots.remove(&name) else {
1249 return Err(fault(
1250 "SnapshotNotFoundFault",
1251 &format!("Snapshot {name} not found."),
1252 ));
1253 };
1254 s.status = "deleting".to_string();
1255 st.tags.remove(&s.arn);
1256 ok(json!({ "Snapshot": snapshot_json(&s) }))
1257 }
1258
1259 fn create_multi_region_cluster(
1260 &self,
1261 req: &AwsRequest,
1262 ) -> Result<AwsResponse, AwsServiceError> {
1263 let b = parse(req)?;
1264 let suffix = req_str(&b, "MultiRegionClusterNameSuffix")?.to_string();
1265 let node_type = req_str(&b, "NodeType")?.to_string();
1266 let name = format!("virxk-{suffix}");
1267 let mut accounts = self.state.write();
1268 let st = accounts.get_or_create(&req.account_id);
1269 if st.multi_region_clusters.contains_key(&name) {
1270 return Err(fault(
1271 "MultiRegionClusterAlreadyExistsFault",
1272 &format!("Multi-region cluster {name} already exists."),
1273 ));
1274 }
1275 let mrc_arn = arn("multiregioncluster", &req.region, &req.account_id, &name);
1276 let mrc = MultiRegionCluster {
1277 name: name.clone(),
1278 description: opt_str(&b, "Description"),
1279 status: "creating".to_string(),
1280 node_type,
1281 engine: opt_str(&b, "Engine").unwrap_or_else(|| DEFAULT_ENGINE.to_string()),
1282 engine_version: opt_str(&b, "EngineVersion")
1283 .unwrap_or_else(|| DEFAULT_ENGINE_VERSION.to_string()),
1284 number_of_shards: b.get("NumShards").and_then(Value::as_i64).unwrap_or(1) as i32,
1285 multi_region_parameter_group_name: opt_str(&b, "MultiRegionParameterGroupName"),
1286 tls_enabled: b.get("TLSEnabled").and_then(Value::as_bool).unwrap_or(true),
1287 arn: mrc_arn.clone(),
1288 clusters: json!([]),
1289 };
1290 let tags = parse_tags(&b);
1291 if !tags.is_empty() {
1292 st.tags.insert(mrc_arn, tags);
1293 }
1294 st.multi_region_clusters.insert(name, mrc.clone());
1295 ok(json!({ "MultiRegionCluster": mrc_json(&mrc) }))
1296 }
1297
1298 fn describe_multi_region_clusters(
1299 &self,
1300 req: &AwsRequest,
1301 ) -> Result<AwsResponse, AwsServiceError> {
1302 let b = parse(req)?;
1303 let mut accounts = self.state.write();
1304 let st = accounts.get_or_create(&req.account_id);
1305 for m in st.multi_region_clusters.values_mut() {
1306 if m.status == "creating" {
1307 m.status = "available".to_string();
1308 }
1309 }
1310 if let Some(name) = opt_str(&b, "MultiRegionClusterName") {
1311 let Some(m) = st.multi_region_clusters.get(&name) else {
1312 return Err(fault(
1313 "MultiRegionClusterNotFoundFault",
1314 &format!("Multi-region cluster {name} not found."),
1315 ));
1316 };
1317 return ok(json!({ "MultiRegionClusters": [mrc_json(m)] }));
1318 }
1319 let items: Vec<Value> = st.multi_region_clusters.values().map(mrc_json).collect();
1320 let (page, next) = paginate(items, &b, 100);
1321 ok(json!({ "MultiRegionClusters": page, "NextToken": next }))
1322 }
1323
1324 fn update_multi_region_cluster(
1325 &self,
1326 req: &AwsRequest,
1327 ) -> Result<AwsResponse, AwsServiceError> {
1328 let b = parse(req)?;
1329 let name = req_str(&b, "MultiRegionClusterName")?.to_string();
1330 let mut accounts = self.state.write();
1331 let st = accounts.get_or_create(&req.account_id);
1332 let Some(m) = st.multi_region_clusters.get_mut(&name) else {
1333 return Err(fault(
1334 "MultiRegionClusterNotFoundFault",
1335 &format!("Multi-region cluster {name} not found."),
1336 ));
1337 };
1338 if let Some(d) = opt_str(&b, "Description") {
1339 m.description = Some(d);
1340 }
1341 if let Some(v) = opt_str(&b, "EngineVersion") {
1342 m.engine_version = v;
1343 }
1344 if let Some(v) = opt_str(&b, "NodeType") {
1345 m.node_type = v;
1346 }
1347 let out = mrc_json(m);
1348 ok(json!({ "MultiRegionCluster": out }))
1349 }
1350
1351 fn delete_multi_region_cluster(
1352 &self,
1353 req: &AwsRequest,
1354 ) -> Result<AwsResponse, AwsServiceError> {
1355 let b = parse(req)?;
1356 let name = req_str(&b, "MultiRegionClusterName")?.to_string();
1357 let mut accounts = self.state.write();
1358 let st = accounts.get_or_create(&req.account_id);
1359 let Some(mut m) = st.multi_region_clusters.remove(&name) else {
1360 return Err(fault(
1361 "MultiRegionClusterNotFoundFault",
1362 &format!("Multi-region cluster {name} not found."),
1363 ));
1364 };
1365 m.status = "deleting".to_string();
1366 st.tags.remove(&m.arn);
1367 ok(json!({ "MultiRegionCluster": mrc_json(&m) }))
1368 }
1369
1370 fn list_allowed_mr_updates(&self, req: &AwsRequest) -> Result<AwsResponse, AwsServiceError> {
1371 let b = parse(req)?;
1372 let name = req_str(&b, "MultiRegionClusterName")?.to_string();
1373 let accounts = self.state.read();
1374 if accounts
1375 .get(&req.account_id)
1376 .and_then(|s| s.multi_region_clusters.get(&name))
1377 .is_none()
1378 {
1379 return Err(fault(
1380 "MultiRegionClusterNotFoundFault",
1381 &format!("Multi-region cluster {name} not found."),
1382 ));
1383 }
1384 ok(
1385 json!({ "ScaleUpNodeTypes": ["db.r7g.large", "db.r7g.xlarge"], "ScaleDownNodeTypes": [] }),
1386 )
1387 }
1388
1389 fn describe_mr_parameter_groups(
1390 &self,
1391 req: &AwsRequest,
1392 ) -> Result<AwsResponse, AwsServiceError> {
1393 let _ = parse(req)?;
1394 ok(json!({ "MultiRegionParameterGroups": [], "NextToken": Value::Null }))
1395 }
1396
1397 fn describe_mr_parameters(&self, req: &AwsRequest) -> Result<AwsResponse, AwsServiceError> {
1398 let b = parse(req)?;
1399 req_str(&b, "MultiRegionParameterGroupName")?;
1400 ok(json!({ "MultiRegionParameters": [], "NextToken": Value::Null }))
1401 }
1402
1403 fn tag_resource(&self, req: &AwsRequest) -> Result<AwsResponse, AwsServiceError> {
1404 let b = parse(req)?;
1405 let resource_arn = req_str(&b, "ResourceArn")?.to_string();
1406 let mut accounts = self.state.write();
1407 let st = accounts.get_or_create(&req.account_id);
1408 if !arn_exists(st, &resource_arn) {
1409 return Err(fault(
1410 "InvalidARNFault",
1411 &format!("{resource_arn} does not refer to a known resource."),
1412 ));
1413 }
1414 let entry = st.tags.entry(resource_arn).or_default();
1415 for (k, v) in parse_tags(&b) {
1416 entry.insert(k, v);
1417 }
1418 let all = entry.clone();
1419 ok(json!({ "TagList": tags_json(&all) }))
1420 }
1421
1422 fn untag_resource(&self, req: &AwsRequest) -> Result<AwsResponse, AwsServiceError> {
1423 let b = parse(req)?;
1424 let resource_arn = req_str(&b, "ResourceArn")?.to_string();
1425 let mut accounts = self.state.write();
1426 let st = accounts.get_or_create(&req.account_id);
1427 if !arn_exists(st, &resource_arn) {
1428 return Err(fault(
1429 "InvalidARNFault",
1430 &format!("{resource_arn} does not refer to a known resource."),
1431 ));
1432 }
1433 let keys = str_list(&b, "TagKeys");
1434 let entry = st.tags.entry(resource_arn).or_default();
1435 for k in &keys {
1436 entry.remove(k);
1437 }
1438 let all = entry.clone();
1439 ok(json!({ "TagList": tags_json(&all) }))
1440 }
1441
1442 fn list_tags(&self, req: &AwsRequest) -> Result<AwsResponse, AwsServiceError> {
1443 let b = parse(req)?;
1444 let resource_arn = req_str(&b, "ResourceArn")?.to_string();
1445 let accounts = self.state.read();
1446 let st = accounts.get(&req.account_id);
1447 let Some(st) = st else {
1448 return ok(json!({ "TagList": [] }));
1449 };
1450 if !arn_exists(st, &resource_arn) {
1451 return Err(fault(
1452 "InvalidARNFault",
1453 &format!("{resource_arn} does not refer to a known resource."),
1454 ));
1455 }
1456 let tags = st.tags.get(&resource_arn).cloned().unwrap_or_default();
1457 ok(json!({ "TagList": tags_json(&tags) }))
1458 }
1459
1460 fn describe_engine_versions(&self, req: &AwsRequest) -> Result<AwsResponse, AwsServiceError> {
1461 let _ = parse(req)?;
1462 let versions = json!([
1463 { "Engine": "redis", "EngineVersion": "7.1", "EnginePatchVersion": "7.1.1", "ParameterGroupFamily": "memorydb_redis7" },
1464 { "Engine": "redis", "EngineVersion": "6.2", "EnginePatchVersion": "6.2.6", "ParameterGroupFamily": "memorydb_redis6" },
1465 { "Engine": "valkey", "EngineVersion": "7.2", "EnginePatchVersion": "7.2.0", "ParameterGroupFamily": "memorydb_valkey7" },
1466 ]);
1467 ok(json!({ "EngineVersions": versions, "NextToken": Value::Null }))
1468 }
1469
1470 fn describe_events(&self, req: &AwsRequest) -> Result<AwsResponse, AwsServiceError> {
1471 let b = parse(req)?;
1472 if let Some(st) = opt_str(&b, "SourceType") {
1473 const VALID: &[&str] = &[
1474 "node",
1475 "parameter_group",
1476 "subnet_group",
1477 "cluster",
1478 "user",
1479 "acl",
1480 ];
1481 if !VALID.contains(&st.as_str()) {
1482 return Err(fault(
1483 "InvalidParameterValueException",
1484 &format!("Invalid SourceType: {st}"),
1485 ));
1486 }
1487 }
1488 ok(json!({ "Events": [], "NextToken": Value::Null }))
1489 }
1490
1491 fn describe_service_updates(&self, req: &AwsRequest) -> Result<AwsResponse, AwsServiceError> {
1492 let _ = parse(req)?;
1493 ok(json!({ "ServiceUpdates": [], "NextToken": Value::Null }))
1494 }
1495
1496 fn describe_reserved_nodes(&self, req: &AwsRequest) -> Result<AwsResponse, AwsServiceError> {
1497 let b = parse(req)?;
1498 let accounts = self.state.read();
1499 let st = accounts.get(&req.account_id);
1500 let Some(st) = st else {
1501 return ok(json!({ "ReservedNodes": [] }));
1502 };
1503 let items: Vec<Value> = st.reserved_nodes.values().map(reserved_node_json).collect();
1504 let (page, next) = paginate(items, &b, 100);
1505 ok(json!({ "ReservedNodes": page, "NextToken": next }))
1506 }
1507
1508 fn describe_reserved_nodes_offerings(
1509 &self,
1510 req: &AwsRequest,
1511 ) -> Result<AwsResponse, AwsServiceError> {
1512 let _ = parse(req)?;
1513 let offerings = json!([
1514 {
1515 "ReservedNodesOfferingId": "00000000-1111-2222-3333-444444444444",
1516 "NodeType": "db.r6g.large", "Duration": 31536000, "FixedPrice": 1200.0,
1517 "OfferingType": "All Upfront", "RecurringCharges": [],
1518 }
1519 ]);
1520 ok(json!({ "ReservedNodesOfferings": offerings, "NextToken": Value::Null }))
1521 }
1522
1523 fn purchase_reserved_nodes_offering(
1524 &self,
1525 req: &AwsRequest,
1526 ) -> Result<AwsResponse, AwsServiceError> {
1527 let b = parse(req)?;
1528 let offering_id = req_str(&b, "ReservedNodesOfferingId")?.to_string();
1529 let reservation_id = opt_str(&b, "ReservationId")
1530 .unwrap_or_else(|| format!("ri-{}", uuid::Uuid::new_v4().simple()));
1531 let count = b.get("NodeCount").and_then(Value::as_i64).unwrap_or(1) as i32;
1532 let mut accounts = self.state.write();
1533 let st = accounts.get_or_create(&req.account_id);
1534 let node_arn = arn(
1535 "reservednode",
1536 &req.region,
1537 &req.account_id,
1538 &reservation_id,
1539 );
1540 let rn = ReservedNode {
1541 reservation_id: reservation_id.clone(),
1542 reserved_nodes_offering_id: offering_id,
1543 node_type: "db.r6g.large".to_string(),
1544 start_time: Utc::now(),
1545 duration: 31536000,
1546 fixed_price: 1200.0,
1547 node_count: count,
1548 offering_type: "All Upfront".to_string(),
1549 state: "active".to_string(),
1550 arn: node_arn,
1551 };
1552 st.reserved_nodes.insert(reservation_id, rn.clone());
1553 ok(json!({ "ReservedNode": reserved_node_json(&rn) }))
1554 }
1555
1556 fn list_allowed_node_type_updates(
1557 &self,
1558 req: &AwsRequest,
1559 ) -> Result<AwsResponse, AwsServiceError> {
1560 let b = parse(req)?;
1561 let name = req_str(&b, "ClusterName")?.to_string();
1562 let accounts = self.state.read();
1563 if accounts
1564 .get(&req.account_id)
1565 .and_then(|s| s.clusters.get(&name))
1566 .is_none()
1567 {
1568 return Err(fault(
1569 "ClusterNotFoundFault",
1570 &format!("Cluster {name} not found."),
1571 ));
1572 }
1573 ok(json!({
1574 "ScaleUpNodeTypes": ["db.r6g.xlarge", "db.r6g.2xlarge"],
1575 "ScaleDownNodeTypes": ["db.r6g.large"],
1576 }))
1577 }
1578}
1579
1580fn validate_user_name(name: &str) -> Result<(), AwsServiceError> {
1583 let valid = !name.is_empty()
1584 && name.starts_with(|c: char| c.is_ascii_alphabetic())
1585 && name.chars().all(|c| c.is_ascii_alphanumeric() || c == '-');
1586 if valid {
1587 Ok(())
1588 } else {
1589 Err(fault(
1590 "InvalidParameterValueException",
1591 &format!("Invalid UserName: {name}"),
1592 ))
1593 }
1594}
1595
1596fn arn_exists(st: &MemoryDbState, resource_arn: &str) -> bool {
1598 st.clusters.values().any(|c| c.arn == resource_arn)
1599 || st.acls.values().any(|a| a.arn == resource_arn)
1600 || st.users.values().any(|u| u.arn == resource_arn)
1601 || st.parameter_groups.values().any(|p| p.arn == resource_arn)
1602 || st.subnet_groups.values().any(|g| g.arn == resource_arn)
1603 || st.snapshots.values().any(|s| s.arn == resource_arn)
1604 || st
1605 .multi_region_clusters
1606 .values()
1607 .any(|m| m.arn == resource_arn)
1608 || st.reserved_nodes.values().any(|r| r.arn == resource_arn)
1609}
1610
1611#[cfg(test)]
1612mod tests {
1613 use super::*;
1614 use fakecloud_core::multi_account::MultiAccountState;
1615
1616 fn service() -> MemoryDbService {
1617 let state = Arc::new(parking_lot::RwLock::new(MultiAccountState::new(
1618 "123456789012",
1619 "us-east-1",
1620 "http://localhost:4566",
1621 )));
1622 MemoryDbService::new(state)
1623 }
1624
1625 fn req(action: &str, body: Value) -> AwsRequest {
1626 AwsRequest {
1627 service: "memorydb".to_string(),
1628 action: action.to_string(),
1629 region: "us-east-1".to_string(),
1630 account_id: "123456789012".to_string(),
1631 request_id: "rid".to_string(),
1632 headers: http::HeaderMap::new(),
1633 query_params: std::collections::HashMap::new(),
1634 body: bytes::Bytes::from(serde_json::to_vec(&body).unwrap()),
1635 body_stream: parking_lot::Mutex::new(None),
1636 path_segments: vec![],
1637 raw_path: "/".to_string(),
1638 raw_query: String::new(),
1639 method: http::Method::POST,
1640 is_query_protocol: false,
1641 access_key_id: None,
1642 principal: None,
1643 }
1644 }
1645
1646 fn call(s: &MemoryDbService, action: &str, body: Value) -> Result<Value, AwsServiceError> {
1647 let resp = dispatch(s, &req(action, body))?;
1648 Ok(serde_json::from_slice(resp.body.expect_bytes()).unwrap())
1649 }
1650
1651 #[test]
1652 fn create_cluster_round_trips_and_describes() {
1653 let s = service();
1654 let out = call(
1655 &s,
1656 "CreateCluster",
1657 json!({"ClusterName": "app", "NodeType": "db.r6g.large", "ACLName": "open-access"}),
1658 )
1659 .unwrap();
1660 assert_eq!(out["Cluster"]["Name"], "app");
1661
1662 let desc = call(&s, "DescribeClusters", json!({"ClusterName": "app"})).unwrap();
1663 let clusters = desc["Clusters"].as_array().unwrap();
1664 assert_eq!(clusters.len(), 1);
1665 assert_eq!(clusters[0]["Status"], "available");
1667 }
1668
1669 #[test]
1670 fn create_cluster_rejects_out_of_range_shard_count() {
1671 let s = service();
1672 let err = call(
1674 &s,
1675 "CreateCluster",
1676 json!({"ClusterName": "big", "NodeType": "db.r6g.large", "ACLName": "open-access", "NumShards": 2_000_000_000i64}),
1677 )
1678 .unwrap_err();
1679 assert!(
1680 err.message().contains("NumShards"),
1681 "got: {}",
1682 err.message()
1683 );
1684 }
1685
1686 #[test]
1687 fn create_cluster_rejects_out_of_range_replica_count() {
1688 let s = service();
1689 let err = call(
1690 &s,
1691 "CreateCluster",
1692 json!({"ClusterName": "r", "NodeType": "db.r6g.large", "ACLName": "open-access", "NumReplicasPerShard": 99}),
1693 )
1694 .unwrap_err();
1695 assert!(
1696 err.message().contains("NumReplicasPerShard"),
1697 "got: {}",
1698 err.message()
1699 );
1700 }
1701
1702 #[test]
1703 fn create_user_rejects_invalid_name() {
1704 let s = service();
1705 let err = call(
1707 &s,
1708 "CreateUser",
1709 json!({"UserName": "1bad", "AccessString": "on ~* &* +@all", "AuthenticationMode": {"Type": "no-password-required"}}),
1710 )
1711 .unwrap_err();
1712 assert!(err.message().contains("UserName"), "got: {}", err.message());
1713 }
1714
1715 #[test]
1716 fn default_acl_and_user_are_seeded() {
1717 let s = service();
1718 let acls = call(&s, "DescribeACLs", json!({})).unwrap();
1719 assert!(acls["ACLs"]
1720 .as_array()
1721 .unwrap()
1722 .iter()
1723 .any(|a| a["Name"] == "open-access"));
1724 let users = call(&s, "DescribeUsers", json!({})).unwrap();
1725 assert!(users["Users"]
1726 .as_array()
1727 .unwrap()
1728 .iter()
1729 .any(|u| u["Name"] == "default"));
1730 }
1731}