Skip to main content

fakecloud_memorydb/
service.rs

1//! MemoryDB (`memorydb`) awsJson1.1 dispatch + operation handlers.
2//!
3//! Full 45-op control plane. Clusters transition `creating` -> `available`
4//! lazily on describe (deterministic, no background timer needed for the
5//! control-plane contract). The Redis/Valkey data-plane container backing is a
6//! follow-on, mirroring how Aurora DSQL shipped its control plane first.
7
8use 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
26/// Every operation name in the MemoryDB Smithy model.
27pub 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    // Note: FailoverShard and BatchUpdateCluster do not persist any state (they
131    // validate + echo), so they are intentionally NOT flagged mutating — no
132    // redundant snapshot write on those calls.
133    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
197// ===== helpers =====
198
199fn 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    // The MemoryDB model declares `httpError: 404` for the entire
217    // `*NotFoundFault` family (ClusterNotFoundFault, ACLNotFoundFault, ...),
218    // with the single exception of ServiceLinkedRoleNotFoundFault (400).
219    // Everything else (InvalidParameterValueException, *AlreadyExistsFault,
220    // etc.) is a 400.
221    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
257/// Read an optional integer request field, defaulting when absent and
258/// range-validating before the `as i32` cast so an out-of-range value is a
259/// clean 400 rather than a silent truncation/wrap.
260fn 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
298/// Decode a numeric next-token; unparseable/empty -> page start 0 (`None` for
299/// a genuinely bad token so callers return a terminal empty page).
300fn 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
314/// Paginate a pre-sorted Vec of JSON items into (page, next_token).
315fn 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
329/// Build the shard/node topology for a cluster of `num_shards` shards, each with
330/// `replicas` read replicas (so `replicas + 1` nodes per shard). Shared by
331/// CreateCluster and UpdateCluster's scaling path so both produce identical
332/// node naming, endpoints and contiguous 16384-slot partitioning. New shards
333/// start `creating` and settle to `available` on the next DescribeClusters.
334fn 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            // Partition the 16384 keyspace slots contiguously across shards,
354            // the way MemoryDB/Redis Cluster does (shard 1 `0-8191`,
355            // shard 2 `8192-16383`, ...), instead of giving every shard the
356            // full range.
357            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
370// ===== JSON builders (member names match the Smithy model) =====
371
372fn 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
396/// `cluster_json`, but drops the per-shard `Shards` detail when `show_shards`
397/// is false (DescribeClusters `ShowShardDetails=false`).
398fn 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
495/// `snapshot_json`, dropping the heavy `ClusterConfiguration` when `show_detail`
496/// is false (DescribeSnapshots `ShowDetail=false`).
497fn 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
546// ===== handlers =====
547
548impl 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        // AWS caps: up to 500 shards per cluster, 0-5 replicas per shard.
563        // Validate before building node vectors so an out-of-range request is a
564        // clean 400 rather than an unbounded allocation.
565        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        // SnapshotRetentionLimit: AWS accepts 0-35 days.
585        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        // Maintain the ACL -> clusters back-reference so DescribeACLs lists the
655        // clusters using each ACL (AWS keeps this bidirectional).
656        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        // ShowShardDetails defaults to true; when explicitly false AWS omits the
668        // per-shard `Shards` detail from each cluster.
669        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        // Lazy transition creating -> available. This mutates under a write lock
676        // on a Describe (non-persisted) path, but the transition is idempotent
677        // and self-healing: after a restart the next Describe re-applies it, and
678        // the first mutating op snapshots the settled state. No data is lost.
679        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        // Track an ACL reassignment so the ACL -> clusters back-references can be
735        // moved after the cluster borrow is released.
736        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        // Scaling: ShardConfiguration.ShardCount changes the number of shards and
757        // ReplicaConfiguration.ReplicaCount changes replicas-per-shard. Either can
758        // appear alone; the current replica count is recovered from an existing
759        // shard's node count (nodes = replicas + 1). Rebuilding the topology keeps
760        // node naming, endpoints and slot partitioning consistent with create.
761        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            // Freshly-built shards start `creating`; mark the cluster so the
799            // existing lazy DescribeClusters transition settles it to `available`.
800            c.status = "creating".to_string();
801        }
802        let out = cluster_json(c);
803        // Apply any ACL reassignment to the ACL -> clusters back-references.
804        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        // Drop the ACL -> clusters back-reference.
833        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                // A name that doesn't resolve to a cluster comes back in
850                // UnprocessedClusters, not silently dropped.
851                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        // ShardName is required by the model and must name an existing shard.
865        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        // Maintain the User -> ACLNames back-reference (AWS keeps it bidirectional).
909        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        // Sync the User -> ACLNames back-reference for the affected users.
962        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        // Drop the ACL from each member user's ACLNames back-reference.
988        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        // AuthenticationMode is required by the model; its Type must be one of the
1002        // InputAuthenticationType enum values (`password` | `iam`), and a
1003        // `password`-type user must ship at least one password.
1004        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        // ShowDetail defaults to true; false omits ClusterConfiguration.
1454        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        // Source filters by snapshot origin; "all" (or absent) matches everything.
1466        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
1819/// Validate a `UserName` against the model constraints: length >= 1 and
1820/// pattern `^[a-zA-Z][a-zA-Z0-9-]*$`.
1821fn 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
1835/// Whether a given ARN refers to any resource in this account's state.
1836fn arn_exists(st: &MemoryDbState, resource_arn: &str) -> bool {
1837    // An empty ARN never refers to a resource (guards against matching a
1838    // resource that happens to carry an empty ARN string).
1839    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        // Lazily transitions creating -> available on describe.
1910        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        // Scale out to 4 shards and 2 replicas each (3 nodes per shard).
1924        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        // Scale replicas down to 0 alone (shard count unchanged).
1945        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        // A huge NumShards must be rejected, not allocated (OOM guard).
1962        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        // Must start with a letter.
1995        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        // A validation error stays a 400.
2042        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        // Deleting the cluster removes the back-reference.
2065        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        // Removing the user from the ACL drops the back-reference.
2090        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        // Missing AuthenticationMode -> rejected.
2125        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        // password type with zero passwords -> rejected.
2137        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        // invalid Type -> rejected.
2145        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        // iam type -> accepted.
2157        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        // Unknown shard -> ShardNotFoundFault (404).
2175        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        // Real shard "0001" -> ok.
2183        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        // ShowDetail=false drops ClusterConfiguration.
2281        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        // Source=system matches nothing (created snapshots are user-source).
2289        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        // The default group is readable by name (the TF provider reads it).
2314        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}