Skip to main content

winterbaume_cloudformation/
views.rs

1//! Serde-compatible view types for CloudFormation state snapshots.
2
3use std::collections::HashMap;
4
5use serde::{Deserialize, Serialize};
6use winterbaume_core::{StateChangeNotifier, StateViewError, StatefulService};
7
8use crate::handlers::CloudFormationService;
9use crate::state::CloudFormationState;
10use crate::types::{
11    ChangeSet, Stack, StackEvent, StackExport, StackInstance, StackInstanceKey, StackParameter,
12    StackResource, StackSet, StackSetOperation, StackTag,
13};
14
15// ---------------------------------------------------------------------------
16// View types
17// ---------------------------------------------------------------------------
18
19#[derive(Debug, Clone, Serialize, Deserialize, Default)]
20pub struct CloudFormationStateView {
21    #[serde(default)]
22    pub stacks: HashMap<String, StackView>,
23    #[serde(default)]
24    pub stack_sets: HashMap<String, StackSetView>,
25    #[serde(default)]
26    pub stack_instances: Vec<StackInstanceView>,
27    #[serde(default)]
28    pub registered_types: Vec<crate::types::RegisteredType>,
29}
30
31#[derive(Debug, Clone, Serialize, Deserialize)]
32pub struct StackView {
33    pub stack_id: String,
34    pub stack_name: String,
35    pub stack_status: String,
36    pub creation_time: String,
37    pub last_updated_time: Option<String>,
38    pub deletion_time: Option<String>,
39    pub description: Option<String>,
40    pub template_body: Option<String>,
41    pub stack_policy_body: Option<String>,
42    pub parameters: Vec<StackParameter>,
43    pub outputs: Vec<crate::types::StackOutput>,
44    pub tags: Vec<StackTag>,
45    pub capabilities: Vec<String>,
46    pub resources: Vec<StackResource>,
47    pub events: Vec<StackEvent>,
48    pub change_sets: Vec<ChangeSet>,
49    pub exports: Vec<StackExport>,
50    pub role_arn: Option<String>,
51    pub timeout_in_minutes: Option<i32>,
52    pub disable_rollback: bool,
53    #[serde(default)]
54    pub enable_termination_protection: bool,
55}
56
57#[derive(Debug, Clone, Serialize, Deserialize)]
58pub struct StackSetView {
59    pub stack_set_id: String,
60    pub stack_set_name: String,
61    pub stack_set_arn: String,
62    pub status: String,
63    pub description: Option<String>,
64    pub template_body: Option<String>,
65    pub parameters: Vec<StackParameter>,
66    pub tags: Vec<StackTag>,
67    pub capabilities: Vec<String>,
68    pub operations: Vec<StackSetOperation>,
69}
70
71#[derive(Debug, Clone, Serialize, Deserialize)]
72pub struct StackInstanceView {
73    pub stack_set_name: String,
74    pub account: String,
75    pub region: String,
76    pub stack_id: Option<String>,
77    pub status: String,
78    pub status_reason: Option<String>,
79    pub stack_set_id: String,
80    pub parameter_overrides: Vec<StackParameter>,
81}
82
83// ---------------------------------------------------------------------------
84// Conversions: State -> View
85// ---------------------------------------------------------------------------
86
87impl From<&Stack> for StackView {
88    fn from(s: &Stack) -> Self {
89        Self {
90            stack_id: s.stack_id.clone(),
91            stack_name: s.stack_name.clone(),
92            stack_status: s.stack_status.clone(),
93            creation_time: s.creation_time.clone(),
94            last_updated_time: s.last_updated_time.clone(),
95            deletion_time: s.deletion_time.clone(),
96            description: s.description.clone(),
97            template_body: s.template_body.clone(),
98            stack_policy_body: s.stack_policy_body.clone(),
99            parameters: s.parameters.clone(),
100            outputs: s.outputs.clone(),
101            tags: s.tags.clone(),
102            capabilities: s.capabilities.clone(),
103            resources: s.resources.clone(),
104            events: s.events.clone(),
105            change_sets: s.change_sets.clone(),
106            exports: s.exports.clone(),
107            role_arn: s.role_arn.clone(),
108            timeout_in_minutes: s.timeout_in_minutes,
109            disable_rollback: s.disable_rollback,
110            enable_termination_protection: s.enable_termination_protection,
111        }
112    }
113}
114
115impl From<StackView> for Stack {
116    fn from(v: StackView) -> Self {
117        Self {
118            stack_id: v.stack_id,
119            stack_name: v.stack_name,
120            stack_status: v.stack_status,
121            creation_time: v.creation_time,
122            last_updated_time: v.last_updated_time,
123            deletion_time: v.deletion_time,
124            description: v.description,
125            template_body: v.template_body,
126            stack_policy_body: v.stack_policy_body,
127            parameters: v.parameters,
128            outputs: v.outputs,
129            tags: v.tags,
130            capabilities: v.capabilities,
131            resources: v.resources,
132            events: v.events,
133            change_sets: v.change_sets,
134            exports: v.exports,
135            role_arn: v.role_arn,
136            timeout_in_minutes: v.timeout_in_minutes,
137            disable_rollback: v.disable_rollback,
138            enable_termination_protection: v.enable_termination_protection,
139        }
140    }
141}
142
143impl From<&StackSet> for StackSetView {
144    fn from(ss: &StackSet) -> Self {
145        Self {
146            stack_set_id: ss.stack_set_id.clone(),
147            stack_set_name: ss.stack_set_name.clone(),
148            stack_set_arn: ss.stack_set_arn.clone(),
149            status: ss.status.clone(),
150            description: ss.description.clone(),
151            template_body: ss.template_body.clone(),
152            parameters: ss.parameters.clone(),
153            tags: ss.tags.clone(),
154            capabilities: ss.capabilities.clone(),
155            operations: ss.operations.clone(),
156        }
157    }
158}
159
160impl From<StackSetView> for StackSet {
161    fn from(v: StackSetView) -> Self {
162        Self {
163            stack_set_id: v.stack_set_id,
164            stack_set_name: v.stack_set_name,
165            stack_set_arn: v.stack_set_arn,
166            status: v.status,
167            description: v.description,
168            template_body: v.template_body,
169            parameters: v.parameters,
170            tags: v.tags,
171            capabilities: v.capabilities,
172            operations: v.operations,
173        }
174    }
175}
176
177impl From<&StackInstance> for StackInstanceView {
178    fn from(i: &StackInstance) -> Self {
179        Self {
180            stack_set_name: i.stack_set_name.clone(),
181            account: i.account.clone(),
182            region: i.region.clone(),
183            stack_id: i.stack_id.clone(),
184            status: i.status.clone(),
185            status_reason: i.status_reason.clone(),
186            stack_set_id: i.stack_set_id.clone(),
187            parameter_overrides: i.parameter_overrides.clone(),
188        }
189    }
190}
191
192impl From<StackInstanceView> for StackInstance {
193    fn from(v: StackInstanceView) -> Self {
194        Self {
195            stack_set_name: v.stack_set_name,
196            account: v.account,
197            region: v.region,
198            stack_id: v.stack_id,
199            status: v.status,
200            status_reason: v.status_reason,
201            stack_set_id: v.stack_set_id,
202            parameter_overrides: v.parameter_overrides,
203        }
204    }
205}
206
207impl From<&CloudFormationState> for CloudFormationStateView {
208    fn from(state: &CloudFormationState) -> Self {
209        Self {
210            stacks: state
211                .stacks
212                .iter()
213                .map(|(k, v)| (k.clone(), StackView::from(v)))
214                .collect(),
215            stack_sets: state
216                .stack_sets
217                .iter()
218                .map(|(k, v)| (k.clone(), StackSetView::from(v)))
219                .collect(),
220            stack_instances: state
221                .stack_instances
222                .values()
223                .map(StackInstanceView::from)
224                .collect(),
225            registered_types: state.registered_types.clone(),
226        }
227    }
228}
229
230impl From<CloudFormationStateView> for CloudFormationState {
231    fn from(view: CloudFormationStateView) -> Self {
232        let stack_instances = view
233            .stack_instances
234            .into_iter()
235            .map(|v| {
236                let key = StackInstanceKey {
237                    stack_set_name: v.stack_set_name.clone(),
238                    account: v.account.clone(),
239                    region: v.region.clone(),
240                };
241                (key, StackInstance::from(v))
242            })
243            .collect();
244        Self {
245            stacks: view
246                .stacks
247                .into_iter()
248                .map(|(k, v)| (k, Stack::from(v)))
249                .collect(),
250            stack_sets: view
251                .stack_sets
252                .into_iter()
253                .map(|(k, v)| (k, StackSet::from(v)))
254                .collect(),
255            stack_instances,
256            registered_types: view.registered_types,
257        }
258    }
259}
260
261// ---------------------------------------------------------------------------
262// StatefulService implementation
263// ---------------------------------------------------------------------------
264
265impl StatefulService for CloudFormationService {
266    type StateView = CloudFormationStateView;
267
268    async fn snapshot(&self, account_id: &str, region: &str) -> Self::StateView {
269        let state = self.state.get(account_id, region);
270        let guard = state.read().await;
271        CloudFormationStateView::from(&*guard)
272    }
273
274    async fn restore(
275        &self,
276        account_id: &str,
277        region: &str,
278        view: Self::StateView,
279    ) -> Result<(), StateViewError> {
280        let state = self.state.get(account_id, region);
281        {
282            let mut guard = state.write().await;
283            *guard = CloudFormationState::from(view);
284        }
285        self.notify_state_changed(account_id, region).await;
286        Ok(())
287    }
288
289    async fn merge(
290        &self,
291        account_id: &str,
292        region: &str,
293        view: Self::StateView,
294    ) -> Result<(), StateViewError> {
295        let state = self.state.get(account_id, region);
296        {
297            let mut guard = state.write().await;
298            for (k, v) in view.stacks {
299                guard.stacks.insert(k, Stack::from(v));
300            }
301            for (k, v) in view.stack_sets {
302                guard.stack_sets.insert(k, StackSet::from(v));
303            }
304            for v in view.stack_instances {
305                let key = StackInstanceKey {
306                    stack_set_name: v.stack_set_name.clone(),
307                    account: v.account.clone(),
308                    region: v.region.clone(),
309                };
310                guard.stack_instances.insert(key, StackInstance::from(v));
311            }
312            for rt in view.registered_types {
313                if !guard
314                    .registered_types
315                    .iter()
316                    .any(|t| t.type_name == rt.type_name && t.type_kind == rt.type_kind)
317                {
318                    guard.registered_types.push(rt);
319                }
320            }
321        }
322        self.notify_state_changed(account_id, region).await;
323        Ok(())
324    }
325
326    fn notifier(&self) -> &StateChangeNotifier<Self::StateView> {
327        &self.notifier
328    }
329}