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
18pub 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 #[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 #[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 #[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 #[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 #[serde(default)]
353 pub extras: BTreeMap<String, BTreeMap<String, serde_json::Value>>,
354 #[serde(default)]
358 pub events: Vec<RdsEventRecord>,
359 #[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#[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
437pub 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 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 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 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 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 ("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); 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); 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}