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