Skip to main content

winterbaume_cloudcontrol/
views.rs

1//! Serde-compatible view types for Cloud Control API state snapshots.
2
3use std::collections::HashMap;
4
5use serde::{Deserialize, Serialize};
6use winterbaume_core::{StateChangeNotifier, StateViewError, StatefulService};
7
8use crate::handlers::CloudControlService;
9use crate::state::CloudControlState;
10use crate::types::{ManagedResource, OperationStatus, OperationType, ResourceRequest};
11
12/// Serializable view of the entire Cloud Control API state for one account/region.
13#[derive(Debug, Clone, Default, Serialize, Deserialize)]
14pub struct CloudControlStateView {
15    /// Resources keyed by "{type_name}|{identifier}".
16    #[serde(default)]
17    pub resources: HashMap<String, ResourceView>,
18    /// Operation requests keyed by request_token.
19    #[serde(default)]
20    pub requests: HashMap<String, RequestView>,
21}
22
23/// Serializable view of a managed resource.
24#[derive(Debug, Clone, Serialize, Deserialize)]
25pub struct ResourceView {
26    pub type_name: String,
27    pub identifier: String,
28    pub resource_model: String,
29}
30
31/// Serializable view of a resource operation request.
32#[derive(Debug, Clone, Serialize, Deserialize)]
33pub struct RequestView {
34    pub request_token: String,
35    pub type_name: String,
36    pub identifier: String,
37    pub operation: String,
38    pub operation_status: String,
39    pub event_time: String,
40    #[serde(default)]
41    pub resource_model: Option<String>,
42    #[serde(default)]
43    pub status_message: Option<String>,
44    #[serde(default)]
45    pub error_code: Option<String>,
46}
47
48impl From<&CloudControlState> for CloudControlStateView {
49    fn from(state: &CloudControlState) -> Self {
50        Self {
51            resources: state
52                .resources
53                .iter()
54                .map(|((type_name, identifier), r)| {
55                    let key = format!("{}|{}", type_name, identifier);
56                    (
57                        key,
58                        ResourceView {
59                            type_name: r.type_name.clone(),
60                            identifier: r.identifier.clone(),
61                            resource_model: r.resource_model.clone(),
62                        },
63                    )
64                })
65                .collect(),
66            requests: state
67                .requests
68                .iter()
69                .map(|(token, r)| {
70                    (
71                        token.clone(),
72                        RequestView {
73                            request_token: r.request_token.clone(),
74                            type_name: r.type_name.clone(),
75                            identifier: r.identifier.clone(),
76                            operation: r.operation.as_str().to_string(),
77                            operation_status: r.operation_status.as_str().to_string(),
78                            event_time: r.event_time.to_rfc3339(),
79                            resource_model: r.resource_model.clone(),
80                            status_message: r.status_message.clone(),
81                            error_code: r.error_code.clone(),
82                        },
83                    )
84                })
85                .collect(),
86        }
87    }
88}
89
90impl From<CloudControlStateView> for CloudControlState {
91    fn from(view: CloudControlStateView) -> Self {
92        let resources = view
93            .resources
94            .into_values()
95            .map(|rv| {
96                let key = (rv.type_name.clone(), rv.identifier.clone());
97                let resource = ManagedResource {
98                    type_name: rv.type_name,
99                    identifier: rv.identifier,
100                    resource_model: rv.resource_model,
101                };
102                (key, resource)
103            })
104            .collect();
105
106        let requests = view
107            .requests
108            .into_values()
109            .map(|rv| {
110                let operation = match rv.operation.as_str() {
111                    "DELETE" => OperationType::Delete,
112                    "UPDATE" => OperationType::Update,
113                    _ => OperationType::Create,
114                };
115                let operation_status = match rv.operation_status.as_str() {
116                    "PENDING" => OperationStatus::Pending,
117                    "IN_PROGRESS" => OperationStatus::InProgress,
118                    "FAILED" => OperationStatus::Failed,
119                    "CANCEL_IN_PROGRESS" => OperationStatus::CancelInProgress,
120                    "CANCEL_COMPLETE" => OperationStatus::CancelComplete,
121                    _ => OperationStatus::Success,
122                };
123                let event_time = chrono::DateTime::parse_from_rfc3339(&rv.event_time)
124                    .map(|dt| dt.with_timezone(&chrono::Utc))
125                    .unwrap_or_else(|_| chrono::Utc::now());
126
127                let request = ResourceRequest {
128                    request_token: rv.request_token.clone(),
129                    type_name: rv.type_name,
130                    identifier: rv.identifier,
131                    operation,
132                    operation_status,
133                    event_time,
134                    resource_model: rv.resource_model,
135                    status_message: rv.status_message,
136                    error_code: rv.error_code,
137                };
138                (rv.request_token, request)
139            })
140            .collect();
141
142        CloudControlState {
143            resources,
144            requests,
145        }
146    }
147}
148
149impl StatefulService for CloudControlService {
150    type StateView = CloudControlStateView;
151
152    async fn snapshot(&self, account_id: &str, region: &str) -> Self::StateView {
153        let state = self.state.get(account_id, region);
154        let guard = state.read().await;
155        CloudControlStateView::from(&*guard)
156    }
157
158    async fn restore(
159        &self,
160        account_id: &str,
161        region: &str,
162        view: Self::StateView,
163    ) -> Result<(), StateViewError> {
164        let state = self.state.get(account_id, region);
165        {
166            let mut guard = state.write().await;
167            *guard = CloudControlState::from(view);
168        }
169        self.notify_state_changed(account_id, region).await;
170        Ok(())
171    }
172
173    async fn merge(
174        &self,
175        account_id: &str,
176        region: &str,
177        view: Self::StateView,
178    ) -> Result<(), StateViewError> {
179        let state = self.state.get(account_id, region);
180        {
181            let mut guard = state.write().await;
182            for rv in view.resources.into_values() {
183                let key = (rv.type_name.clone(), rv.identifier.clone());
184                guard.resources.insert(
185                    key,
186                    ManagedResource {
187                        type_name: rv.type_name,
188                        identifier: rv.identifier,
189                        resource_model: rv.resource_model,
190                    },
191                );
192            }
193            // Merge requests additively
194            for rv in view.requests.into_values() {
195                let operation = match rv.operation.as_str() {
196                    "DELETE" => OperationType::Delete,
197                    "UPDATE" => OperationType::Update,
198                    _ => OperationType::Create,
199                };
200                let operation_status = match rv.operation_status.as_str() {
201                    "PENDING" => OperationStatus::Pending,
202                    "IN_PROGRESS" => OperationStatus::InProgress,
203                    "FAILED" => OperationStatus::Failed,
204                    "CANCEL_IN_PROGRESS" => OperationStatus::CancelInProgress,
205                    "CANCEL_COMPLETE" => OperationStatus::CancelComplete,
206                    _ => OperationStatus::Success,
207                };
208                let event_time = chrono::DateTime::parse_from_rfc3339(&rv.event_time)
209                    .map(|dt| dt.with_timezone(&chrono::Utc))
210                    .unwrap_or_else(|_| chrono::Utc::now());
211
212                guard.requests.insert(
213                    rv.request_token.clone(),
214                    ResourceRequest {
215                        request_token: rv.request_token,
216                        type_name: rv.type_name,
217                        identifier: rv.identifier,
218                        operation,
219                        operation_status,
220                        event_time,
221                        resource_model: rv.resource_model,
222                        status_message: rv.status_message,
223                        error_code: rv.error_code,
224                    },
225                );
226            }
227        }
228        self.notify_state_changed(account_id, region).await;
229        Ok(())
230    }
231
232    fn notifier(&self) -> &StateChangeNotifier<Self::StateView> {
233        &self.notifier
234    }
235}