use chrono::{DateTime, Utc};
use parking_lot::RwLock;
use serde::{Deserialize, Serialize};
use std::collections::BTreeMap;
use std::sync::Arc;
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct StackResource {
pub logical_id: String,
pub physical_id: String,
pub resource_type: String,
pub status: String,
pub service_token: Option<String>,
#[serde(default)]
pub attributes: BTreeMap<String, String>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct StackOutput {
pub key: String,
pub value: String,
pub description: Option<String>,
pub export_name: Option<String>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct StackExport {
pub value: String,
pub exporting_stack_id: String,
pub exporting_stack_name: String,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct Stack {
pub name: String,
pub stack_id: String,
pub template: String,
pub status: String,
pub resources: Vec<StackResource>,
pub parameters: BTreeMap<String, String>,
pub tags: BTreeMap<String, String>,
pub created_at: DateTime<Utc>,
pub updated_at: Option<DateTime<Utc>>,
pub description: Option<String>,
pub notification_arns: Vec<String>,
#[serde(default)]
pub outputs: Vec<StackOutput>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct CloudFormationState {
pub account_id: String,
pub region: String,
#[serde(default)]
pub stacks: BTreeMap<String, Stack>,
#[serde(default)]
pub extras: BTreeMap<String, BTreeMap<String, serde_json::Value>>,
#[serde(default)]
pub events: BTreeMap<String, Vec<serde_json::Value>>,
#[serde(default)]
pub stack_policies: BTreeMap<String, String>,
#[serde(default)]
pub termination_protection: BTreeMap<String, bool>,
#[serde(default)]
pub orgs_access_enabled: bool,
#[serde(default)]
pub exports: BTreeMap<String, StackExport>,
#[serde(default)]
pub imports: BTreeMap<String, Vec<String>>,
}
impl CloudFormationState {
pub fn new(account_id: &str, region: &str) -> Self {
Self {
account_id: account_id.to_string(),
region: region.to_string(),
stacks: BTreeMap::new(),
extras: BTreeMap::new(),
events: BTreeMap::new(),
stack_policies: BTreeMap::new(),
termination_protection: BTreeMap::new(),
orgs_access_enabled: false,
exports: BTreeMap::new(),
imports: BTreeMap::new(),
}
}
pub fn reset(&mut self) {
self.stacks.clear();
self.extras.clear();
self.events.clear();
self.stack_policies.clear();
self.termination_protection.clear();
self.orgs_access_enabled = false;
self.exports.clear();
self.imports.clear();
}
}
pub type SharedCloudFormationState =
Arc<RwLock<fakecloud_core::multi_account::MultiAccountState<CloudFormationState>>>;
impl fakecloud_core::multi_account::AccountState for CloudFormationState {
fn new_for_account(account_id: &str, region: &str, _endpoint: &str) -> Self {
Self::new(account_id, region)
}
}
pub const CLOUDFORMATION_SNAPSHOT_SCHEMA_VERSION: u32 = 2;
#[derive(Debug, Serialize, Deserialize)]
pub struct CloudFormationSnapshot {
pub schema_version: u32,
#[serde(default)]
pub accounts: Option<fakecloud_core::multi_account::MultiAccountState<CloudFormationState>>,
#[serde(default)]
pub state: Option<CloudFormationState>,
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn new_initializes_empty() {
let state = CloudFormationState::new("123456789012", "us-east-1");
assert_eq!(state.account_id, "123456789012");
assert_eq!(state.region, "us-east-1");
assert!(state.stacks.is_empty());
}
#[test]
fn reset_clears_stacks() {
let mut state = CloudFormationState::new("123456789012", "us-east-1");
state.stacks.insert(
"s1".to_string(),
Stack {
name: "s1".to_string(),
stack_id: "id".to_string(),
template: "{}".to_string(),
status: "CREATE_COMPLETE".to_string(),
resources: vec![],
parameters: BTreeMap::new(),
tags: BTreeMap::new(),
created_at: Utc::now(),
updated_at: None,
description: None,
notification_arns: vec![],
outputs: vec![],
},
);
state.reset();
assert!(state.stacks.is_empty());
}
}