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    /// Per-parameter `ApplyMethod` (`immediate` | `pending-reboot`),
417    /// keyed by parameter name. Defaulted for snapshots written before
418    /// this field existed; a missing entry reads back as `immediate`.
419    #[serde(default)]
420    pub parameter_apply_methods: BTreeMap<String, String>,
421    pub tags: Vec<RdsTag>,
422}
423
424/// Static metadata for an engine-default parameter, used by
425/// `DescribeDBParameters`/`DescribeDBClusterParameters`/`DescribeEngineDefaultParameters`
426/// to surface a baseline set of parameters when no user override exists.
427///
428/// The seed below is intentionally small (a handful of common knobs per
429/// engine family). Real RDS exposes hundreds of parameters per family;
430/// callers needing comprehensive coverage should add entries to
431/// [`engine_default_parameters`] as needs arise.
432#[derive(Debug, Clone, PartialEq, Eq)]
433pub struct EngineDefaultParameter {
434    pub name: &'static str,
435    pub value: &'static str,
436    pub apply_type: &'static str,
437    pub data_type: &'static str,
438    pub allowed_values: &'static str,
439    pub is_modifiable: bool,
440}
441
442/// Return a small, representative set of engine-default parameters for the
443/// given parameter group family (e.g. `postgres16`, `mysql8.0`,
444/// `aurora-postgresql15`). The list is not comprehensive — real RDS
445/// exposes hundreds of parameters; we ship just enough to make callers
446/// that round-trip `DescribeDBParameters` with `Source=engine-default`
447/// see meaningful entries. Unknown families fall through to an empty list.
448pub fn engine_default_parameters(family: &str) -> &'static [EngineDefaultParameter] {
449    if family.starts_with("postgres") || family.starts_with("aurora-postgresql") {
450        POSTGRES_DEFAULT_PARAMETERS
451    } else if family.starts_with("mysql") || family.starts_with("aurora-mysql") {
452        MYSQL_DEFAULT_PARAMETERS
453    } else if family.starts_with("mariadb") {
454        MARIADB_DEFAULT_PARAMETERS
455    } else {
456        &[]
457    }
458}
459
460const POSTGRES_DEFAULT_PARAMETERS: &[EngineDefaultParameter] = &[
461    EngineDefaultParameter {
462        name: "max_connections",
463        value: "LEAST({DBInstanceClassMemory/9531392},5000)",
464        apply_type: "static",
465        data_type: "integer",
466        allowed_values: "6-8388607",
467        is_modifiable: true,
468    },
469    EngineDefaultParameter {
470        name: "shared_buffers",
471        value: "{DBInstanceClassMemory/32768}",
472        apply_type: "static",
473        data_type: "integer",
474        allowed_values: "16-1073741823",
475        is_modifiable: true,
476    },
477    EngineDefaultParameter {
478        name: "work_mem",
479        value: "4096",
480        apply_type: "dynamic",
481        data_type: "integer",
482        allowed_values: "64-2147483647",
483        is_modifiable: true,
484    },
485    EngineDefaultParameter {
486        name: "maintenance_work_mem",
487        value: "GREATEST({DBInstanceClassMemory/63963136*1024},65536)",
488        apply_type: "dynamic",
489        data_type: "integer",
490        allowed_values: "1024-2147483647",
491        is_modifiable: true,
492    },
493    EngineDefaultParameter {
494        name: "effective_cache_size",
495        value: "{DBInstanceClassMemory/16384}",
496        apply_type: "dynamic",
497        data_type: "integer",
498        allowed_values: "1-2147483647",
499        is_modifiable: true,
500    },
501];
502
503const MYSQL_DEFAULT_PARAMETERS: &[EngineDefaultParameter] = &[
504    EngineDefaultParameter {
505        name: "max_connections",
506        value: "{DBInstanceClassMemory/12582880}",
507        apply_type: "dynamic",
508        data_type: "integer",
509        allowed_values: "1-100000",
510        is_modifiable: true,
511    },
512    EngineDefaultParameter {
513        name: "innodb_buffer_pool_size",
514        value: "{DBInstanceClassMemory*3/4}",
515        apply_type: "static",
516        data_type: "integer",
517        allowed_values: "5242880-2147483648",
518        is_modifiable: true,
519    },
520    EngineDefaultParameter {
521        name: "max_allowed_packet",
522        value: "67108864",
523        apply_type: "dynamic",
524        data_type: "integer",
525        allowed_values: "1024-1073741824",
526        is_modifiable: true,
527    },
528    EngineDefaultParameter {
529        name: "character_set_server",
530        value: "utf8mb4",
531        apply_type: "dynamic",
532        data_type: "string",
533        allowed_values: "utf8,utf8mb4,latin1",
534        is_modifiable: true,
535    },
536];
537
538const MARIADB_DEFAULT_PARAMETERS: &[EngineDefaultParameter] = &[
539    EngineDefaultParameter {
540        name: "max_connections",
541        value: "{DBInstanceClassMemory/12582880}",
542        apply_type: "dynamic",
543        data_type: "integer",
544        allowed_values: "1-100000",
545        is_modifiable: true,
546    },
547    EngineDefaultParameter {
548        name: "innodb_buffer_pool_size",
549        value: "{DBInstanceClassMemory*3/4}",
550        apply_type: "static",
551        data_type: "integer",
552        allowed_values: "5242880-2147483648",
553        is_modifiable: true,
554    },
555    EngineDefaultParameter {
556        name: "max_allowed_packet",
557        value: "67108864",
558        apply_type: "dynamic",
559        data_type: "integer",
560        allowed_values: "1024-1073741824",
561        is_modifiable: true,
562    },
563];
564
565impl RdsState {
566    pub fn new(account_id: &str, region: &str) -> Self {
567        Self {
568            account_id: account_id.to_string(),
569            region: region.to_string(),
570            instances: BTreeMap::new(),
571            in_progress_instance_ids: HashSet::new(),
572            snapshots: BTreeMap::new(),
573            subnet_groups: BTreeMap::new(),
574            parameter_groups: default_parameter_groups(account_id, region),
575            extras: BTreeMap::new(),
576            events: Vec::new(),
577            default_certificate_identifier: None,
578        }
579    }
580
581    pub fn reset(&mut self) {
582        self.instances.clear();
583        self.in_progress_instance_ids.clear();
584        self.snapshots.clear();
585        self.subnet_groups.clear();
586        self.parameter_groups = default_parameter_groups(&self.account_id, &self.region);
587        self.extras.clear();
588        self.events.clear();
589        self.default_certificate_identifier = None;
590    }
591
592    /// Append an event row to the in-memory ring, dropping the oldest
593    /// entries beyond a 14-day window (matching real RDS retention).
594    pub fn push_event(&mut self, event: RdsEventRecord) {
595        const RETENTION_DAYS: i64 = 14;
596        let cutoff = chrono::Utc::now() - chrono::Duration::days(RETENTION_DAYS);
597        self.events.retain(|e| e.date >= cutoff);
598        self.events.push(event);
599    }
600
601    pub fn db_instance_arn(&self, db_instance_identifier: &str) -> String {
602        Arn::new(
603            "rds",
604            &self.region,
605            &self.account_id,
606            &format!("db:{db_instance_identifier}"),
607        )
608        .to_string()
609    }
610
611    pub fn db_snapshot_arn(&self, db_snapshot_identifier: &str) -> String {
612        Arn::new(
613            "rds",
614            &self.region,
615            &self.account_id,
616            &format!("snapshot:{db_snapshot_identifier}"),
617        )
618        .to_string()
619    }
620
621    pub fn db_subnet_group_arn(&self, db_subnet_group_name: &str) -> String {
622        Arn::new(
623            "rds",
624            &self.region,
625            &self.account_id,
626            &format!("subgrp:{db_subnet_group_name}"),
627        )
628        .to_string()
629    }
630
631    pub fn db_parameter_group_arn(&self, db_parameter_group_name: &str) -> String {
632        Arn::new(
633            "rds",
634            &self.region,
635            &self.account_id,
636            &format!("pg:{db_parameter_group_name}"),
637        )
638        .to_string()
639    }
640
641    pub fn next_dbi_resource_id(&self) -> String {
642        format!("db-{}", Uuid::new_v4().simple())
643    }
644
645    pub fn begin_instance_creation(&mut self, db_instance_identifier: &str) -> bool {
646        if self.instances.contains_key(db_instance_identifier)
647            || self
648                .in_progress_instance_ids
649                .contains(db_instance_identifier)
650        {
651            return false;
652        }
653
654        self.in_progress_instance_ids
655            .insert(db_instance_identifier.to_string());
656        true
657    }
658
659    pub fn finish_instance_creation(&mut self, instance: DbInstance) {
660        self.in_progress_instance_ids
661            .remove(&instance.db_instance_identifier);
662        self.instances
663            .insert(instance.db_instance_identifier.clone(), instance);
664    }
665
666    pub fn cancel_instance_creation(&mut self, db_instance_identifier: &str) {
667        self.in_progress_instance_ids.remove(db_instance_identifier);
668    }
669}
670
671pub fn default_engine_versions() -> Vec<EngineVersionInfo> {
672    vec![
673        // PostgreSQL versions
674        EngineVersionInfo {
675            engine: "postgres".to_string(),
676            engine_version: "16.3".to_string(),
677            db_parameter_group_family: "postgres16".to_string(),
678            db_engine_description: "PostgreSQL".to_string(),
679            db_engine_version_description: "PostgreSQL 16.3".to_string(),
680            status: "available".to_string(),
681        },
682        EngineVersionInfo {
683            engine: "postgres".to_string(),
684            engine_version: "15.5".to_string(),
685            db_parameter_group_family: "postgres15".to_string(),
686            db_engine_description: "PostgreSQL".to_string(),
687            db_engine_version_description: "PostgreSQL 15.5".to_string(),
688            status: "available".to_string(),
689        },
690        EngineVersionInfo {
691            engine: "postgres".to_string(),
692            engine_version: "14.10".to_string(),
693            db_parameter_group_family: "postgres14".to_string(),
694            db_engine_description: "PostgreSQL".to_string(),
695            db_engine_version_description: "PostgreSQL 14.10".to_string(),
696            status: "available".to_string(),
697        },
698        EngineVersionInfo {
699            engine: "postgres".to_string(),
700            engine_version: "13.13".to_string(),
701            db_parameter_group_family: "postgres13".to_string(),
702            db_engine_description: "PostgreSQL".to_string(),
703            db_engine_version_description: "PostgreSQL 13.13".to_string(),
704            status: "available".to_string(),
705        },
706        // MySQL versions
707        EngineVersionInfo {
708            engine: "mysql".to_string(),
709            engine_version: "8.0.35".to_string(),
710            db_parameter_group_family: "mysql8.0".to_string(),
711            db_engine_description: "MySQL Community Edition".to_string(),
712            db_engine_version_description: "MySQL 8.0.35".to_string(),
713            status: "available".to_string(),
714        },
715        EngineVersionInfo {
716            engine: "mysql".to_string(),
717            engine_version: "8.0.28".to_string(),
718            db_parameter_group_family: "mysql8.0".to_string(),
719            db_engine_description: "MySQL Community Edition".to_string(),
720            db_engine_version_description: "MySQL 8.0.28".to_string(),
721            status: "available".to_string(),
722        },
723        EngineVersionInfo {
724            engine: "mysql".to_string(),
725            engine_version: "5.7.44".to_string(),
726            db_parameter_group_family: "mysql5.7".to_string(),
727            db_engine_description: "MySQL Community Edition".to_string(),
728            db_engine_version_description: "MySQL 5.7.44".to_string(),
729            status: "available".to_string(),
730        },
731        // MariaDB versions
732        EngineVersionInfo {
733            engine: "mariadb".to_string(),
734            engine_version: "11.4.5".to_string(),
735            db_parameter_group_family: "mariadb11.4".to_string(),
736            db_engine_description: "MariaDB Community Edition".to_string(),
737            db_engine_version_description: "MariaDB 11.4.5".to_string(),
738            status: "available".to_string(),
739        },
740        EngineVersionInfo {
741            engine: "mariadb".to_string(),
742            engine_version: "10.11.6".to_string(),
743            db_parameter_group_family: "mariadb10.11".to_string(),
744            db_engine_description: "MariaDB Community Edition".to_string(),
745            db_engine_version_description: "MariaDB 10.11.6".to_string(),
746            status: "available".to_string(),
747        },
748        EngineVersionInfo {
749            engine: "mariadb".to_string(),
750            engine_version: "10.6.16".to_string(),
751            db_parameter_group_family: "mariadb10.6".to_string(),
752            db_engine_description: "MariaDB Community Edition".to_string(),
753            db_engine_version_description: "MariaDB 10.6.16".to_string(),
754            status: "available".to_string(),
755        },
756    ]
757}
758
759pub fn default_orderable_options() -> Vec<OrderableDbInstanceOption> {
760    let mut options = Vec::new();
761    let engines_and_versions = vec![
762        ("postgres", "16.3", "postgresql-license"),
763        ("postgres", "15.5", "postgresql-license"),
764        ("postgres", "14.10", "postgresql-license"),
765        ("postgres", "13.13", "postgresql-license"),
766        ("mysql", "8.0.35", "general-public-license"),
767        ("mysql", "8.0.28", "general-public-license"),
768        ("mysql", "5.7.44", "general-public-license"),
769        ("mariadb", "11.4.5", "general-public-license"),
770        ("mariadb", "10.11.6", "general-public-license"),
771        ("mariadb", "10.6.16", "general-public-license"),
772    ];
773
774    for (engine, version, license) in engines_and_versions {
775        for class in SUPPORTED_INSTANCE_CLASSES {
776            options.push(OrderableDbInstanceOption {
777                engine: engine.to_string(),
778                engine_version: version.to_string(),
779                db_instance_class: class.to_string(),
780                license_model: license.to_string(),
781                storage_type: "gp2".to_string(),
782                min_storage_size: 20,
783                max_storage_size: 16384,
784            });
785        }
786    }
787
788    options
789}
790
791pub fn default_parameter_groups(
792    account_id: &str,
793    region: &str,
794) -> BTreeMap<String, DbParameterGroup> {
795    let mut groups = BTreeMap::new();
796
797    let families = vec![
798        ("postgres16", "Default parameter group for postgres16"),
799        ("postgres15", "Default parameter group for postgres15"),
800        ("postgres14", "Default parameter group for postgres14"),
801        ("postgres13", "Default parameter group for postgres13"),
802        ("mysql8.0", "Default parameter group for mysql8.0"),
803        ("mysql5.7", "Default parameter group for mysql5.7"),
804        ("mariadb11.4", "Default parameter group for mariadb11.4"),
805        ("mariadb10.11", "Default parameter group for mariadb10.11"),
806        ("mariadb10.6", "Default parameter group for mariadb10.6"),
807        // Heavy-engine families. The names match what
808        // `service::default_parameter_group` returns so callers that
809        // omit `DBParameterGroupName` get a hit instead of a
810        // `DBParameterGroupNotFound`.
811        ("oracle-ee-23", "Default parameter group for oracle-ee-23"),
812        ("oracle-ee-21", "Default parameter group for oracle-ee-21"),
813        ("oracle-ee-19", "Default parameter group for oracle-ee-19"),
814        ("oracle-se2-23", "Default parameter group for oracle-se2-23"),
815        ("oracle-se2-21", "Default parameter group for oracle-se2-21"),
816        ("oracle-se2-19", "Default parameter group for oracle-se2-19"),
817        (
818            "oracle-ee-cdb-23",
819            "Default parameter group for oracle-ee-cdb-23",
820        ),
821        (
822            "oracle-se2-cdb-23",
823            "Default parameter group for oracle-se2-cdb-23",
824        ),
825        (
826            "sqlserver-ee-16",
827            "Default parameter group for sqlserver-ee-16",
828        ),
829        (
830            "sqlserver-ee-15",
831            "Default parameter group for sqlserver-ee-15",
832        ),
833        (
834            "sqlserver-se-16",
835            "Default parameter group for sqlserver-se-16",
836        ),
837        (
838            "sqlserver-se-15",
839            "Default parameter group for sqlserver-se-15",
840        ),
841        (
842            "sqlserver-ex-16",
843            "Default parameter group for sqlserver-ex-16",
844        ),
845        (
846            "sqlserver-ex-15",
847            "Default parameter group for sqlserver-ex-15",
848        ),
849        (
850            "sqlserver-web-16",
851            "Default parameter group for sqlserver-web-16",
852        ),
853        (
854            "sqlserver-web-15",
855            "Default parameter group for sqlserver-web-15",
856        ),
857        ("db2-se-11.5", "Default parameter group for db2-se-11.5"),
858        ("db2-ae-11.5", "Default parameter group for db2-ae-11.5"),
859    ];
860
861    for (family, description) in families {
862        let group_name = format!("default.{}", family);
863        let group = DbParameterGroup {
864            db_parameter_group_name: group_name.clone(),
865            db_parameter_group_arn: Arn::new(
866                "rds",
867                region,
868                account_id,
869                &format!("pg:{group_name}"),
870            )
871            .to_string(),
872            db_parameter_group_family: family.to_string(),
873            description: description.to_string(),
874            parameters: BTreeMap::new(),
875            parameter_apply_methods: BTreeMap::new(),
876            tags: Vec::new(),
877        };
878        groups.insert(group_name, group);
879    }
880
881    groups
882}
883
884pub const RDS_SNAPSHOT_SCHEMA_VERSION: u32 = 2;
885
886#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
887pub struct RdsSnapshot {
888    pub schema_version: u32,
889    #[serde(default)]
890    pub accounts: Option<fakecloud_core::multi_account::MultiAccountState<RdsState>>,
891    #[serde(default)]
892    pub state: Option<RdsState>,
893}
894
895#[cfg(test)]
896mod tests {
897    use chrono::Utc;
898
899    use super::{
900        default_engine_versions, default_orderable_options, default_parameter_groups, Arn,
901        DbInstance, RdsState,
902    };
903
904    #[test]
905    fn new_initializes_account_and_region() {
906        let state = RdsState::new("123456789012", "us-east-1");
907
908        assert_eq!(state.account_id, "123456789012");
909        assert_eq!(state.region, "us-east-1");
910        assert!(state.instances.is_empty());
911        assert!(state.in_progress_instance_ids.is_empty());
912    }
913
914    #[test]
915    fn reset_clears_instances() {
916        let mut state = RdsState::new("123456789012", "us-east-1");
917        let created_at = Utc::now();
918        state.instances.insert(
919            "db-1".to_string(),
920            DbInstance {
921                db_instance_identifier: "db-1".to_string(),
922                db_instance_arn: "arn:aws:rds:us-east-1:123456789012:db:db-1".to_string(),
923                db_instance_class: "db.t3.micro".to_string(),
924                engine: "postgres".to_string(),
925                engine_version: "16.3".to_string(),
926                db_instance_status: "available".to_string(),
927                master_username: "admin".to_string(),
928                db_name: Some("postgres".to_string()),
929                endpoint_address: "127.0.0.1".to_string(),
930                port: 5432,
931                allocated_storage: 20,
932                publicly_accessible: true,
933                deletion_protection: false,
934                created_at,
935                dbi_resource_id: "db-test".to_string(),
936                master_user_password: "secret123".to_string(),
937                container_id: "container-id".to_string(),
938                host_port: 15432,
939                tags: Vec::new(),
940                read_replica_source_db_instance_identifier: None,
941                read_replica_db_instance_identifiers: Vec::new(),
942                vpc_security_group_ids: Vec::new(),
943                db_parameter_group_name: None,
944                backup_retention_period: 1,
945                preferred_backup_window: "03:00-04:00".to_string(),
946                preferred_maintenance_window: None,
947                latest_restorable_time: Some(created_at),
948                option_group_name: None,
949                multi_az: false,
950                pending_modified_values: None,
951                availability_zone: None,
952                storage_type: None,
953                storage_encrypted: false,
954                kms_key_id: None,
955                iam_database_authentication_enabled: false,
956                iops: None,
957                monitoring_interval: None,
958                monitoring_role_arn: None,
959                performance_insights_enabled: false,
960                performance_insights_kms_key_id: None,
961                performance_insights_retention_period: None,
962                enabled_cloudwatch_logs_exports: Vec::new(),
963                ca_certificate_identifier: None,
964                network_type: None,
965                character_set_name: None,
966                auto_minor_version_upgrade: None,
967                copy_tags_to_snapshot: None,
968                master_user_secret_arn: None,
969                master_user_secret_kms_key_id: None,
970                license_model: None,
971                max_allocated_storage: None,
972                multi_tenant: None,
973                storage_throughput: None,
974                tde_credential_arn: None,
975                delete_automated_backups: None,
976                db_security_groups: Vec::new(),
977                domain: None,
978                domain_fqdn: None,
979                domain_ou: None,
980                domain_iam_role_name: None,
981                domain_auth_secret_arn: None,
982                domain_dns_ips: Vec::new(),
983                db_cluster_identifier: None,
984            },
985        );
986
987        state.reset();
988
989        assert!(state.instances.is_empty());
990        assert!(state.in_progress_instance_ids.is_empty());
991    }
992
993    #[test]
994    fn default_engine_versions_are_postgres_metadata() {
995        let versions = default_engine_versions();
996
997        assert_eq!(versions.len(), 10); // 4 postgres + 3 mysql + 3 mariadb
998                                        // Check first postgres version
999        assert_eq!(versions[0].engine, "postgres");
1000        assert_eq!(versions[0].engine_version, "16.3");
1001        assert_eq!(versions[0].db_parameter_group_family, "postgres16");
1002    }
1003
1004    #[test]
1005    fn default_orderable_options_match_engine_versions() {
1006        let versions = default_engine_versions();
1007        let options = default_orderable_options();
1008
1009        assert_eq!(options.len(), 70); // 10 versions * 7 instance classes
1010                                       // Verify all engines and versions have orderable options
1011        for version in &versions {
1012            assert!(options.iter().any(|opt| {
1013                opt.engine == version.engine && opt.engine_version == version.engine_version
1014            }));
1015        }
1016    }
1017
1018    #[test]
1019    fn begin_instance_creation_rejects_duplicate_identifiers() {
1020        let mut state = RdsState::new("123456789012", "us-east-1");
1021
1022        assert!(state.begin_instance_creation("db-1"));
1023        assert!(!state.begin_instance_creation("db-1"));
1024
1025        state.cancel_instance_creation("db-1");
1026        assert!(state.begin_instance_creation("db-1"));
1027    }
1028
1029    #[test]
1030    fn arn_helpers_format_correctly() {
1031        let state = RdsState::new("123456789012", "eu-west-1");
1032        assert!(state.db_instance_arn("mydb").contains(":db:mydb"));
1033        assert!(state.db_snapshot_arn("snap1").contains(":snapshot:snap1"));
1034        assert!(state.db_subnet_group_arn("sng").contains("sng"));
1035        assert!(state.db_parameter_group_arn("pg").contains("pg"));
1036    }
1037
1038    #[test]
1039    fn next_dbi_resource_id_format() {
1040        let state = RdsState::new("123456789012", "us-east-1");
1041        let id = state.next_dbi_resource_id();
1042        assert!(id.starts_with("db-"));
1043        assert!(id.len() > 3);
1044    }
1045
1046    #[test]
1047    fn default_engine_versions_list_not_empty() {
1048        let versions = default_engine_versions();
1049        assert!(!versions.is_empty());
1050    }
1051
1052    #[test]
1053    fn default_orderable_options_list_not_empty() {
1054        let opts = default_orderable_options();
1055        assert!(!opts.is_empty());
1056    }
1057
1058    #[test]
1059    fn default_parameter_groups_returned_per_family() {
1060        let groups = default_parameter_groups("123456789012", "us-east-1");
1061        assert!(!groups.is_empty());
1062    }
1063
1064    fn make_instance(id: &str) -> DbInstance {
1065        let created_at = Utc::now();
1066        DbInstance {
1067            db_instance_identifier: id.to_string(),
1068            db_instance_arn: Arn::new("rds", "us-east-1", "123", &format!("db:{id}")).to_string(),
1069            db_instance_class: "db.t3.micro".to_string(),
1070            engine: "postgres".to_string(),
1071            engine_version: "16.3".to_string(),
1072            db_instance_status: "available".to_string(),
1073            master_username: "admin".to_string(),
1074            db_name: None,
1075            endpoint_address: "x".to_string(),
1076            port: 5432,
1077            allocated_storage: 20,
1078            publicly_accessible: false,
1079            deletion_protection: false,
1080            created_at,
1081            dbi_resource_id: "d".to_string(),
1082            master_user_password: "p".to_string(),
1083            container_id: "c".to_string(),
1084            host_port: 0,
1085            tags: Vec::new(),
1086            read_replica_source_db_instance_identifier: None,
1087            read_replica_db_instance_identifiers: Vec::new(),
1088            vpc_security_group_ids: Vec::new(),
1089            db_parameter_group_name: None,
1090            backup_retention_period: 0,
1091            preferred_backup_window: String::new(),
1092            preferred_maintenance_window: None,
1093            latest_restorable_time: None,
1094            option_group_name: None,
1095            multi_az: false,
1096            pending_modified_values: None,
1097            availability_zone: None,
1098            storage_type: None,
1099            storage_encrypted: false,
1100            kms_key_id: None,
1101            iam_database_authentication_enabled: false,
1102            iops: None,
1103            monitoring_interval: None,
1104            monitoring_role_arn: None,
1105            performance_insights_enabled: false,
1106            performance_insights_kms_key_id: None,
1107            performance_insights_retention_period: None,
1108            enabled_cloudwatch_logs_exports: Vec::new(),
1109            ca_certificate_identifier: None,
1110            network_type: None,
1111            character_set_name: None,
1112            auto_minor_version_upgrade: None,
1113            copy_tags_to_snapshot: None,
1114            master_user_secret_arn: None,
1115            master_user_secret_kms_key_id: None,
1116            license_model: None,
1117            max_allocated_storage: None,
1118            multi_tenant: None,
1119            storage_throughput: None,
1120            tde_credential_arn: None,
1121            delete_automated_backups: None,
1122            db_security_groups: Vec::new(),
1123            domain: None,
1124            domain_fqdn: None,
1125            domain_ou: None,
1126            domain_iam_role_name: None,
1127            domain_auth_secret_arn: None,
1128            domain_dns_ips: Vec::new(),
1129            db_cluster_identifier: None,
1130        }
1131    }
1132
1133    #[test]
1134    fn finish_instance_creation_moves_from_pending_to_instances() {
1135        let mut state = RdsState::new("123456789012", "us-east-1");
1136        assert!(state.begin_instance_creation("db-x"));
1137        assert!(state.in_progress_instance_ids.contains("db-x"));
1138        state.finish_instance_creation(make_instance("db-x"));
1139        assert!(!state.in_progress_instance_ids.contains("db-x"));
1140        assert!(state.instances.contains_key("db-x"));
1141    }
1142
1143    #[test]
1144    fn cancel_instance_creation_drops_pending() {
1145        let mut state = RdsState::new("123456789012", "us-east-1");
1146        state.begin_instance_creation("db-y");
1147        state.cancel_instance_creation("db-y");
1148        assert!(!state.in_progress_instance_ids.contains("db-y"));
1149    }
1150
1151    #[test]
1152    fn begin_instance_creation_rejects_when_already_created() {
1153        let mut state = RdsState::new("123456789012", "us-east-1");
1154        state
1155            .instances
1156            .insert("db-z".to_string(), make_instance("db-z"));
1157        assert!(!state.begin_instance_creation("db-z"));
1158    }
1159
1160    #[test]
1161    fn reset_restores_default_parameter_groups() {
1162        let mut state = RdsState::new("123456789012", "us-east-1");
1163        state.parameter_groups.clear();
1164        state.reset();
1165        assert!(!state.parameter_groups.is_empty());
1166    }
1167
1168    #[test]
1169    fn arn_helpers_include_region_and_account() {
1170        let state = RdsState::new("111122223333", "ap-southeast-2");
1171        let arn = state.db_instance_arn("my-db");
1172        assert!(arn.contains("111122223333"));
1173        assert!(arn.contains("ap-southeast-2"));
1174        let snap = state.db_snapshot_arn("snap");
1175        assert!(snap.contains("snapshot:snap"));
1176    }
1177
1178    #[test]
1179    fn next_dbi_resource_id_unique_across_calls() {
1180        let state = RdsState::new("123", "us-east-1");
1181        let a = state.next_dbi_resource_id();
1182        let b = state.next_dbi_resource_id();
1183        assert_ne!(a, b);
1184    }
1185}