Skip to main content

fakecloud_cloudcontrol/
state.rs

1//! Account-partitioned, serializable state for Cloud Control API.
2
3use std::collections::BTreeMap;
4use std::sync::Arc;
5
6use chrono::{DateTime, Utc};
7use parking_lot::RwLock;
8use serde::{Deserialize, Serialize};
9
10use fakecloud_core::multi_account::{AccountState, MultiAccountState};
11
12pub const CLOUDCONTROL_SNAPSHOT_SCHEMA_VERSION: u32 = 1;
13
14/// A resource created through Cloud Control, keyed by `(type_name, identifier)`.
15/// `properties` is the canonical JSON of the resource's current desired state
16/// (what `GetResource` returns), `attributes` are the provisioner's
17/// `GetAtt`-resolvable outputs used to satisfy read-only identifiers.
18#[derive(Debug, Clone, Serialize, Deserialize)]
19pub struct ManagedResource {
20    pub type_name: String,
21    pub identifier: String,
22    pub properties: serde_json::Value,
23    pub attributes: BTreeMap<String, String>,
24    pub created_at: DateTime<Utc>,
25}
26
27/// A resource-operation request, keyed by its `RequestToken`. Cloud Control
28/// provisioning is synchronous here, so `operation_status` is terminal
29/// (`SUCCESS`/`FAILED`) by the time the request returns.
30#[derive(Debug, Clone, Serialize, Deserialize)]
31pub struct ResourceRequest {
32    pub request_token: String,
33    pub type_name: String,
34    pub identifier: Option<String>,
35    /// `Operation`: CREATE | UPDATE | DELETE.
36    pub operation: String,
37    /// `OperationStatus`: PENDING | IN_PROGRESS | SUCCESS | FAILED |
38    /// CANCEL_IN_PROGRESS | CANCEL_COMPLETE.
39    pub operation_status: String,
40    pub event_time: DateTime<Utc>,
41    pub resource_model: Option<serde_json::Value>,
42    pub status_message: Option<String>,
43    pub error_code: Option<String>,
44    /// The `ClientToken` that created this request, for idempotency.
45    pub client_token: Option<String>,
46    /// A fingerprint of the request parameters (operation + type + identity +
47    /// desired state / patch). Used to reject reuse of a `ClientToken` with
48    /// different parameters (`ClientTokenConflictException`), matching AWS.
49    #[serde(default)]
50    pub fingerprint: Option<String>,
51}
52
53#[derive(Debug, Clone, Default, Serialize, Deserialize)]
54pub struct CloudControlState {
55    /// Created resources keyed by `"<type_name>\u{1f}<identifier>"`.
56    pub resources: BTreeMap<String, ManagedResource>,
57    /// Request ledger keyed by `RequestToken`.
58    pub requests: BTreeMap<String, ResourceRequest>,
59}
60
61impl CloudControlState {
62    pub fn resource_key(type_name: &str, identifier: &str) -> String {
63        format!("{type_name}\u{1f}{identifier}")
64    }
65}
66
67impl AccountState for CloudControlState {
68    fn new_for_account(_account_id: &str, _region: &str, _endpoint: &str) -> Self {
69        Self::default()
70    }
71}
72
73pub type SharedCloudControlState = Arc<RwLock<MultiAccountState<CloudControlState>>>;
74
75#[derive(Debug, Serialize, Deserialize)]
76pub struct CloudControlSnapshot {
77    pub schema_version: u32,
78    pub accounts: MultiAccountState<CloudControlState>,
79}
80
81#[cfg(test)]
82mod tests {
83    use super::*;
84
85    #[test]
86    fn resource_key_is_unit_separated() {
87        let k = CloudControlState::resource_key("AWS::S3::Bucket", "my-bucket");
88        assert!(k.contains('\u{1f}'));
89        assert!(k.starts_with("AWS::S3::Bucket"));
90        assert!(k.ends_with("my-bucket"));
91    }
92}