Skip to main content

fakecloud_rds/
state.rs

1use std::collections::{BTreeMap, HashSet};
2use std::fmt;
3use std::sync::Arc;
4
5use chrono::{DateTime, Utc};
6use fakecloud_aws::arn::Arn;
7use parking_lot::RwLock;
8use uuid::Uuid;
9
10pub type SharedRdsState = Arc<RwLock<fakecloud_core::multi_account::MultiAccountState<RdsState>>>;
11
12impl fakecloud_core::multi_account::AccountState for RdsState {
13    fn new_for_account(account_id: &str, region: &str, _endpoint: &str) -> Self {
14        Self::new(account_id, region)
15    }
16}
17
18/// Supported DB instance classes — single source of truth.
19pub const SUPPORTED_INSTANCE_CLASSES: &[&str] = &[
20    "db.t3.micro",
21    "db.t3.small",
22    "db.t3.medium",
23    "db.t3.large",
24    "db.t4g.micro",
25    "db.t4g.small",
26    "db.m5.large",
27];
28
29#[derive(Clone, serde::Serialize, serde::Deserialize)]
30pub struct DbInstance {
31    pub db_instance_identifier: String,
32    pub db_instance_arn: String,
33    pub db_instance_class: String,
34    pub engine: String,
35    pub engine_version: String,
36    pub db_instance_status: String,
37    pub master_username: String,
38    pub db_name: Option<String>,
39    pub endpoint_address: String,
40    pub port: i32,
41    pub allocated_storage: i32,
42    pub publicly_accessible: bool,
43    pub deletion_protection: bool,
44    pub created_at: DateTime<Utc>,
45    pub dbi_resource_id: String,
46    pub master_user_password: String,
47    pub container_id: String,
48    pub host_port: u16,
49    pub tags: Vec<RdsTag>,
50    pub read_replica_source_db_instance_identifier: Option<String>,
51    pub read_replica_db_instance_identifiers: Vec<String>,
52    pub vpc_security_group_ids: Vec<String>,
53    pub db_parameter_group_name: Option<String>,
54    pub backup_retention_period: i32,
55    pub preferred_backup_window: String,
56    #[serde(default)]
57    pub preferred_maintenance_window: Option<String>,
58    pub latest_restorable_time: Option<DateTime<Utc>>,
59    pub option_group_name: Option<String>,
60    pub multi_az: bool,
61    pub pending_modified_values: Option<PendingModifiedValues>,
62    /// Read from input on Create/Modify; defaults preserve existing
63    /// behaviour (non-encrypted, gp2, single AZ, no IAM auth).
64    #[serde(default)]
65    pub availability_zone: Option<String>,
66    #[serde(default)]
67    pub storage_type: Option<String>,
68    #[serde(default)]
69    pub storage_encrypted: bool,
70    #[serde(default)]
71    pub kms_key_id: Option<String>,
72    #[serde(default)]
73    pub iam_database_authentication_enabled: bool,
74    #[serde(default)]
75    pub iops: Option<i32>,
76    #[serde(default)]
77    pub monitoring_interval: Option<i32>,
78    #[serde(default)]
79    pub monitoring_role_arn: Option<String>,
80    #[serde(default)]
81    pub performance_insights_enabled: bool,
82    #[serde(default)]
83    pub performance_insights_kms_key_id: Option<String>,
84    #[serde(default)]
85    pub performance_insights_retention_period: Option<i32>,
86    #[serde(default)]
87    pub enabled_cloudwatch_logs_exports: Vec<String>,
88    #[serde(default)]
89    pub ca_certificate_identifier: Option<String>,
90    #[serde(default)]
91    pub network_type: Option<String>,
92    #[serde(default)]
93    pub character_set_name: Option<String>,
94    #[serde(default)]
95    pub auto_minor_version_upgrade: Option<bool>,
96    #[serde(default)]
97    pub copy_tags_to_snapshot: Option<bool>,
98    #[serde(default)]
99    pub master_user_secret_arn: Option<String>,
100    #[serde(default)]
101    pub master_user_secret_kms_key_id: Option<String>,
102    /// Settable via Modify; AWS reports the engine-derived default until
103    /// the caller overrides. We honor explicit overrides but fall back to
104    /// `license_model_for_engine` in XML when this is `None`.
105    #[serde(default)]
106    pub license_model: Option<String>,
107    #[serde(default)]
108    pub max_allocated_storage: Option<i32>,
109    #[serde(default)]
110    pub multi_tenant: Option<bool>,
111    #[serde(default)]
112    pub storage_throughput: Option<i32>,
113    #[serde(default)]
114    pub tde_credential_arn: Option<String>,
115    #[serde(default)]
116    pub delete_automated_backups: Option<bool>,
117    #[serde(default)]
118    pub db_security_groups: Vec<String>,
119    /// Active Directory domain membership. AWS exposes these via
120    /// `<DomainMemberships><DomainMembership>...` in describe responses.
121    #[serde(default)]
122    pub domain: Option<String>,
123    #[serde(default)]
124    pub domain_fqdn: Option<String>,
125    #[serde(default)]
126    pub domain_ou: Option<String>,
127    #[serde(default)]
128    pub domain_iam_role_name: Option<String>,
129    #[serde(default)]
130    pub domain_auth_secret_arn: Option<String>,
131    #[serde(default)]
132    pub domain_dns_ips: Vec<String>,
133    /// Aurora cluster the instance is a member of, when set. Mirrors
134    /// `DBClusterIdentifier` on CreateDBInstance / RestoreDB* requests so
135    /// snapshot/restore paths can find the writer for a given cluster.
136    #[serde(default)]
137    pub db_cluster_identifier: Option<String>,
138    /// Database Activity Stream configuration, written by
139    /// `StartActivityStream` / `ModifyActivityStream` and cleared to
140    /// `stopped` by `StopActivityStream`. `None` reads back as a stopped
141    /// stream in describe responses.
142    #[serde(default)]
143    pub activity_stream: Option<ActivityStreamConfig>,
144}
145
146/// Database Activity Stream state for a DB instance. Persisted so that
147/// `StartActivityStream` / `StopActivityStream` / `ModifyActivityStream`
148/// round-trip through `DescribeDBInstances` instead of always reporting
149/// `stopped`.
150#[derive(Debug, Clone, Default, serde::Serialize, serde::Deserialize)]
151pub struct ActivityStreamConfig {
152    /// One of `starting` | `started` | `stopping` | `stopped`.
153    pub status: String,
154    #[serde(default)]
155    pub mode: Option<String>,
156    #[serde(default)]
157    pub kms_key_id: Option<String>,
158    #[serde(default)]
159    pub kinesis_stream_name: Option<String>,
160}
161
162#[derive(Clone, Default, serde::Serialize, serde::Deserialize)]
163pub struct PendingModifiedValues {
164    pub db_instance_class: Option<String>,
165    pub allocated_storage: Option<i32>,
166    pub backup_retention_period: Option<i32>,
167    pub multi_az: Option<bool>,
168    pub engine_version: Option<String>,
169    pub master_user_password: Option<String>,
170    #[serde(default)]
171    pub preferred_backup_window: Option<String>,
172    #[serde(default)]
173    pub preferred_maintenance_window: Option<String>,
174    #[serde(default)]
175    pub db_parameter_group_name: Option<String>,
176    #[serde(default)]
177    pub iops: Option<i32>,
178    #[serde(default)]
179    pub storage_type: Option<String>,
180    #[serde(default)]
181    pub monitoring_interval: Option<i32>,
182    #[serde(default)]
183    pub performance_insights_enabled: Option<bool>,
184    #[serde(default)]
185    pub enabled_cloudwatch_logs_exports: Option<Vec<String>>,
186    #[serde(default)]
187    pub storage_throughput: Option<i32>,
188    #[serde(default)]
189    pub license_model: Option<String>,
190    #[serde(default)]
191    pub multi_tenant: Option<bool>,
192    #[serde(default)]
193    pub publicly_accessible: Option<bool>,
194    #[serde(default)]
195    pub tde_credential_arn: Option<String>,
196    #[serde(default)]
197    pub port: Option<i32>,
198    #[serde(default)]
199    pub ca_certificate_identifier: Option<String>,
200}
201
202impl fmt::Debug for PendingModifiedValues {
203    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
204        f.debug_struct("PendingModifiedValues")
205            .field("db_instance_class", &self.db_instance_class)
206            .field("allocated_storage", &self.allocated_storage)
207            .field("backup_retention_period", &self.backup_retention_period)
208            .field("multi_az", &self.multi_az)
209            .field("engine_version", &self.engine_version)
210            .field(
211                "master_user_password",
212                &self.master_user_password.as_ref().map(|_| "<redacted>"),
213            )
214            .field("preferred_backup_window", &self.preferred_backup_window)
215            .field(
216                "preferred_maintenance_window",
217                &self.preferred_maintenance_window,
218            )
219            .field("db_parameter_group_name", &self.db_parameter_group_name)
220            .field("iops", &self.iops)
221            .field("storage_type", &self.storage_type)
222            .field("monitoring_interval", &self.monitoring_interval)
223            .field(
224                "performance_insights_enabled",
225                &self.performance_insights_enabled,
226            )
227            .field(
228                "enabled_cloudwatch_logs_exports",
229                &self.enabled_cloudwatch_logs_exports,
230            )
231            .field("storage_throughput", &self.storage_throughput)
232            .field("license_model", &self.license_model)
233            .field("multi_tenant", &self.multi_tenant)
234            .field("publicly_accessible", &self.publicly_accessible)
235            .field("tde_credential_arn", &self.tde_credential_arn)
236            .field("port", &self.port)
237            .field("ca_certificate_identifier", &self.ca_certificate_identifier)
238            .finish()
239    }
240}
241
242impl fmt::Debug for DbInstance {
243    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
244        f.debug_struct("DbInstance")
245            .field("db_instance_identifier", &self.db_instance_identifier)
246            .field("db_instance_arn", &self.db_instance_arn)
247            .field("db_instance_class", &self.db_instance_class)
248            .field("engine", &self.engine)
249            .field("engine_version", &self.engine_version)
250            .field("db_instance_status", &self.db_instance_status)
251            .field("master_username", &self.master_username)
252            .field("db_name", &self.db_name)
253            .field("endpoint_address", &self.endpoint_address)
254            .field("port", &self.port)
255            .field("allocated_storage", &self.allocated_storage)
256            .field("publicly_accessible", &self.publicly_accessible)
257            .field("deletion_protection", &self.deletion_protection)
258            .field("created_at", &self.created_at)
259            .field("dbi_resource_id", &self.dbi_resource_id)
260            .field("master_user_password", &"<redacted>")
261            .field("container_id", &self.container_id)
262            .field("host_port", &self.host_port)
263            .field("tags", &self.tags)
264            .field(
265                "read_replica_source_db_instance_identifier",
266                &self.read_replica_source_db_instance_identifier,
267            )
268            .field(
269                "read_replica_db_instance_identifiers",
270                &self.read_replica_db_instance_identifiers,
271            )
272            .field("vpc_security_group_ids", &self.vpc_security_group_ids)
273            .field("db_parameter_group_name", &self.db_parameter_group_name)
274            .field("backup_retention_period", &self.backup_retention_period)
275            .field("preferred_backup_window", &self.preferred_backup_window)
276            .field("latest_restorable_time", &self.latest_restorable_time)
277            .field("option_group_name", &self.option_group_name)
278            .field("multi_az", &self.multi_az)
279            .field("pending_modified_values", &self.pending_modified_values)
280            .finish()
281    }
282}
283
284#[derive(Debug, Clone, PartialEq, Eq, serde::Serialize, serde::Deserialize)]
285pub struct RdsTag {
286    pub key: String,
287    pub value: String,
288}
289
290#[derive(Clone, serde::Serialize, serde::Deserialize)]
291pub struct DbSnapshot {
292    pub db_snapshot_identifier: String,
293    pub db_snapshot_arn: String,
294    pub db_instance_identifier: String,
295    pub snapshot_create_time: DateTime<Utc>,
296    pub engine: String,
297    pub engine_version: String,
298    pub allocated_storage: i32,
299    pub status: String,
300    pub port: i32,
301    pub master_username: String,
302    pub db_name: Option<String>,
303    pub dbi_resource_id: String,
304    pub snapshot_type: String,
305    pub master_user_password: String,
306    pub tags: Vec<RdsTag>,
307    pub dump_data: Vec<u8>,
308    #[serde(default)]
309    pub availability_zone: Option<String>,
310    #[serde(default)]
311    pub vpc_id: Option<String>,
312    #[serde(default)]
313    pub instance_create_time: Option<DateTime<Utc>>,
314    #[serde(default)]
315    pub license_model: Option<String>,
316    #[serde(default)]
317    pub iops: Option<i32>,
318    #[serde(default)]
319    pub option_group_name: Option<String>,
320    #[serde(default)]
321    pub percent_progress: Option<i32>,
322    #[serde(default)]
323    pub storage_type: Option<String>,
324    #[serde(default)]
325    pub encrypted: bool,
326    #[serde(default)]
327    pub kms_key_id: Option<String>,
328    #[serde(default)]
329    pub iam_database_authentication_enabled: bool,
330    #[serde(default)]
331    pub timezone: Option<String>,
332    #[serde(default)]
333    pub storage_throughput: Option<i32>,
334    /// Snapshot share attributes keyed by attribute name (currently only
335    /// `restore`), written by `ModifyDBSnapshotAttribute` and surfaced by
336    /// `DescribeDBSnapshotAttributes`. The `restore` list holds the AWS
337    /// account ids the snapshot is shared with (or the literal `all` for a
338    /// public snapshot).
339    #[serde(default)]
340    pub snapshot_attributes: BTreeMap<String, Vec<String>>,
341}
342
343impl fmt::Debug for DbSnapshot {
344    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
345        f.debug_struct("DbSnapshot")
346            .field("db_snapshot_identifier", &self.db_snapshot_identifier)
347            .field("db_snapshot_arn", &self.db_snapshot_arn)
348            .field("db_instance_identifier", &self.db_instance_identifier)
349            .field("snapshot_create_time", &self.snapshot_create_time)
350            .field("engine", &self.engine)
351            .field("engine_version", &self.engine_version)
352            .field("allocated_storage", &self.allocated_storage)
353            .field("status", &self.status)
354            .field("port", &self.port)
355            .field("master_username", &self.master_username)
356            .field("db_name", &self.db_name)
357            .field("dbi_resource_id", &self.dbi_resource_id)
358            .field("snapshot_type", &self.snapshot_type)
359            .field("master_user_password", &"<redacted>")
360            .field("tags", &self.tags)
361            .field("dump_data", &format!("<{} bytes>", self.dump_data.len()))
362            .finish()
363    }
364}
365
366#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
367pub struct RdsState {
368    pub account_id: String,
369    pub region: String,
370    pub instances: BTreeMap<String, DbInstance>,
371    pub in_progress_instance_ids: HashSet<String>,
372    pub snapshots: BTreeMap<String, DbSnapshot>,
373    pub subnet_groups: BTreeMap<String, DbSubnetGroup>,
374    pub parameter_groups: BTreeMap<String, DbParameterGroup>,
375    /// Generic stores keyed by category (clusters, cluster_snapshots,
376    /// cluster_param_groups, proxies, proxy_endpoints, security_groups,
377    /// option_groups, event_subscriptions, global_clusters, integrations,
378    /// blue_green, shard_groups, custom_engine_versions, tenant_dbs,
379    /// export_tasks, etc.) so the extras handlers can persist state
380    /// without proliferating per-category fields.
381    #[serde(default)]
382    pub extras: BTreeMap<String, BTreeMap<String, serde_json::Value>>,
383    /// In-memory ring of RDS events emitted by the service, used by
384    /// `DescribeEvents`. Capped at the most recent ~14 days of events
385    /// (matching real RDS retention) by [`Self::push_event`].
386    #[serde(default)]
387    pub events: Vec<RdsEventRecord>,
388    /// Account-level default CA certificate identifier set by
389    /// `ModifyCertificates`. Returned by `DescribeCertificates` so
390    /// callers see their override on subsequent reads.
391    #[serde(default)]
392    pub default_certificate_identifier: Option<String>,
393}
394
395#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
396pub struct RdsEventRecord {
397    pub source_identifier: String,
398    pub source_type: String,
399    pub source_arn: String,
400    pub event_id: String,
401    pub event_categories: Vec<String>,
402    pub message: String,
403    pub date: chrono::DateTime<chrono::Utc>,
404}
405
406#[derive(Debug, Clone, PartialEq, Eq)]
407pub struct EngineVersionInfo {
408    pub engine: String,
409    pub engine_version: String,
410    pub db_parameter_group_family: String,
411    pub db_engine_description: String,
412    pub db_engine_version_description: String,
413    pub status: String,
414}
415
416#[derive(Debug, Clone, PartialEq, Eq)]
417pub struct OrderableDbInstanceOption {
418    pub engine: String,
419    pub engine_version: String,
420    pub db_instance_class: String,
421    pub license_model: String,
422    pub storage_type: String,
423    pub min_storage_size: i32,
424    pub max_storage_size: i32,
425}
426
427#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
428pub struct DbSubnetGroup {
429    pub db_subnet_group_name: String,
430    pub db_subnet_group_arn: String,
431    pub db_subnet_group_description: String,
432    pub vpc_id: String,
433    pub subnet_ids: Vec<String>,
434    pub subnet_availability_zones: Vec<String>,
435    pub tags: Vec<RdsTag>,
436}
437
438#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
439pub struct DbParameterGroup {
440    pub db_parameter_group_name: String,
441    pub db_parameter_group_arn: String,
442    pub db_parameter_group_family: String,
443    pub description: String,
444    pub parameters: BTreeMap<String, String>,
445    /// Per-parameter `ApplyMethod` (`immediate` | `pending-reboot`),
446    /// keyed by parameter name. Defaulted for snapshots written before
447    /// this field existed; a missing entry reads back as `immediate`.
448    #[serde(default)]
449    pub parameter_apply_methods: BTreeMap<String, String>,
450    pub tags: Vec<RdsTag>,
451}
452
453/// Static metadata for an engine-default parameter, used by
454/// `DescribeDBParameters`/`DescribeDBClusterParameters`/`DescribeEngineDefaultParameters`
455/// to surface a baseline set of parameters when no user override exists.
456///
457/// The seed below is intentionally small (a handful of common knobs per
458/// engine family). Real RDS exposes hundreds of parameters per family;
459/// callers needing comprehensive coverage should add entries to
460/// [`engine_default_parameters`] as needs arise.
461#[derive(Debug, Clone, PartialEq, Eq)]
462pub struct EngineDefaultParameter {
463    pub name: &'static str,
464    pub value: &'static str,
465    pub apply_type: &'static str,
466    pub data_type: &'static str,
467    pub allowed_values: &'static str,
468    pub is_modifiable: bool,
469}
470
471/// Return a small, representative set of engine-default parameters for the
472/// given parameter group family (e.g. `postgres16`, `mysql8.0`,
473/// `aurora-postgresql15`). The list is not comprehensive — real RDS
474/// exposes hundreds of parameters; we ship just enough to make callers
475/// that round-trip `DescribeDBParameters` with `Source=engine-default`
476/// see meaningful entries. Unknown families fall through to an empty list.
477pub fn engine_default_parameters(family: &str) -> &'static [EngineDefaultParameter] {
478    if family.starts_with("postgres") || family.starts_with("aurora-postgresql") {
479        POSTGRES_DEFAULT_PARAMETERS
480    } else if family.starts_with("mysql") || family.starts_with("aurora-mysql") {
481        MYSQL_DEFAULT_PARAMETERS
482    } else if family.starts_with("mariadb") {
483        MARIADB_DEFAULT_PARAMETERS
484    } else {
485        &[]
486    }
487}
488
489const POSTGRES_DEFAULT_PARAMETERS: &[EngineDefaultParameter] = &[
490    EngineDefaultParameter {
491        name: "max_connections",
492        value: "LEAST({DBInstanceClassMemory/9531392},5000)",
493        apply_type: "static",
494        data_type: "integer",
495        allowed_values: "6-8388607",
496        is_modifiable: true,
497    },
498    EngineDefaultParameter {
499        name: "shared_buffers",
500        value: "{DBInstanceClassMemory/32768}",
501        apply_type: "static",
502        data_type: "integer",
503        allowed_values: "16-1073741823",
504        is_modifiable: true,
505    },
506    EngineDefaultParameter {
507        name: "work_mem",
508        value: "4096",
509        apply_type: "dynamic",
510        data_type: "integer",
511        allowed_values: "64-2147483647",
512        is_modifiable: true,
513    },
514    EngineDefaultParameter {
515        name: "maintenance_work_mem",
516        value: "GREATEST({DBInstanceClassMemory/63963136*1024},65536)",
517        apply_type: "dynamic",
518        data_type: "integer",
519        allowed_values: "1024-2147483647",
520        is_modifiable: true,
521    },
522    EngineDefaultParameter {
523        name: "effective_cache_size",
524        value: "{DBInstanceClassMemory/16384}",
525        apply_type: "dynamic",
526        data_type: "integer",
527        allowed_values: "1-2147483647",
528        is_modifiable: true,
529    },
530];
531
532const MYSQL_DEFAULT_PARAMETERS: &[EngineDefaultParameter] = &[
533    EngineDefaultParameter {
534        name: "max_connections",
535        value: "{DBInstanceClassMemory/12582880}",
536        apply_type: "dynamic",
537        data_type: "integer",
538        allowed_values: "1-100000",
539        is_modifiable: true,
540    },
541    EngineDefaultParameter {
542        name: "innodb_buffer_pool_size",
543        value: "{DBInstanceClassMemory*3/4}",
544        apply_type: "static",
545        data_type: "integer",
546        allowed_values: "5242880-2147483648",
547        is_modifiable: true,
548    },
549    EngineDefaultParameter {
550        name: "max_allowed_packet",
551        value: "67108864",
552        apply_type: "dynamic",
553        data_type: "integer",
554        allowed_values: "1024-1073741824",
555        is_modifiable: true,
556    },
557    EngineDefaultParameter {
558        name: "character_set_server",
559        value: "utf8mb4",
560        apply_type: "dynamic",
561        data_type: "string",
562        allowed_values: "utf8,utf8mb4,latin1",
563        is_modifiable: true,
564    },
565];
566
567const MARIADB_DEFAULT_PARAMETERS: &[EngineDefaultParameter] = &[
568    EngineDefaultParameter {
569        name: "max_connections",
570        value: "{DBInstanceClassMemory/12582880}",
571        apply_type: "dynamic",
572        data_type: "integer",
573        allowed_values: "1-100000",
574        is_modifiable: true,
575    },
576    EngineDefaultParameter {
577        name: "innodb_buffer_pool_size",
578        value: "{DBInstanceClassMemory*3/4}",
579        apply_type: "static",
580        data_type: "integer",
581        allowed_values: "5242880-2147483648",
582        is_modifiable: true,
583    },
584    EngineDefaultParameter {
585        name: "max_allowed_packet",
586        value: "67108864",
587        apply_type: "dynamic",
588        data_type: "integer",
589        allowed_values: "1024-1073741824",
590        is_modifiable: true,
591    },
592];
593
594impl RdsState {
595    pub fn new(account_id: &str, region: &str) -> Self {
596        Self {
597            account_id: account_id.to_string(),
598            region: region.to_string(),
599            instances: BTreeMap::new(),
600            in_progress_instance_ids: HashSet::new(),
601            snapshots: BTreeMap::new(),
602            subnet_groups: BTreeMap::new(),
603            parameter_groups: default_parameter_groups(account_id, region),
604            extras: BTreeMap::new(),
605            events: Vec::new(),
606            default_certificate_identifier: None,
607        }
608    }
609
610    pub fn reset(&mut self) {
611        self.instances.clear();
612        self.in_progress_instance_ids.clear();
613        self.snapshots.clear();
614        self.subnet_groups.clear();
615        self.parameter_groups = default_parameter_groups(&self.account_id, &self.region);
616        self.extras.clear();
617        self.events.clear();
618        self.default_certificate_identifier = None;
619    }
620
621    /// Append an event row to the in-memory ring, dropping the oldest
622    /// entries beyond a 14-day window (matching real RDS retention).
623    pub fn push_event(&mut self, event: RdsEventRecord) {
624        const RETENTION_DAYS: i64 = 14;
625        let cutoff = chrono::Utc::now() - chrono::Duration::days(RETENTION_DAYS);
626        self.events.retain(|e| e.date >= cutoff);
627        self.events.push(event);
628    }
629
630    pub fn db_instance_arn(&self, db_instance_identifier: &str) -> String {
631        Arn::new(
632            "rds",
633            &self.region,
634            &self.account_id,
635            &format!("db:{db_instance_identifier}"),
636        )
637        .to_string()
638    }
639
640    pub fn db_snapshot_arn(&self, db_snapshot_identifier: &str) -> String {
641        Arn::new(
642            "rds",
643            &self.region,
644            &self.account_id,
645            &format!("snapshot:{db_snapshot_identifier}"),
646        )
647        .to_string()
648    }
649
650    pub fn db_subnet_group_arn(&self, db_subnet_group_name: &str) -> String {
651        Arn::new(
652            "rds",
653            &self.region,
654            &self.account_id,
655            &format!("subgrp:{db_subnet_group_name}"),
656        )
657        .to_string()
658    }
659
660    pub fn db_parameter_group_arn(&self, db_parameter_group_name: &str) -> String {
661        Arn::new(
662            "rds",
663            &self.region,
664            &self.account_id,
665            &format!("pg:{db_parameter_group_name}"),
666        )
667        .to_string()
668    }
669
670    pub fn next_dbi_resource_id(&self) -> String {
671        format!("db-{}", Uuid::new_v4().simple())
672    }
673
674    pub fn begin_instance_creation(&mut self, db_instance_identifier: &str) -> bool {
675        if self.instances.contains_key(db_instance_identifier)
676            || self
677                .in_progress_instance_ids
678                .contains(db_instance_identifier)
679        {
680            return false;
681        }
682
683        self.in_progress_instance_ids
684            .insert(db_instance_identifier.to_string());
685        true
686    }
687
688    pub fn finish_instance_creation(&mut self, instance: DbInstance) {
689        self.in_progress_instance_ids
690            .remove(&instance.db_instance_identifier);
691        self.instances
692            .insert(instance.db_instance_identifier.clone(), instance);
693    }
694
695    pub fn cancel_instance_creation(&mut self, db_instance_identifier: &str) {
696        self.in_progress_instance_ids.remove(db_instance_identifier);
697    }
698}
699
700pub fn default_engine_versions() -> Vec<EngineVersionInfo> {
701    vec![
702        // PostgreSQL versions
703        EngineVersionInfo {
704            engine: "postgres".to_string(),
705            engine_version: "16.3".to_string(),
706            db_parameter_group_family: "postgres16".to_string(),
707            db_engine_description: "PostgreSQL".to_string(),
708            db_engine_version_description: "PostgreSQL 16.3".to_string(),
709            status: "available".to_string(),
710        },
711        EngineVersionInfo {
712            engine: "postgres".to_string(),
713            engine_version: "15.5".to_string(),
714            db_parameter_group_family: "postgres15".to_string(),
715            db_engine_description: "PostgreSQL".to_string(),
716            db_engine_version_description: "PostgreSQL 15.5".to_string(),
717            status: "available".to_string(),
718        },
719        EngineVersionInfo {
720            engine: "postgres".to_string(),
721            engine_version: "14.10".to_string(),
722            db_parameter_group_family: "postgres14".to_string(),
723            db_engine_description: "PostgreSQL".to_string(),
724            db_engine_version_description: "PostgreSQL 14.10".to_string(),
725            status: "available".to_string(),
726        },
727        EngineVersionInfo {
728            engine: "postgres".to_string(),
729            engine_version: "13.13".to_string(),
730            db_parameter_group_family: "postgres13".to_string(),
731            db_engine_description: "PostgreSQL".to_string(),
732            db_engine_version_description: "PostgreSQL 13.13".to_string(),
733            status: "available".to_string(),
734        },
735        // MySQL versions
736        EngineVersionInfo {
737            engine: "mysql".to_string(),
738            engine_version: "8.0.35".to_string(),
739            db_parameter_group_family: "mysql8.0".to_string(),
740            db_engine_description: "MySQL Community Edition".to_string(),
741            db_engine_version_description: "MySQL 8.0.35".to_string(),
742            status: "available".to_string(),
743        },
744        EngineVersionInfo {
745            engine: "mysql".to_string(),
746            engine_version: "8.0.28".to_string(),
747            db_parameter_group_family: "mysql8.0".to_string(),
748            db_engine_description: "MySQL Community Edition".to_string(),
749            db_engine_version_description: "MySQL 8.0.28".to_string(),
750            status: "available".to_string(),
751        },
752        EngineVersionInfo {
753            engine: "mysql".to_string(),
754            engine_version: "5.7.44".to_string(),
755            db_parameter_group_family: "mysql5.7".to_string(),
756            db_engine_description: "MySQL Community Edition".to_string(),
757            db_engine_version_description: "MySQL 5.7.44".to_string(),
758            status: "available".to_string(),
759        },
760        // MariaDB versions
761        EngineVersionInfo {
762            engine: "mariadb".to_string(),
763            engine_version: "11.4.5".to_string(),
764            db_parameter_group_family: "mariadb11.4".to_string(),
765            db_engine_description: "MariaDB Community Edition".to_string(),
766            db_engine_version_description: "MariaDB 11.4.5".to_string(),
767            status: "available".to_string(),
768        },
769        EngineVersionInfo {
770            engine: "mariadb".to_string(),
771            engine_version: "10.11.6".to_string(),
772            db_parameter_group_family: "mariadb10.11".to_string(),
773            db_engine_description: "MariaDB Community Edition".to_string(),
774            db_engine_version_description: "MariaDB 10.11.6".to_string(),
775            status: "available".to_string(),
776        },
777        EngineVersionInfo {
778            engine: "mariadb".to_string(),
779            engine_version: "10.6.16".to_string(),
780            db_parameter_group_family: "mariadb10.6".to_string(),
781            db_engine_description: "MariaDB Community Edition".to_string(),
782            db_engine_version_description: "MariaDB 10.6.16".to_string(),
783            status: "available".to_string(),
784        },
785    ]
786}
787
788pub fn default_orderable_options() -> Vec<OrderableDbInstanceOption> {
789    let mut options = Vec::new();
790    let engines_and_versions = vec![
791        ("postgres", "16.3", "postgresql-license"),
792        ("postgres", "15.5", "postgresql-license"),
793        ("postgres", "14.10", "postgresql-license"),
794        ("postgres", "13.13", "postgresql-license"),
795        ("mysql", "8.0.35", "general-public-license"),
796        ("mysql", "8.0.28", "general-public-license"),
797        ("mysql", "5.7.44", "general-public-license"),
798        ("mariadb", "11.4.5", "general-public-license"),
799        ("mariadb", "10.11.6", "general-public-license"),
800        ("mariadb", "10.6.16", "general-public-license"),
801    ];
802
803    for (engine, version, license) in engines_and_versions {
804        for class in SUPPORTED_INSTANCE_CLASSES {
805            options.push(OrderableDbInstanceOption {
806                engine: engine.to_string(),
807                engine_version: version.to_string(),
808                db_instance_class: class.to_string(),
809                license_model: license.to_string(),
810                storage_type: "gp2".to_string(),
811                min_storage_size: 20,
812                max_storage_size: 16384,
813            });
814        }
815    }
816
817    options
818}
819
820pub fn default_parameter_groups(
821    account_id: &str,
822    region: &str,
823) -> BTreeMap<String, DbParameterGroup> {
824    let mut groups = BTreeMap::new();
825
826    let families = vec![
827        ("postgres16", "Default parameter group for postgres16"),
828        ("postgres15", "Default parameter group for postgres15"),
829        ("postgres14", "Default parameter group for postgres14"),
830        ("postgres13", "Default parameter group for postgres13"),
831        ("mysql8.0", "Default parameter group for mysql8.0"),
832        ("mysql5.7", "Default parameter group for mysql5.7"),
833        ("mariadb11.4", "Default parameter group for mariadb11.4"),
834        ("mariadb10.11", "Default parameter group for mariadb10.11"),
835        ("mariadb10.6", "Default parameter group for mariadb10.6"),
836        // Heavy-engine families. The names match what
837        // `service::default_parameter_group` returns so callers that
838        // omit `DBParameterGroupName` get a hit instead of a
839        // `DBParameterGroupNotFound`.
840        ("oracle-ee-23", "Default parameter group for oracle-ee-23"),
841        ("oracle-ee-21", "Default parameter group for oracle-ee-21"),
842        ("oracle-ee-19", "Default parameter group for oracle-ee-19"),
843        ("oracle-se2-23", "Default parameter group for oracle-se2-23"),
844        ("oracle-se2-21", "Default parameter group for oracle-se2-21"),
845        ("oracle-se2-19", "Default parameter group for oracle-se2-19"),
846        (
847            "oracle-ee-cdb-23",
848            "Default parameter group for oracle-ee-cdb-23",
849        ),
850        (
851            "oracle-se2-cdb-23",
852            "Default parameter group for oracle-se2-cdb-23",
853        ),
854        (
855            "sqlserver-ee-16",
856            "Default parameter group for sqlserver-ee-16",
857        ),
858        (
859            "sqlserver-ee-15",
860            "Default parameter group for sqlserver-ee-15",
861        ),
862        (
863            "sqlserver-se-16",
864            "Default parameter group for sqlserver-se-16",
865        ),
866        (
867            "sqlserver-se-15",
868            "Default parameter group for sqlserver-se-15",
869        ),
870        (
871            "sqlserver-ex-16",
872            "Default parameter group for sqlserver-ex-16",
873        ),
874        (
875            "sqlserver-ex-15",
876            "Default parameter group for sqlserver-ex-15",
877        ),
878        (
879            "sqlserver-web-16",
880            "Default parameter group for sqlserver-web-16",
881        ),
882        (
883            "sqlserver-web-15",
884            "Default parameter group for sqlserver-web-15",
885        ),
886        ("db2-se-11.5", "Default parameter group for db2-se-11.5"),
887        ("db2-ae-11.5", "Default parameter group for db2-ae-11.5"),
888    ];
889
890    for (family, description) in families {
891        let group_name = format!("default.{}", family);
892        let group = DbParameterGroup {
893            db_parameter_group_name: group_name.clone(),
894            db_parameter_group_arn: Arn::new(
895                "rds",
896                region,
897                account_id,
898                &format!("pg:{group_name}"),
899            )
900            .to_string(),
901            db_parameter_group_family: family.to_string(),
902            description: description.to_string(),
903            parameters: BTreeMap::new(),
904            parameter_apply_methods: BTreeMap::new(),
905            tags: Vec::new(),
906        };
907        groups.insert(group_name, group);
908    }
909
910    groups
911}
912
913pub const RDS_SNAPSHOT_SCHEMA_VERSION: u32 = 2;
914
915#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
916pub struct RdsSnapshot {
917    pub schema_version: u32,
918    #[serde(default)]
919    pub accounts: Option<fakecloud_core::multi_account::MultiAccountState<RdsState>>,
920    #[serde(default)]
921    pub state: Option<RdsState>,
922}
923
924#[cfg(test)]
925mod tests {
926    use chrono::Utc;
927
928    use super::{
929        default_engine_versions, default_orderable_options, default_parameter_groups, Arn,
930        DbInstance, RdsState,
931    };
932
933    #[test]
934    fn new_initializes_account_and_region() {
935        let state = RdsState::new("123456789012", "us-east-1");
936
937        assert_eq!(state.account_id, "123456789012");
938        assert_eq!(state.region, "us-east-1");
939        assert!(state.instances.is_empty());
940        assert!(state.in_progress_instance_ids.is_empty());
941    }
942
943    #[test]
944    fn reset_clears_instances() {
945        let mut state = RdsState::new("123456789012", "us-east-1");
946        let created_at = Utc::now();
947        state.instances.insert(
948            "db-1".to_string(),
949            DbInstance {
950                db_instance_identifier: "db-1".to_string(),
951                db_instance_arn: "arn:aws:rds:us-east-1:123456789012:db:db-1".to_string(),
952                db_instance_class: "db.t3.micro".to_string(),
953                engine: "postgres".to_string(),
954                engine_version: "16.3".to_string(),
955                db_instance_status: "available".to_string(),
956                master_username: "admin".to_string(),
957                db_name: Some("postgres".to_string()),
958                endpoint_address: "127.0.0.1".to_string(),
959                port: 5432,
960                allocated_storage: 20,
961                publicly_accessible: true,
962                deletion_protection: false,
963                created_at,
964                dbi_resource_id: "db-test".to_string(),
965                master_user_password: "secret123".to_string(),
966                container_id: "container-id".to_string(),
967                host_port: 15432,
968                tags: Vec::new(),
969                read_replica_source_db_instance_identifier: None,
970                read_replica_db_instance_identifiers: Vec::new(),
971                vpc_security_group_ids: Vec::new(),
972                db_parameter_group_name: None,
973                backup_retention_period: 1,
974                preferred_backup_window: "03:00-04:00".to_string(),
975                preferred_maintenance_window: None,
976                latest_restorable_time: Some(created_at),
977                option_group_name: None,
978                multi_az: false,
979                pending_modified_values: None,
980                availability_zone: None,
981                storage_type: None,
982                storage_encrypted: false,
983                kms_key_id: None,
984                iam_database_authentication_enabled: false,
985                iops: None,
986                monitoring_interval: None,
987                monitoring_role_arn: None,
988                performance_insights_enabled: false,
989                performance_insights_kms_key_id: None,
990                performance_insights_retention_period: None,
991                enabled_cloudwatch_logs_exports: Vec::new(),
992                ca_certificate_identifier: None,
993                network_type: None,
994                character_set_name: None,
995                auto_minor_version_upgrade: None,
996                copy_tags_to_snapshot: None,
997                master_user_secret_arn: None,
998                master_user_secret_kms_key_id: None,
999                license_model: None,
1000                max_allocated_storage: None,
1001                multi_tenant: None,
1002                storage_throughput: None,
1003                tde_credential_arn: None,
1004                delete_automated_backups: None,
1005                db_security_groups: Vec::new(),
1006                domain: None,
1007                domain_fqdn: None,
1008                domain_ou: None,
1009                domain_iam_role_name: None,
1010                domain_auth_secret_arn: None,
1011                domain_dns_ips: Vec::new(),
1012                db_cluster_identifier: None,
1013                activity_stream: None,
1014            },
1015        );
1016
1017        state.reset();
1018
1019        assert!(state.instances.is_empty());
1020        assert!(state.in_progress_instance_ids.is_empty());
1021    }
1022
1023    #[test]
1024    fn default_engine_versions_are_postgres_metadata() {
1025        let versions = default_engine_versions();
1026
1027        assert_eq!(versions.len(), 10); // 4 postgres + 3 mysql + 3 mariadb
1028                                        // Check first postgres version
1029        assert_eq!(versions[0].engine, "postgres");
1030        assert_eq!(versions[0].engine_version, "16.3");
1031        assert_eq!(versions[0].db_parameter_group_family, "postgres16");
1032    }
1033
1034    #[test]
1035    fn default_orderable_options_match_engine_versions() {
1036        let versions = default_engine_versions();
1037        let options = default_orderable_options();
1038
1039        assert_eq!(options.len(), 70); // 10 versions * 7 instance classes
1040                                       // Verify all engines and versions have orderable options
1041        for version in &versions {
1042            assert!(options.iter().any(|opt| {
1043                opt.engine == version.engine && opt.engine_version == version.engine_version
1044            }));
1045        }
1046    }
1047
1048    #[test]
1049    fn begin_instance_creation_rejects_duplicate_identifiers() {
1050        let mut state = RdsState::new("123456789012", "us-east-1");
1051
1052        assert!(state.begin_instance_creation("db-1"));
1053        assert!(!state.begin_instance_creation("db-1"));
1054
1055        state.cancel_instance_creation("db-1");
1056        assert!(state.begin_instance_creation("db-1"));
1057    }
1058
1059    #[test]
1060    fn arn_helpers_format_correctly() {
1061        let state = RdsState::new("123456789012", "eu-west-1");
1062        assert!(state.db_instance_arn("mydb").contains(":db:mydb"));
1063        assert!(state.db_snapshot_arn("snap1").contains(":snapshot:snap1"));
1064        assert!(state.db_subnet_group_arn("sng").contains("sng"));
1065        assert!(state.db_parameter_group_arn("pg").contains("pg"));
1066    }
1067
1068    #[test]
1069    fn next_dbi_resource_id_format() {
1070        let state = RdsState::new("123456789012", "us-east-1");
1071        let id = state.next_dbi_resource_id();
1072        assert!(id.starts_with("db-"));
1073        assert!(id.len() > 3);
1074    }
1075
1076    #[test]
1077    fn default_engine_versions_list_not_empty() {
1078        let versions = default_engine_versions();
1079        assert!(!versions.is_empty());
1080    }
1081
1082    #[test]
1083    fn default_orderable_options_list_not_empty() {
1084        let opts = default_orderable_options();
1085        assert!(!opts.is_empty());
1086    }
1087
1088    #[test]
1089    fn default_parameter_groups_returned_per_family() {
1090        let groups = default_parameter_groups("123456789012", "us-east-1");
1091        assert!(!groups.is_empty());
1092    }
1093
1094    fn make_instance(id: &str) -> DbInstance {
1095        let created_at = Utc::now();
1096        DbInstance {
1097            db_instance_identifier: id.to_string(),
1098            db_instance_arn: Arn::new("rds", "us-east-1", "123", &format!("db:{id}")).to_string(),
1099            db_instance_class: "db.t3.micro".to_string(),
1100            engine: "postgres".to_string(),
1101            engine_version: "16.3".to_string(),
1102            db_instance_status: "available".to_string(),
1103            master_username: "admin".to_string(),
1104            db_name: None,
1105            endpoint_address: "x".to_string(),
1106            port: 5432,
1107            allocated_storage: 20,
1108            publicly_accessible: false,
1109            deletion_protection: false,
1110            created_at,
1111            dbi_resource_id: "d".to_string(),
1112            master_user_password: "p".to_string(),
1113            container_id: "c".to_string(),
1114            host_port: 0,
1115            tags: Vec::new(),
1116            read_replica_source_db_instance_identifier: None,
1117            read_replica_db_instance_identifiers: Vec::new(),
1118            vpc_security_group_ids: Vec::new(),
1119            db_parameter_group_name: None,
1120            backup_retention_period: 0,
1121            preferred_backup_window: String::new(),
1122            preferred_maintenance_window: None,
1123            latest_restorable_time: None,
1124            option_group_name: None,
1125            multi_az: false,
1126            pending_modified_values: None,
1127            availability_zone: None,
1128            storage_type: None,
1129            storage_encrypted: false,
1130            kms_key_id: None,
1131            iam_database_authentication_enabled: false,
1132            iops: None,
1133            monitoring_interval: None,
1134            monitoring_role_arn: None,
1135            performance_insights_enabled: false,
1136            performance_insights_kms_key_id: None,
1137            performance_insights_retention_period: None,
1138            enabled_cloudwatch_logs_exports: Vec::new(),
1139            ca_certificate_identifier: None,
1140            network_type: None,
1141            character_set_name: None,
1142            auto_minor_version_upgrade: None,
1143            copy_tags_to_snapshot: None,
1144            master_user_secret_arn: None,
1145            master_user_secret_kms_key_id: None,
1146            license_model: None,
1147            max_allocated_storage: None,
1148            multi_tenant: None,
1149            storage_throughput: None,
1150            tde_credential_arn: None,
1151            delete_automated_backups: None,
1152            db_security_groups: Vec::new(),
1153            domain: None,
1154            domain_fqdn: None,
1155            domain_ou: None,
1156            domain_iam_role_name: None,
1157            domain_auth_secret_arn: None,
1158            domain_dns_ips: Vec::new(),
1159            db_cluster_identifier: None,
1160            activity_stream: None,
1161        }
1162    }
1163
1164    #[test]
1165    fn finish_instance_creation_moves_from_pending_to_instances() {
1166        let mut state = RdsState::new("123456789012", "us-east-1");
1167        assert!(state.begin_instance_creation("db-x"));
1168        assert!(state.in_progress_instance_ids.contains("db-x"));
1169        state.finish_instance_creation(make_instance("db-x"));
1170        assert!(!state.in_progress_instance_ids.contains("db-x"));
1171        assert!(state.instances.contains_key("db-x"));
1172    }
1173
1174    #[test]
1175    fn cancel_instance_creation_drops_pending() {
1176        let mut state = RdsState::new("123456789012", "us-east-1");
1177        state.begin_instance_creation("db-y");
1178        state.cancel_instance_creation("db-y");
1179        assert!(!state.in_progress_instance_ids.contains("db-y"));
1180    }
1181
1182    #[test]
1183    fn begin_instance_creation_rejects_when_already_created() {
1184        let mut state = RdsState::new("123456789012", "us-east-1");
1185        state
1186            .instances
1187            .insert("db-z".to_string(), make_instance("db-z"));
1188        assert!(!state.begin_instance_creation("db-z"));
1189    }
1190
1191    #[test]
1192    fn reset_restores_default_parameter_groups() {
1193        let mut state = RdsState::new("123456789012", "us-east-1");
1194        state.parameter_groups.clear();
1195        state.reset();
1196        assert!(!state.parameter_groups.is_empty());
1197    }
1198
1199    #[test]
1200    fn arn_helpers_include_region_and_account() {
1201        let state = RdsState::new("111122223333", "ap-southeast-2");
1202        let arn = state.db_instance_arn("my-db");
1203        assert!(arn.contains("111122223333"));
1204        assert!(arn.contains("ap-southeast-2"));
1205        let snap = state.db_snapshot_arn("snap");
1206        assert!(snap.contains("snapshot:snap"));
1207    }
1208
1209    #[test]
1210    fn next_dbi_resource_id_unique_across_calls() {
1211        let state = RdsState::new("123", "us-east-1");
1212        let a = state.next_dbi_resource_id();
1213        let b = state.next_dbi_resource_id();
1214        assert_ne!(a, b);
1215    }
1216}