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 #[serde(default)]
420 pub parameter_apply_methods: BTreeMap<String, String>,
421 pub tags: Vec<RdsTag>,
422}
423
424#[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
442pub 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 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 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 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 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 ("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); 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); 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}