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