Skip to main content

fakecloud_ssm/
state.rs

1use chrono::{DateTime, Utc};
2use parking_lot::RwLock;
3use std::collections::BTreeMap;
4use std::sync::Arc;
5
6#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
7pub struct SsmParameter {
8    pub name: String,
9    pub value: String,
10    pub param_type: String, // String, StringList, SecureString
11    pub version: i64,
12    pub arn: String,
13    pub last_modified: DateTime<Utc>,
14    pub history: Vec<SsmParameterVersion>,
15    pub tags: BTreeMap<String, String>,
16    pub labels: BTreeMap<i64, Vec<String>>, // version -> labels
17    pub description: Option<String>,
18    pub allowed_pattern: Option<String>,
19    pub key_id: Option<String>,
20    pub data_type: String, // "text" or "aws:ec2:image"
21    pub tier: String,      // "Standard", "Advanced", "Intelligent-Tiering"
22    pub policies: Option<String>,
23    /// Whether the `ExpirationNotification` event has already been
24    /// emitted for the current Policies list. Reset whenever the
25    /// parameter is overwritten so updated policies fire fresh
26    /// notifications. Snapshots from before this field existed
27    /// deserialize as `false`.
28    #[serde(default)]
29    pub expiration_notified: bool,
30    /// Whether the `NoChangeNotification` event has already been
31    /// emitted for the current value. Reset whenever the parameter is
32    /// overwritten so the inactivity window restarts on each update.
33    #[serde(default)]
34    pub no_change_notified: bool,
35}
36
37#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
38pub struct SsmParameterVersion {
39    pub value: String,
40    pub version: i64,
41    pub last_modified: DateTime<Utc>,
42    pub param_type: String,
43    pub description: Option<String>,
44    pub key_id: Option<String>,
45    pub labels: Vec<String>,
46}
47
48#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
49pub struct SsmDocument {
50    pub name: String,
51    pub content: String,
52    pub document_type: String,
53    pub document_format: String,
54    pub target_type: Option<String>,
55    pub version_name: Option<String>,
56    pub tags: BTreeMap<String, String>,
57    pub versions: Vec<SsmDocumentVersion>,
58    pub default_version: String,
59    pub latest_version: String,
60    pub created_date: DateTime<Utc>,
61    pub owner: String,
62    pub status: String,
63    pub permissions: BTreeMap<String, Vec<String>>, // permission_type -> account_ids
64    #[serde(default)]
65    pub reviews: Vec<DocumentReview>,
66}
67
68#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
69pub struct DocumentReview {
70    pub reviewer: String,
71    pub action: String, // SendForReview / Approve / Reject
72    pub comment: Vec<DocumentReviewComment>,
73    pub created_time: DateTime<Utc>,
74    pub updated_time: DateTime<Utc>,
75}
76
77#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
78pub struct DocumentReviewComment {
79    pub comment_type: String, // Comment
80    pub content: String,
81}
82
83#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
84pub struct SsmDocumentVersion {
85    pub content: String,
86    pub document_version: String,
87    pub version_name: Option<String>,
88    pub created_date: DateTime<Utc>,
89    pub status: String,
90    pub document_format: String,
91    pub is_default_version: bool,
92}
93
94#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
95pub struct SsmCommand {
96    pub command_id: String,
97    pub document_name: String,
98    pub instance_ids: Vec<String>,
99    pub parameters: BTreeMap<String, Vec<String>>,
100    pub status: String,
101    pub requested_date_time: DateTime<Utc>,
102    /// When the command's results stop being readable. Defaults to
103    /// `requested_date_time + 1h` for snapshots written before this
104    /// field existed so old data still deserializes cleanly.
105    #[serde(default = "default_command_expiry")]
106    pub expires_after: DateTime<Utc>,
107    pub comment: Option<String>,
108    pub output_s3_bucket_name: Option<String>,
109    pub output_s3_key_prefix: Option<String>,
110    pub output_s3_region: Option<String>,
111    pub timeout_seconds: Option<i64>,
112    pub service_role_arn: Option<String>,
113    pub notification_config: Option<serde_json::Value>,
114    pub targets: Vec<serde_json::Value>,
115    pub document_hash: Option<String>,
116    pub document_hash_type: Option<String>,
117    /// Per-instance invocation state. One entry per `InstanceIds`
118    /// member; updated independently by the async transition task or
119    /// by the admin force-fail endpoint.
120    #[serde(default)]
121    pub invocations: Vec<SsmCommandInvocation>,
122}
123
124fn default_command_expiry() -> DateTime<Utc> {
125    chrono::Utc::now() + chrono::Duration::seconds(3600)
126}
127
128/// One execution of a command on a single managed instance. The
129/// real SSM API exposes this via `GetCommandInvocation` and
130/// `ListCommandInvocations`; per-invocation status diverges from the
131/// parent command status when only some instances fail.
132#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
133pub struct SsmCommandInvocation {
134    pub instance_id: String,
135    pub status: String,
136    pub status_details: String,
137    pub standard_output_content: String,
138    pub standard_error_content: String,
139    pub response_code: i64,
140    pub requested_date_time: DateTime<Utc>,
141    pub last_update_at: DateTime<Utc>,
142}
143
144#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
145pub struct MaintenanceWindowTarget {
146    pub window_target_id: String,
147    pub window_id: String,
148    pub resource_type: String,
149    pub targets: Vec<serde_json::Value>,
150    pub name: Option<String>,
151    pub description: Option<String>,
152    pub owner_information: Option<String>,
153}
154
155#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
156pub struct MaintenanceWindowTask {
157    pub window_task_id: String,
158    pub window_id: String,
159    pub task_arn: String,
160    pub task_type: String,
161    pub targets: Vec<serde_json::Value>,
162    pub max_concurrency: Option<String>,
163    pub max_errors: Option<String>,
164    pub priority: i64,
165    pub service_role_arn: Option<String>,
166    pub name: Option<String>,
167    pub description: Option<String>,
168}
169
170#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
171pub struct MaintenanceWindow {
172    pub id: String,
173    pub name: String,
174    pub schedule: String,
175    pub duration: i64,
176    pub cutoff: i64,
177    pub allow_unassociated_targets: bool,
178    pub enabled: bool,
179    pub description: Option<String>,
180    pub tags: BTreeMap<String, String>,
181    pub targets: Vec<MaintenanceWindowTarget>,
182    pub tasks: Vec<MaintenanceWindowTask>,
183    pub schedule_timezone: Option<String>,
184    pub schedule_offset: Option<i64>,
185    pub start_date: Option<String>,
186    pub end_date: Option<String>,
187    pub client_token: Option<String>,
188}
189
190#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
191pub struct PatchBaseline {
192    pub id: String,
193    pub name: String,
194    pub operating_system: String,
195    pub description: Option<String>,
196    pub approval_rules: Option<serde_json::Value>,
197    pub approved_patches: Vec<String>,
198    pub rejected_patches: Vec<String>,
199    pub tags: BTreeMap<String, String>,
200    pub approved_patches_compliance_level: String,
201    pub rejected_patches_action: String,
202    pub global_filters: Option<serde_json::Value>,
203    pub sources: Vec<serde_json::Value>,
204    pub approved_patches_enable_non_security: bool,
205    pub available_security_updates_compliance_status: Option<String>,
206    pub client_token: Option<String>,
207}
208
209#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
210pub struct PatchGroup {
211    pub baseline_id: String,
212    pub patch_group: String,
213}
214
215#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
216pub struct SsmAssociation {
217    pub association_id: String,
218    pub name: String, // document name
219    pub targets: Vec<serde_json::Value>,
220    pub schedule_expression: Option<String>,
221    pub parameters: BTreeMap<String, Vec<String>>,
222    pub association_name: Option<String>,
223    pub document_version: Option<String>,
224    pub output_location: Option<serde_json::Value>,
225    pub automation_target_parameter_name: Option<String>,
226    pub max_errors: Option<String>,
227    pub max_concurrency: Option<String>,
228    pub compliance_severity: Option<String>,
229    pub sync_compliance: Option<String>,
230    pub apply_only_at_cron_interval: bool,
231    pub calendar_names: Vec<String>,
232    pub target_locations: Vec<serde_json::Value>,
233    pub schedule_offset: Option<i64>,
234    pub target_maps: Vec<serde_json::Value>,
235    pub tags: BTreeMap<String, String>,
236    pub status: String,
237    pub status_date: DateTime<Utc>,
238    pub overview: serde_json::Value,
239    pub created_date: DateTime<Utc>,
240    pub last_update_association_date: DateTime<Utc>,
241    pub last_execution_date: Option<DateTime<Utc>>,
242    pub instance_id: Option<String>,
243    pub versions: Vec<SsmAssociationVersion>,
244    /// Recorded executions (StartAssociationsOnce / scheduled applies). Empty
245    /// until the association runs; surfaced by DescribeAssociationExecutions
246    /// and DescribeAssociationExecutionTargets (bug-audit 2026-05-28, 1.15).
247    #[serde(default)]
248    pub executions: Vec<AssociationExecution>,
249}
250
251/// One recorded run of an SSM State Manager association. fakecloud applies
252/// associations synchronously and always succeeds, so every execution is a
253/// `Success` over the association's resolved targets.
254#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
255pub struct AssociationExecution {
256    pub execution_id: String,
257    pub status: String,
258    pub detailed_status: String,
259    pub created_time: DateTime<Utc>,
260    pub resource_count: usize,
261    /// Resolved target resource ids covered by this execution.
262    pub resource_ids: Vec<String>,
263}
264
265#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
266pub struct SsmAssociationVersion {
267    pub version: i64,
268    pub name: String,
269    pub targets: Vec<serde_json::Value>,
270    pub schedule_expression: Option<String>,
271    pub parameters: BTreeMap<String, Vec<String>>,
272    pub document_version: Option<String>,
273    pub created_date: DateTime<Utc>,
274    pub association_name: Option<String>,
275    pub max_errors: Option<String>,
276    pub max_concurrency: Option<String>,
277    pub compliance_severity: Option<String>,
278}
279
280#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
281pub struct SsmOpsItem {
282    pub ops_item_id: String,
283    pub title: String,
284    pub description: Option<String>,
285    pub source: String,
286    pub status: String,
287    pub priority: Option<i64>,
288    pub severity: Option<String>,
289    pub category: Option<String>,
290    pub operational_data: BTreeMap<String, serde_json::Value>,
291    pub notifications: Vec<serde_json::Value>,
292    pub related_ops_items: Vec<serde_json::Value>,
293    pub tags: BTreeMap<String, String>,
294    pub created_time: DateTime<Utc>,
295    pub last_modified_time: DateTime<Utc>,
296    pub created_by: String,
297    pub last_modified_by: String,
298    pub ops_item_type: Option<String>,
299    pub planned_start_time: Option<DateTime<Utc>>,
300    pub planned_end_time: Option<DateTime<Utc>>,
301    pub actual_start_time: Option<DateTime<Utc>>,
302    pub actual_end_time: Option<DateTime<Utc>>,
303}
304
305#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
306pub struct SsmResourcePolicy {
307    pub policy_id: String,
308    pub policy_hash: String,
309    pub policy: String,
310    pub resource_arn: String,
311}
312
313#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
314pub struct SsmServiceSetting {
315    pub setting_id: String,
316    pub setting_value: String,
317    pub last_modified_date: DateTime<Utc>,
318    pub last_modified_user: String,
319    pub status: String,
320}
321
322#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
323pub struct OpsItemRelatedItem {
324    pub association_id: String,
325    pub ops_item_id: String,
326    pub association_type: String,
327    pub resource_type: String,
328    pub resource_uri: String,
329    pub created_time: DateTime<Utc>,
330    pub created_by: String,
331    pub last_modified_time: DateTime<Utc>,
332    pub last_modified_by: String,
333}
334
335#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
336pub struct OpsItemEvent {
337    pub ops_item_id: String,
338    pub event_id: String,
339    pub source: String,
340    pub detail_type: String,
341    pub created_time: DateTime<Utc>,
342    pub created_by: String,
343}
344
345#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
346pub struct OpsMetadataEntry {
347    pub ops_metadata_arn: String,
348    pub resource_id: String,
349    pub metadata: BTreeMap<String, serde_json::Value>,
350    pub creation_date: DateTime<Utc>,
351}
352
353#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
354pub struct AutomationExecution {
355    pub automation_execution_id: String,
356    pub document_name: String,
357    pub document_version: Option<String>,
358    pub automation_execution_status: String,
359    pub execution_start_time: DateTime<Utc>,
360    pub execution_end_time: Option<DateTime<Utc>>,
361    pub parameters: BTreeMap<String, Vec<String>>,
362    pub outputs: BTreeMap<String, Vec<String>>,
363    pub mode: String,
364    pub target: Option<String>,
365    pub targets: Vec<serde_json::Value>,
366    pub max_concurrency: Option<String>,
367    pub max_errors: Option<String>,
368    pub executed_by: String,
369    pub step_executions: Vec<AutomationStepExecution>,
370    pub automation_subtype: Option<String>,
371    pub runbooks: Vec<serde_json::Value>,
372    pub change_request_name: Option<String>,
373    pub scheduled_time: Option<DateTime<Utc>>,
374}
375
376#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
377pub struct AutomationStepExecution {
378    pub step_name: String,
379    pub action: String,
380    pub step_status: String,
381    pub execution_start_time: Option<DateTime<Utc>>,
382    pub execution_end_time: Option<DateTime<Utc>>,
383    pub inputs: BTreeMap<String, String>,
384    pub outputs: BTreeMap<String, Vec<String>>,
385    pub step_execution_id: String,
386}
387
388#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
389pub struct SsmSession {
390    pub session_id: String,
391    pub target: String,
392    pub status: String,
393    pub start_date: DateTime<Utc>,
394    pub end_date: Option<DateTime<Utc>>,
395    pub owner: String,
396    pub reason: Option<String>,
397}
398
399#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
400pub struct SsmActivation {
401    pub activation_id: String,
402    pub iam_role: String,
403    pub registration_limit: i64,
404    pub registrations_count: i64,
405    pub expiration_date: Option<DateTime<Utc>>,
406    pub description: Option<String>,
407    pub default_instance_name: Option<String>,
408    pub created_date: DateTime<Utc>,
409    pub expired: bool,
410    pub tags: BTreeMap<String, String>,
411}
412
413#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
414pub struct ManagedInstance {
415    pub instance_id: String,
416    pub activation_id: Option<String>,
417    pub iam_role: String,
418    pub ping_status: String,
419    pub platform_type: String,
420    pub platform_name: String,
421    pub platform_version: String,
422    pub agent_version: String,
423    pub last_ping_date_time: DateTime<Utc>,
424    pub registration_date: DateTime<Utc>,
425    pub resource_type: String,
426    pub computer_name: String,
427    pub ip_address: String,
428    pub is_latest_version: bool,
429    pub association_status: Option<String>,
430    pub source_id: Option<String>,
431    pub source_type: Option<String>,
432}
433
434#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
435pub struct ExecutionPreview {
436    pub execution_preview_id: String,
437    pub document_name: String,
438    pub status: String,
439    pub created_time: DateTime<Utc>,
440}
441
442#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
443pub struct SsmState {
444    pub account_id: String,
445    pub region: String,
446    pub parameters: BTreeMap<String, SsmParameter>, // name -> param (BTreeMap for path queries)
447    pub documents: BTreeMap<String, SsmDocument>,
448    pub commands: Vec<SsmCommand>,
449    pub maintenance_windows: BTreeMap<String, MaintenanceWindow>,
450    pub patch_baselines: BTreeMap<String, PatchBaseline>,
451    pub patch_groups: Vec<PatchGroup>,
452    pub associations: BTreeMap<String, SsmAssociation>,
453    pub ops_items: BTreeMap<String, SsmOpsItem>,
454    pub resource_policies: Vec<SsmResourcePolicy>,
455    pub service_settings: BTreeMap<String, SsmServiceSetting>,
456    pub default_patch_baseline_id: Option<String>,
457    pub ops_item_counter: u64,
458    pub maintenance_window_executions: Vec<MaintenanceWindowExecution>,
459    pub inventory_entries: BTreeMap<String, InventoryEntry>, // instance_id -> entry
460    pub inventory_deletions: Vec<InventoryDeletion>,
461    pub compliance_items: Vec<ComplianceItem>,
462    pub resource_data_syncs: BTreeMap<String, ResourceDataSync>,
463    pub mw_execution_counter: u64,
464    pub inventory_deletion_counter: u64,
465    pub ops_item_related_items: Vec<OpsItemRelatedItem>,
466    pub ops_item_related_item_counter: u64,
467    pub ops_item_events: Vec<OpsItemEvent>,
468    pub ops_metadata: BTreeMap<String, OpsMetadataEntry>,
469    pub automation_executions: BTreeMap<String, AutomationExecution>,
470    pub automation_execution_counter: u64,
471    pub sessions: BTreeMap<String, SsmSession>,
472    pub session_counter: u64,
473    pub activations: BTreeMap<String, SsmActivation>,
474    pub activation_counter: u64,
475    pub managed_instances: BTreeMap<String, ManagedInstance>,
476    pub execution_previews: BTreeMap<String, ExecutionPreview>,
477    pub execution_preview_counter: u64,
478    /// Local log of parameter-policy notification events. Real AWS sends
479    /// these to EventBridge; we record them in-memory so tests can
480    /// inspect notification fan-out via the admin endpoint. Defaults to
481    /// empty when deserializing snapshots from before this field
482    /// existed.
483    #[serde(default)]
484    pub parameter_policy_events: Vec<ParameterPolicyEvent>,
485}
486
487/// One emission of a parameter-policy notification (Expiration/
488/// ExpirationNotification/NoChangeNotification). Captured at PutParameter
489/// time and at read time when an Expiration ages out a parameter.
490#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
491pub struct ParameterPolicyEvent {
492    pub parameter_name: String,
493    pub parameter_arn: String,
494    pub event_type: String,
495    pub message: String,
496    pub created_at: DateTime<Utc>,
497}
498
499impl SsmState {
500    pub fn new(account_id: &str, region: &str) -> Self {
501        let mut state = Self {
502            account_id: account_id.to_string(),
503            region: region.to_string(),
504            parameters: BTreeMap::new(),
505            documents: BTreeMap::new(),
506            commands: Vec::new(),
507            maintenance_windows: BTreeMap::new(),
508            patch_baselines: BTreeMap::new(),
509            patch_groups: Vec::new(),
510            associations: BTreeMap::new(),
511            ops_items: BTreeMap::new(),
512            resource_policies: Vec::new(),
513            service_settings: BTreeMap::new(),
514            default_patch_baseline_id: None,
515            ops_item_counter: 0,
516            maintenance_window_executions: Vec::new(),
517            inventory_entries: BTreeMap::new(),
518            inventory_deletions: Vec::new(),
519            compliance_items: Vec::new(),
520            resource_data_syncs: BTreeMap::new(),
521            mw_execution_counter: 0,
522            inventory_deletion_counter: 0,
523            ops_item_related_items: Vec::new(),
524            ops_item_related_item_counter: 0,
525            ops_item_events: Vec::new(),
526            ops_metadata: BTreeMap::new(),
527            automation_executions: BTreeMap::new(),
528            automation_execution_counter: 0,
529            sessions: BTreeMap::new(),
530            session_counter: 0,
531            activations: BTreeMap::new(),
532            activation_counter: 0,
533            managed_instances: BTreeMap::new(),
534            execution_previews: BTreeMap::new(),
535            execution_preview_counter: 0,
536            parameter_policy_events: Vec::new(),
537        };
538        state.seed_defaults();
539        state
540    }
541
542    pub fn reset(&mut self) {
543        self.parameters.clear();
544        self.documents.clear();
545        self.commands.clear();
546        self.maintenance_windows.clear();
547        self.patch_baselines.clear();
548        self.patch_groups.clear();
549        self.associations.clear();
550        self.ops_items.clear();
551        self.resource_policies.clear();
552        self.service_settings.clear();
553        self.default_patch_baseline_id = None;
554        self.ops_item_counter = 0;
555        self.maintenance_window_executions.clear();
556        self.inventory_entries.clear();
557        self.inventory_deletions.clear();
558        self.compliance_items.clear();
559        self.resource_data_syncs.clear();
560        self.mw_execution_counter = 0;
561        self.inventory_deletion_counter = 0;
562        self.ops_item_related_items.clear();
563        self.ops_item_related_item_counter = 0;
564        self.ops_item_events.clear();
565        self.ops_metadata.clear();
566        self.automation_executions.clear();
567        self.automation_execution_counter = 0;
568        self.sessions.clear();
569        self.session_counter = 0;
570        self.activations.clear();
571        self.activation_counter = 0;
572        self.managed_instances.clear();
573        self.execution_previews.clear();
574        self.execution_preview_counter = 0;
575        self.parameter_policy_events.clear();
576        self.seed_defaults();
577    }
578
579    fn seed_defaults(&mut self) {
580        let now = chrono::Utc::now();
581
582        // Seed region parameters
583        let regions: &[(&str, &str)] = &[
584            ("af-south-1", "Africa (Cape Town)"),
585            ("ap-east-1", "Asia Pacific (Hong Kong)"),
586            ("ap-northeast-1", "Asia Pacific (Tokyo)"),
587            ("ap-northeast-2", "Asia Pacific (Seoul)"),
588            ("ap-northeast-3", "Asia Pacific (Osaka)"),
589            ("ap-south-1", "Asia Pacific (Mumbai)"),
590            ("ap-south-2", "Asia Pacific (Hyderabad)"),
591            ("ap-southeast-1", "Asia Pacific (Singapore)"),
592            ("ap-southeast-2", "Asia Pacific (Sydney)"),
593            ("ap-southeast-3", "Asia Pacific (Jakarta)"),
594            ("ca-central-1", "Canada (Central)"),
595            ("eu-central-1", "Europe (Frankfurt)"),
596            ("eu-central-2", "Europe (Zurich)"),
597            ("eu-north-1", "Europe (Stockholm)"),
598            ("eu-south-1", "Europe (Milan)"),
599            ("eu-south-2", "Europe (Spain)"),
600            ("eu-west-1", "Europe (Ireland)"),
601            ("eu-west-2", "Europe (London)"),
602            ("eu-west-3", "Europe (Paris)"),
603            ("me-central-1", "Middle East (UAE)"),
604            ("me-south-1", "Middle East (Bahrain)"),
605            ("sa-east-1", "South America (Sao Paulo)"),
606            ("us-east-1", "US East (N. Virginia)"),
607            ("us-east-2", "US East (Ohio)"),
608            ("us-west-1", "US West (N. California)"),
609            ("us-west-2", "US West (Oregon)"),
610        ];
611
612        for (region_code, long_name) in regions {
613            let base_path = format!("/aws/service/global-infrastructure/regions/{region_code}");
614            self.insert_default_param(&base_path, region_code, now);
615            self.insert_default_param(&format!("{base_path}/longName"), long_name, now);
616            self.insert_default_param(&format!("{base_path}/domain"), "amazonaws.com", now);
617            self.insert_default_param(&format!("{base_path}/geolocationRegion"), region_code, now);
618            let country = match region_code.split('-').next().unwrap_or("") {
619                "us" => "US",
620                "eu" => "DE",
621                "ap" => "JP",
622                "sa" => "BR",
623                "ca" => "CA",
624                "me" => "BH",
625                "af" => "ZA",
626                "il" => "IL",
627                _ => "US",
628            };
629            self.insert_default_param(&format!("{base_path}/geolocationCountry"), country, now);
630            self.insert_default_param(&format!("{base_path}/partition"), "aws", now);
631        }
632
633        // Seed service parameters
634        let services = [
635            "acm",
636            "apigateway",
637            "autoscaling",
638            "cloudformation",
639            "cloudfront",
640            "cloudwatch",
641            "codebuild",
642            "codecommit",
643            "codedeploy",
644            "dynamodb",
645            "ec2",
646            "ecr",
647            "ecs",
648            "eks",
649            "elasticache",
650            "elasticbeanstalk",
651            "elasticloadbalancing",
652            "es",
653            "events",
654            "firehose",
655            "iam",
656            "kinesis",
657            "kms",
658            "lambda",
659            "logs",
660            "rds",
661            "redshift",
662            "route53",
663            "s3",
664            "ses",
665            "sns",
666            "sqs",
667            "ssm",
668            "sts",
669        ];
670        for svc in &services {
671            let name = format!("/aws/service/global-infrastructure/services/{svc}");
672            self.insert_default_param(&name, svc, now);
673        }
674
675        // Seed AMI parameters (10 entries per region)
676        let ami_names = [
677            "al2023-ami-kernel-default-x86_64",
678            "al2023-ami-kernel-default-arm64",
679            "al2023-ami-minimal-kernel-default-x86_64",
680            "al2023-ami-minimal-kernel-default-arm64",
681            "amzn2-ami-hvm-x86_64-gp2",
682            "amzn2-ami-hvm-arm64-gp2",
683            "amzn2-ami-kernel-5.10-hvm-x86_64-gp2",
684            "amzn2-ami-kernel-5.10-hvm-arm64-gp2",
685            "amzn2-ami-minimal-hvm-x86_64-ebs",
686            "amzn2-ami-minimal-hvm-arm64-ebs",
687        ];
688
689        // Generate region-specific AMI IDs using a simple hash
690        for (i, ami_name) in ami_names.iter().enumerate() {
691            let name = format!("/aws/service/ami-amazon-linux-latest/{ami_name}");
692            let ami_id = format!(
693                "ami-{:017x}",
694                // Simple region-specific hash
695                {
696                    let mut h: u64 = 0xcbf29ce484222325;
697                    for b in self.region.as_bytes() {
698                        h ^= *b as u64;
699                        h = h.wrapping_mul(0x100000001b3);
700                    }
701                    for b in ami_name.as_bytes() {
702                        h ^= *b as u64;
703                        h = h.wrapping_mul(0x100000001b3);
704                    }
705                    h.wrapping_add(i as u64)
706                }
707            );
708            self.insert_default_param(&name, &ami_id, now);
709        }
710    }
711
712    fn insert_default_param(&mut self, name: &str, value: &str, now: DateTime<Utc>) {
713        let arn = if name.starts_with('/') {
714            format!(
715                "arn:aws:ssm:{}:{}:parameter{}",
716                self.region, self.account_id, name
717            )
718        } else {
719            format!(
720                "arn:aws:ssm:{}:{}:parameter/{}",
721                self.region, self.account_id, name
722            )
723        };
724        self.parameters.insert(
725            name.to_string(),
726            SsmParameter {
727                name: name.to_string(),
728                value: value.to_string(),
729                param_type: "String".to_string(),
730                version: 1,
731                arn,
732                last_modified: now,
733                history: Vec::new(),
734                tags: BTreeMap::new(),
735                labels: BTreeMap::new(),
736                description: None,
737                allowed_pattern: None,
738                key_id: None,
739                data_type: "text".to_string(),
740                tier: "Standard".to_string(),
741                policies: None,
742                expiration_notified: false,
743                no_change_notified: false,
744            },
745        );
746    }
747}
748
749#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
750pub struct MaintenanceWindowExecution {
751    pub window_execution_id: String,
752    pub window_id: String,
753    pub status: String,
754    pub start_time: DateTime<Utc>,
755    pub end_time: Option<DateTime<Utc>>,
756    pub tasks: Vec<MaintenanceWindowExecutionTask>,
757}
758
759#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
760pub struct MaintenanceWindowExecutionTask {
761    pub task_execution_id: String,
762    pub window_execution_id: String,
763    pub task_arn: String,
764    pub task_type: String,
765    pub status: String,
766    pub start_time: DateTime<Utc>,
767    pub end_time: Option<DateTime<Utc>>,
768    pub invocations: Vec<MaintenanceWindowExecutionTaskInvocation>,
769}
770
771#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
772pub struct MaintenanceWindowExecutionTaskInvocation {
773    pub invocation_id: String,
774    pub task_execution_id: String,
775    pub window_execution_id: String,
776    pub execution_id: Option<String>,
777    pub status: String,
778    pub start_time: DateTime<Utc>,
779    pub end_time: Option<DateTime<Utc>>,
780    pub parameters: Option<String>,
781    pub owner_information: Option<String>,
782    pub window_target_id: Option<String>,
783    pub status_details: Option<String>,
784}
785
786#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
787pub struct InventoryItem {
788    pub type_name: String,
789    pub schema_version: String,
790    pub capture_time: String,
791    pub content: Vec<BTreeMap<String, String>>,
792    pub content_hash: Option<String>,
793    pub context: Option<BTreeMap<String, String>>,
794}
795
796#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
797pub struct InventoryEntry {
798    pub instance_id: String,
799    pub items: Vec<InventoryItem>,
800}
801
802#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
803pub struct InventoryDeletion {
804    pub deletion_id: String,
805    pub type_name: String,
806    pub deletion_start_time: DateTime<Utc>,
807    pub last_status: String,
808    pub last_status_message: String,
809    pub deletion_summary: serde_json::Value,
810    pub last_status_update_time: DateTime<Utc>,
811}
812
813#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
814pub struct ComplianceItem {
815    pub resource_id: String,
816    pub resource_type: String,
817    pub compliance_type: String,
818    pub severity: String,
819    pub status: String,
820    pub title: Option<String>,
821    pub id: Option<String>,
822    pub details: BTreeMap<String, String>,
823    pub execution_summary: serde_json::Value,
824}
825
826#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
827pub struct ResourceDataSync {
828    pub sync_name: String,
829    pub sync_type: Option<String>,
830    pub sync_source: Option<serde_json::Value>,
831    pub s3_destination: Option<serde_json::Value>,
832    pub created_date: DateTime<Utc>,
833    pub last_sync_time: Option<DateTime<Utc>>,
834    pub last_successful_sync_time: Option<DateTime<Utc>>,
835    pub last_status: String,
836    pub sync_last_modified_time: DateTime<Utc>,
837}
838
839pub type SharedSsmState = Arc<RwLock<fakecloud_core::multi_account::MultiAccountState<SsmState>>>;
840
841impl fakecloud_core::multi_account::AccountState for SsmState {
842    fn new_for_account(account_id: &str, region: &str, _endpoint: &str) -> Self {
843        Self::new(account_id, region)
844    }
845}
846
847/// On-disk snapshot envelope for SSM state. Versioned so format
848/// changes fail loudly on upgrade.
849#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
850pub struct SsmSnapshot {
851    pub schema_version: u32,
852    #[serde(default)]
853    pub accounts: Option<fakecloud_core::multi_account::MultiAccountState<SsmState>>,
854    #[serde(default)]
855    pub state: Option<SsmState>,
856}
857
858pub const SSM_SNAPSHOT_SCHEMA_VERSION: u32 = 2;
859
860#[cfg(test)]
861mod tests {
862    use super::*;
863
864    #[test]
865    fn new_initializes() {
866        let state = SsmState::new("123456789012", "us-east-1");
867        assert_eq!(state.account_id, "123456789012");
868        assert_eq!(state.region, "us-east-1");
869    }
870
871    #[test]
872    fn new_seeds_default_region_parameters() {
873        let state = SsmState::new("123456789012", "us-east-1");
874        let region_key = "/aws/service/global-infrastructure/regions/us-east-1";
875        assert!(state.parameters.contains_key(region_key));
876        let long_key = format!("{region_key}/longName");
877        assert!(state.parameters.contains_key(&long_key));
878    }
879
880    #[test]
881    fn new_seeds_default_service_parameters() {
882        let state = SsmState::new("123456789012", "us-east-1");
883        let key = "/aws/service/global-infrastructure/services/lambda";
884        assert!(state.parameters.contains_key(key));
885    }
886
887    #[test]
888    fn reset_reseeds_defaults() {
889        let mut state = SsmState::new("123456789012", "us-east-1");
890        state.parameters.clear();
891        state.documents.clear();
892        state.ops_item_counter = 42;
893        state.reset();
894        // Defaults re-seeded
895        let key = "/aws/service/global-infrastructure/services/s3";
896        assert!(state.parameters.contains_key(key));
897        assert_eq!(state.ops_item_counter, 0);
898    }
899
900    #[test]
901    fn reset_clears_ephemeral_counters() {
902        let mut state = SsmState::new("123456789012", "us-east-1");
903        state.mw_execution_counter = 7;
904        state.automation_execution_counter = 3;
905        state.session_counter = 9;
906        state.activation_counter = 2;
907        state.execution_preview_counter = 5;
908        state.reset();
909        assert_eq!(state.mw_execution_counter, 0);
910        assert_eq!(state.automation_execution_counter, 0);
911        assert_eq!(state.session_counter, 0);
912        assert_eq!(state.activation_counter, 0);
913        assert_eq!(state.execution_preview_counter, 0);
914    }
915}