Skip to main content

fakecloud_cloudformation/
state.rs

1use chrono::{DateTime, Utc};
2use parking_lot::RwLock;
3use serde::{Deserialize, Serialize};
4use std::collections::BTreeMap;
5use std::sync::Arc;
6
7#[derive(Debug, Clone, Serialize, Deserialize)]
8pub struct StackResource {
9    pub logical_id: String,
10    pub physical_id: String,
11    pub resource_type: String,
12    pub status: String,
13    /// For custom resources, the Lambda ARN (ServiceToken) used for invocation.
14    pub service_token: Option<String>,
15    /// Per-resource attributes resolvable via `Fn::GetAtt`. Populated at
16    /// provisioning time by each resource type's create handler.
17    #[serde(default)]
18    pub attributes: BTreeMap<String, String>,
19}
20
21#[derive(Debug, Clone, Serialize, Deserialize)]
22pub struct StackOutput {
23    pub key: String,
24    pub value: String,
25    pub description: Option<String>,
26    pub export_name: Option<String>,
27}
28
29/// Cross-stack export entry, keyed by `Export.Name` in `state.exports`.
30/// Tracks the resolved value plus the stack that owns it so `ListExports`
31/// can return a stable `ExportingStackId` and `DeleteStack` can attribute
32/// the export back to its source.
33#[derive(Debug, Clone, Serialize, Deserialize)]
34pub struct StackExport {
35    pub value: String,
36    pub exporting_stack_id: String,
37    pub exporting_stack_name: String,
38}
39
40#[derive(Debug, Clone, Serialize, Deserialize)]
41pub struct Stack {
42    pub name: String,
43    pub stack_id: String,
44    pub template: String,
45    pub status: String,
46    pub resources: Vec<StackResource>,
47    pub parameters: BTreeMap<String, String>,
48    pub tags: BTreeMap<String, String>,
49    pub created_at: DateTime<Utc>,
50    pub updated_at: Option<DateTime<Utc>>,
51    pub description: Option<String>,
52    pub notification_arns: Vec<String>,
53    #[serde(default)]
54    pub outputs: Vec<StackOutput>,
55}
56
57#[derive(Debug, Clone, Serialize, Deserialize)]
58pub struct CloudFormationState {
59    pub account_id: String,
60    pub region: String,
61    #[serde(default)]
62    pub stacks: BTreeMap<String, Stack>,
63    /// Generic stores keyed by `category` (change_sets, stack_sets, types,
64    /// generated_templates, resource_scans, refactors, etc.) so the
65    /// extras handlers can keep state alive without proliferating
66    /// per-category fields.
67    #[serde(default)]
68    pub extras: BTreeMap<String, BTreeMap<String, serde_json::Value>>,
69    #[serde(default)]
70    pub events: BTreeMap<String, Vec<serde_json::Value>>,
71    #[serde(default)]
72    pub stack_policies: BTreeMap<String, String>,
73    #[serde(default)]
74    pub termination_protection: BTreeMap<String, bool>,
75    #[serde(default)]
76    pub orgs_access_enabled: bool,
77    /// Account-wide exports keyed by `Export.Name`. Populated whenever a
78    /// stack's outputs include `Export.Name` and removed on stack delete.
79    #[serde(default)]
80    pub exports: BTreeMap<String, StackExport>,
81    /// Reverse-ref map: `imports[export_name]` lists the stack names that
82    /// have consumed the export via `Fn::ImportValue`. CloudFormation
83    /// blocks deleting a stack whose exports still appear here.
84    #[serde(default)]
85    pub imports: BTreeMap<String, Vec<String>>,
86}
87
88impl CloudFormationState {
89    pub fn new(account_id: &str, region: &str) -> Self {
90        Self {
91            account_id: account_id.to_string(),
92            region: region.to_string(),
93            stacks: BTreeMap::new(),
94            extras: BTreeMap::new(),
95            events: BTreeMap::new(),
96            stack_policies: BTreeMap::new(),
97            termination_protection: BTreeMap::new(),
98            orgs_access_enabled: false,
99            exports: BTreeMap::new(),
100            imports: BTreeMap::new(),
101        }
102    }
103
104    pub fn reset(&mut self) {
105        self.stacks.clear();
106        self.extras.clear();
107        self.events.clear();
108        self.stack_policies.clear();
109        self.termination_protection.clear();
110        self.orgs_access_enabled = false;
111        self.exports.clear();
112        self.imports.clear();
113    }
114}
115
116pub type SharedCloudFormationState =
117    Arc<RwLock<fakecloud_core::multi_account::MultiAccountState<CloudFormationState>>>;
118
119impl fakecloud_core::multi_account::AccountState for CloudFormationState {
120    fn new_for_account(account_id: &str, region: &str, _endpoint: &str) -> Self {
121        Self::new(account_id, region)
122    }
123}
124
125pub const CLOUDFORMATION_SNAPSHOT_SCHEMA_VERSION: u32 = 2;
126
127#[derive(Debug, Serialize, Deserialize)]
128pub struct CloudFormationSnapshot {
129    pub schema_version: u32,
130    #[serde(default)]
131    pub accounts: Option<fakecloud_core::multi_account::MultiAccountState<CloudFormationState>>,
132    #[serde(default)]
133    pub state: Option<CloudFormationState>,
134}
135
136#[cfg(test)]
137mod tests {
138    use super::*;
139
140    #[test]
141    fn new_initializes_empty() {
142        let state = CloudFormationState::new("123456789012", "us-east-1");
143        assert_eq!(state.account_id, "123456789012");
144        assert_eq!(state.region, "us-east-1");
145        assert!(state.stacks.is_empty());
146    }
147
148    #[test]
149    fn reset_clears_stacks() {
150        let mut state = CloudFormationState::new("123456789012", "us-east-1");
151        state.stacks.insert(
152            "s1".to_string(),
153            Stack {
154                name: "s1".to_string(),
155                stack_id: "id".to_string(),
156                template: "{}".to_string(),
157                status: "CREATE_COMPLETE".to_string(),
158                resources: vec![],
159                parameters: BTreeMap::new(),
160                tags: BTreeMap::new(),
161                created_at: Utc::now(),
162                updated_at: None,
163                description: None,
164                notification_arns: vec![],
165                outputs: vec![],
166            },
167        );
168        state.reset();
169        assert!(state.stacks.is_empty());
170    }
171}