Skip to main content

fakecloud_resource_groups/
state.rs

1//! Account-partitioned, serializable state for AWS Resource Groups.
2
3use std::collections::{BTreeMap, BTreeSet};
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 RESOURCE_GROUPS_SNAPSHOT_SCHEMA_VERSION: u32 = 1;
13
14/// A resource query attached to a group: either a tag filter or a
15/// CloudFormation-stack membership query. `query` is the raw JSON string AWS
16/// carries in the `Query` member.
17#[derive(Debug, Clone, Serialize, Deserialize)]
18pub struct ResourceQuery {
19    /// `TAG_FILTERS_1_0` or `CLOUDFORMATION_STACK_1_0`.
20    pub type_: String,
21    pub query: String,
22}
23
24/// A resource group. `resources` holds ARNs explicitly associated via
25/// `GroupResources` (for configuration/service-linked groups without a query);
26/// query-based groups compute membership from their `query`.
27#[derive(Debug, Clone, Serialize, Deserialize)]
28pub struct ResourceGroup {
29    pub name: String,
30    pub arn: String,
31    pub description: Option<String>,
32    pub query: Option<ResourceQuery>,
33    /// `GroupConfigurationItem` list, stored as raw JSON values.
34    pub configuration: Vec<serde_json::Value>,
35    pub tags: BTreeMap<String, String>,
36    pub criticality: Option<i32>,
37    pub owner: Option<String>,
38    pub display_name: Option<String>,
39    pub application_tag: BTreeMap<String, String>,
40    /// ARNs explicitly grouped via `GroupResources`.
41    pub resources: BTreeSet<String>,
42    pub created_at: DateTime<Utc>,
43}
44
45/// A tag-sync task binding a tag key/value to a group so matching resources are
46/// auto-associated.
47#[derive(Debug, Clone, Serialize, Deserialize)]
48pub struct TagSyncTask {
49    pub task_arn: String,
50    pub group_arn: String,
51    pub group_name: String,
52    pub tag_key: Option<String>,
53    pub tag_value: Option<String>,
54    pub resource_query: Option<ResourceQuery>,
55    pub role_arn: String,
56    pub status: String,
57    pub created_at: DateTime<Utc>,
58}
59
60#[derive(Debug, Clone, Default, Serialize, Deserialize)]
61pub struct AccountSettings {
62    /// `GroupLifecycleEventsDesiredStatus`: ACTIVE | INACTIVE (default INACTIVE).
63    pub desired_status: Option<String>,
64}
65
66#[derive(Debug, Clone, Default, Serialize, Deserialize)]
67pub struct ResourceGroupsState {
68    /// Groups keyed by name.
69    pub groups: BTreeMap<String, ResourceGroup>,
70    /// Tag-sync tasks keyed by task ARN.
71    pub tag_sync_tasks: BTreeMap<String, TagSyncTask>,
72    pub account_settings: AccountSettings,
73}
74
75impl ResourceGroupsState {
76    /// Resolve a group by name or ARN (the `GroupStringV2` member accepts both).
77    pub fn resolve_name<'a>(&'a self, name_or_arn: &'a str) -> Option<&'a str> {
78        if self.groups.contains_key(name_or_arn) {
79            return Some(name_or_arn);
80        }
81        // Exact stored ARN: arn:aws:resource-groups:region:acct:group/<name>/<uid>
82        if let Some(g) = self.groups.values().find(|g| g.arn == name_or_arn) {
83            return Some(g.name.as_str());
84        }
85        // ARN whose `group/` resource part names an existing group, with or without
86        // the generated id suffix (`group/<name>` or `group/<name>/<uid>`). Match by
87        // the leading name segment so a client-reconstructed ARN still resolves.
88        if let Some(resource) = name_or_arn
89            .starts_with("arn:")
90            .then(|| name_or_arn.split(":group/").nth(1))
91            .flatten()
92        {
93            let group_name = resource.split('/').next().unwrap_or(resource);
94            if let Some((stored, _)) = self.groups.get_key_value(group_name) {
95                return Some(stored.as_str());
96            }
97        }
98        None
99    }
100}
101
102impl AccountState for ResourceGroupsState {
103    fn new_for_account(_account_id: &str, _region: &str, _endpoint: &str) -> Self {
104        Self::default()
105    }
106}
107
108pub type SharedResourceGroupsState = Arc<RwLock<MultiAccountState<ResourceGroupsState>>>;
109
110#[derive(Debug, Serialize, Deserialize)]
111pub struct ResourceGroupsSnapshot {
112    pub schema_version: u32,
113    pub accounts: MultiAccountState<ResourceGroupsState>,
114}
115
116#[cfg(test)]
117mod tests {
118    use super::*;
119
120    #[test]
121    fn resolve_by_name_and_arn() {
122        let mut st = ResourceGroupsState::default();
123        st.groups.insert(
124            "g1".into(),
125            ResourceGroup {
126                name: "g1".into(),
127                arn: "arn:aws:resource-groups:us-east-1:123456789012:group/g1/abcd".into(),
128                description: None,
129                query: None,
130                configuration: vec![],
131                tags: BTreeMap::new(),
132                criticality: None,
133                owner: None,
134                display_name: None,
135                application_tag: BTreeMap::new(),
136                resources: BTreeSet::new(),
137                created_at: Utc::now(),
138            },
139        );
140        assert_eq!(st.resolve_name("g1"), Some("g1"));
141        assert_eq!(
142            st.resolve_name("arn:aws:resource-groups:us-east-1:123456789012:group/g1/abcd"),
143            Some("g1")
144        );
145        // Client-reconstructed ARN without the generated id suffix still resolves.
146        assert_eq!(
147            st.resolve_name("arn:aws:resource-groups:us-east-1:123456789012:group/g1"),
148            Some("g1")
149        );
150        // ARN with a different (wrong) id suffix but the right name still resolves.
151        assert_eq!(
152            st.resolve_name("arn:aws:resource-groups:us-west-2:123456789012:group/g1/zzzz"),
153            Some("g1")
154        );
155        assert_eq!(st.resolve_name("missing"), None);
156        assert_eq!(
157            st.resolve_name("arn:aws:resource-groups:us-east-1:123456789012:group/missing"),
158            None
159        );
160    }
161}