Skip to main content

fakecloud_dsql/
state.rs

1//! Account-partitioned, serializable state for the Aurora DSQL control plane.
2
3use std::collections::BTreeMap;
4use std::sync::Arc;
5
6use chrono::{DateTime, Utc};
7use parking_lot::RwLock;
8use rand::Rng;
9use serde::{Deserialize, Serialize};
10
11use fakecloud_aws::arn::Arn;
12use fakecloud_core::multi_account::{AccountState, MultiAccountState};
13
14/// Bumped whenever the on-disk shape of [`DsqlSnapshot`] changes.
15pub const DSQL_SNAPSHOT_SCHEMA_VERSION: u32 = 1;
16
17/// A DSQL cluster and everything hanging off it (policy + change streams).
18#[derive(Debug, Clone, Serialize, Deserialize)]
19pub struct Cluster {
20    pub identifier: String,
21    pub arn: String,
22    /// `ClusterStatus`: CREATING | ACTIVE | IDLE | INACTIVE | UPDATING |
23    /// DELETING | DELETED | FAILED | PENDING_SETUP | PENDING_DELETE.
24    pub status: String,
25    pub creation_time: DateTime<Utc>,
26    pub deletion_protection_enabled: bool,
27    pub multi_region_properties: Option<MultiRegionProperties>,
28    pub encryption_details: EncryptionDetails,
29    /// The customer-facing SQL endpoint host (`<id>.dsql.<region>.on.aws`).
30    pub endpoint: String,
31    pub tags: BTreeMap<String, String>,
32    /// Resource-based policy document (JSON) and its monotonically increasing
33    /// version, set via `PutClusterPolicy`.
34    pub policy: Option<String>,
35    pub policy_version: u64,
36    /// The `clientToken` that created this cluster, for create idempotency.
37    #[serde(default)]
38    pub client_token: Option<String>,
39    /// When `DeleteCluster` was requested; drives the ticker's grace window
40    /// before the record is removed. `None` until deletion is requested.
41    #[serde(default)]
42    pub deleted_at: Option<DateTime<Utc>>,
43    /// Change streams keyed by `streamIdentifier`.
44    #[serde(default)]
45    pub streams: BTreeMap<String, Stream>,
46}
47
48#[derive(Debug, Clone, Serialize, Deserialize)]
49pub struct MultiRegionProperties {
50    pub witness_region: Option<String>,
51    pub clusters: Vec<String>,
52}
53
54#[derive(Debug, Clone, Serialize, Deserialize)]
55pub struct EncryptionDetails {
56    /// `EncryptionType`: AWS_OWNED_KMS_KEY | CUSTOMER_MANAGED_KMS_KEY.
57    pub encryption_type: String,
58    pub kms_key_arn: Option<String>,
59    /// `EncryptionStatus`: ENABLED | UPDATING | KMS_KEY_INACCESSIBLE | ENABLING.
60    pub encryption_status: String,
61}
62
63#[derive(Debug, Clone, Serialize, Deserialize)]
64pub struct Stream {
65    pub cluster_identifier: String,
66    pub stream_identifier: String,
67    pub arn: String,
68    /// `StreamStatus`: CREATING | ACTIVE | DELETING | DELETED | FAILED | IMPAIRED.
69    pub status: String,
70    pub creation_time: DateTime<Utc>,
71    /// `StreamOrdering`: only UNORDERED is modeled by AWS today.
72    pub ordering: String,
73    /// `StreamFormat`: only JSON is modeled by AWS today.
74    pub format: String,
75    /// `TargetDefinition` union, round-tripped verbatim (currently `{ kinesis }`).
76    pub target_definition: serde_json::Value,
77    pub status_reason: Option<String>,
78    pub tags: BTreeMap<String, String>,
79    #[serde(default)]
80    pub client_token: Option<String>,
81    /// When `DeleteStream` was requested; drives the ticker's grace window
82    /// before the record is removed. `None` until deletion is requested.
83    #[serde(default)]
84    pub deleted_at: Option<DateTime<Utc>>,
85}
86
87/// One account's DSQL clusters, keyed by cluster identifier.
88#[derive(Debug, Clone, Serialize, Deserialize)]
89pub struct DsqlState {
90    pub account_id: String,
91    pub region: String,
92    pub clusters: BTreeMap<String, Cluster>,
93}
94
95impl DsqlState {
96    pub fn new(account_id: &str, region: &str) -> Self {
97        Self {
98            account_id: account_id.to_string(),
99            region: region.to_string(),
100            clusters: BTreeMap::new(),
101        }
102    }
103}
104
105impl AccountState for DsqlState {
106    fn new_for_account(account_id: &str, region: &str, _endpoint: &str) -> Self {
107        Self::new(account_id, region)
108    }
109}
110
111pub type SharedDsqlState = Arc<RwLock<MultiAccountState<DsqlState>>>;
112
113#[derive(Debug, Serialize, Deserialize)]
114pub struct DsqlSnapshot {
115    pub schema_version: u32,
116    pub accounts: MultiAccountState<DsqlState>,
117}
118
119/// Generate a 26-character lowercase-alphanumeric identifier, matching the AWS
120/// `^[a-z0-9]{26}$` pattern used for both cluster and stream ids.
121pub fn gen_id() -> String {
122    const CHARS: &[u8] = b"abcdefghijklmnopqrstuvwxyz0123456789";
123    let mut rng = rand::thread_rng();
124    (0..26)
125        .map(|_| CHARS[rng.gen_range(0..CHARS.len())] as char)
126        .collect()
127}
128
129/// Build a cluster ARN: `arn:aws:dsql:<region>:<account>:cluster/<id>`.
130pub fn cluster_arn(region: &str, account_id: &str, id: &str) -> String {
131    Arn::new("dsql", region, account_id, &format!("cluster/{id}")).to_string()
132}
133
134/// Build a stream ARN:
135/// `arn:aws:dsql:<region>:<account>:cluster/<cluster>/stream/<stream>`.
136pub fn stream_arn(region: &str, account_id: &str, cluster: &str, stream: &str) -> String {
137    Arn::new(
138        "dsql",
139        region,
140        account_id,
141        &format!("cluster/{cluster}/stream/{stream}"),
142    )
143    .to_string()
144}
145
146/// The customer-facing SQL endpoint host for a cluster.
147pub fn cluster_endpoint(id: &str, region: &str) -> String {
148    format!("{id}.dsql.{region}.on.aws")
149}
150
151#[cfg(test)]
152mod tests {
153    use super::*;
154
155    #[test]
156    fn gen_id_matches_aws_pattern() {
157        let id = gen_id();
158        assert_eq!(id.len(), 26);
159        assert!(id
160            .chars()
161            .all(|c| c.is_ascii_lowercase() || c.is_ascii_digit()));
162    }
163
164    #[test]
165    fn arns_have_expected_shape() {
166        let id = "abcdefghij0123456789klmnop";
167        let c = cluster_arn("us-east-1", "123456789012", id);
168        assert_eq!(
169            c,
170            format!("arn:aws:dsql:us-east-1:123456789012:cluster/{id}")
171        );
172        let s = stream_arn("us-east-1", "123456789012", id, id);
173        assert_eq!(
174            s,
175            format!("arn:aws:dsql:us-east-1:123456789012:cluster/{id}/stream/{id}")
176        );
177    }
178
179    #[test]
180    fn endpoint_shape() {
181        assert_eq!(
182            cluster_endpoint("abcdefghij0123456789klmnop", "us-west-2"),
183            "abcdefghij0123456789klmnop.dsql.us-west-2.on.aws"
184        );
185    }
186}