fakecloud-cloudformation 0.13.1

CloudFormation implementation for FakeCloud
Documentation
use chrono::{DateTime, Utc};
use parking_lot::RwLock;
use serde::{Deserialize, Serialize};
use std::collections::HashMap;
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,
    /// For custom resources, the Lambda ARN (ServiceToken) used for invocation.
    pub service_token: Option<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: HashMap<String, String>,
    pub tags: HashMap<String, String>,
    pub created_at: DateTime<Utc>,
    pub updated_at: Option<DateTime<Utc>>,
    pub description: Option<String>,
    pub notification_arns: Vec<String>,
}

#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct CloudFormationState {
    pub account_id: String,
    pub region: String,
    #[serde(default)]
    pub stacks: HashMap<String, Stack>,
    /// Generic stores keyed by `category` (change_sets, stack_sets, types,
    /// generated_templates, resource_scans, refactors, etc.) so the
    /// extras handlers can keep state alive without proliferating
    /// per-category fields.
    #[serde(default)]
    pub extras: HashMap<String, HashMap<String, serde_json::Value>>,
    #[serde(default)]
    pub events: HashMap<String, Vec<serde_json::Value>>,
    #[serde(default)]
    pub stack_policies: HashMap<String, String>,
    #[serde(default)]
    pub termination_protection: HashMap<String, bool>,
    #[serde(default)]
    pub orgs_access_enabled: bool,
}

impl CloudFormationState {
    pub fn new(account_id: &str, region: &str) -> Self {
        Self {
            account_id: account_id.to_string(),
            region: region.to_string(),
            stacks: HashMap::new(),
            extras: HashMap::new(),
            events: HashMap::new(),
            stack_policies: HashMap::new(),
            termination_protection: HashMap::new(),
            orgs_access_enabled: false,
        }
    }

    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;
    }
}

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: HashMap::new(),
                tags: HashMap::new(),
                created_at: Utc::now(),
                updated_at: None,
                description: None,
                notification_arns: vec![],
            },
        );
        state.reset();
        assert!(state.stacks.is_empty());
    }
}