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}
245
246#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
247pub struct SsmAssociationVersion {
248    pub version: i64,
249    pub name: String,
250    pub targets: Vec<serde_json::Value>,
251    pub schedule_expression: Option<String>,
252    pub parameters: BTreeMap<String, Vec<String>>,
253    pub document_version: Option<String>,
254    pub created_date: DateTime<Utc>,
255    pub association_name: Option<String>,
256    pub max_errors: Option<String>,
257    pub max_concurrency: Option<String>,
258    pub compliance_severity: Option<String>,
259}
260
261#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
262pub struct SsmOpsItem {
263    pub ops_item_id: String,
264    pub title: String,
265    pub description: Option<String>,
266    pub source: String,
267    pub status: String,
268    pub priority: Option<i64>,
269    pub severity: Option<String>,
270    pub category: Option<String>,
271    pub operational_data: BTreeMap<String, serde_json::Value>,
272    pub notifications: Vec<serde_json::Value>,
273    pub related_ops_items: Vec<serde_json::Value>,
274    pub tags: BTreeMap<String, String>,
275    pub created_time: DateTime<Utc>,
276    pub last_modified_time: DateTime<Utc>,
277    pub created_by: String,
278    pub last_modified_by: String,
279    pub ops_item_type: Option<String>,
280    pub planned_start_time: Option<DateTime<Utc>>,
281    pub planned_end_time: Option<DateTime<Utc>>,
282    pub actual_start_time: Option<DateTime<Utc>>,
283    pub actual_end_time: Option<DateTime<Utc>>,
284}
285
286#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
287pub struct SsmResourcePolicy {
288    pub policy_id: String,
289    pub policy_hash: String,
290    pub policy: String,
291    pub resource_arn: String,
292}
293
294#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
295pub struct SsmServiceSetting {
296    pub setting_id: String,
297    pub setting_value: String,
298    pub last_modified_date: DateTime<Utc>,
299    pub last_modified_user: String,
300    pub status: String,
301}
302
303#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
304pub struct OpsItemRelatedItem {
305    pub association_id: String,
306    pub ops_item_id: String,
307    pub association_type: String,
308    pub resource_type: String,
309    pub resource_uri: String,
310    pub created_time: DateTime<Utc>,
311    pub created_by: String,
312    pub last_modified_time: DateTime<Utc>,
313    pub last_modified_by: String,
314}
315
316#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
317pub struct OpsItemEvent {
318    pub ops_item_id: String,
319    pub event_id: String,
320    pub source: String,
321    pub detail_type: String,
322    pub created_time: DateTime<Utc>,
323    pub created_by: String,
324}
325
326#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
327pub struct OpsMetadataEntry {
328    pub ops_metadata_arn: String,
329    pub resource_id: String,
330    pub metadata: BTreeMap<String, serde_json::Value>,
331    pub creation_date: DateTime<Utc>,
332}
333
334#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
335pub struct AutomationExecution {
336    pub automation_execution_id: String,
337    pub document_name: String,
338    pub document_version: Option<String>,
339    pub automation_execution_status: String,
340    pub execution_start_time: DateTime<Utc>,
341    pub execution_end_time: Option<DateTime<Utc>>,
342    pub parameters: BTreeMap<String, Vec<String>>,
343    pub outputs: BTreeMap<String, Vec<String>>,
344    pub mode: String,
345    pub target: Option<String>,
346    pub targets: Vec<serde_json::Value>,
347    pub max_concurrency: Option<String>,
348    pub max_errors: Option<String>,
349    pub executed_by: String,
350    pub step_executions: Vec<AutomationStepExecution>,
351    pub automation_subtype: Option<String>,
352    pub runbooks: Vec<serde_json::Value>,
353    pub change_request_name: Option<String>,
354    pub scheduled_time: Option<DateTime<Utc>>,
355}
356
357#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
358pub struct AutomationStepExecution {
359    pub step_name: String,
360    pub action: String,
361    pub step_status: String,
362    pub execution_start_time: Option<DateTime<Utc>>,
363    pub execution_end_time: Option<DateTime<Utc>>,
364    pub inputs: BTreeMap<String, String>,
365    pub outputs: BTreeMap<String, Vec<String>>,
366    pub step_execution_id: String,
367}
368
369#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
370pub struct SsmSession {
371    pub session_id: String,
372    pub target: String,
373    pub status: String,
374    pub start_date: DateTime<Utc>,
375    pub end_date: Option<DateTime<Utc>>,
376    pub owner: String,
377    pub reason: Option<String>,
378}
379
380#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
381pub struct SsmActivation {
382    pub activation_id: String,
383    pub iam_role: String,
384    pub registration_limit: i64,
385    pub registrations_count: i64,
386    pub expiration_date: Option<DateTime<Utc>>,
387    pub description: Option<String>,
388    pub default_instance_name: Option<String>,
389    pub created_date: DateTime<Utc>,
390    pub expired: bool,
391    pub tags: BTreeMap<String, String>,
392}
393
394#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
395pub struct ManagedInstance {
396    pub instance_id: String,
397    pub activation_id: Option<String>,
398    pub iam_role: String,
399    pub ping_status: String,
400    pub platform_type: String,
401    pub platform_name: String,
402    pub platform_version: String,
403    pub agent_version: String,
404    pub last_ping_date_time: DateTime<Utc>,
405    pub registration_date: DateTime<Utc>,
406    pub resource_type: String,
407    pub computer_name: String,
408    pub ip_address: String,
409    pub is_latest_version: bool,
410    pub association_status: Option<String>,
411    pub source_id: Option<String>,
412    pub source_type: Option<String>,
413}
414
415#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
416pub struct ExecutionPreview {
417    pub execution_preview_id: String,
418    pub document_name: String,
419    pub status: String,
420    pub created_time: DateTime<Utc>,
421}
422
423#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
424pub struct SsmState {
425    pub account_id: String,
426    pub region: String,
427    pub parameters: BTreeMap<String, SsmParameter>, // name -> param (BTreeMap for path queries)
428    pub documents: BTreeMap<String, SsmDocument>,
429    pub commands: Vec<SsmCommand>,
430    pub maintenance_windows: BTreeMap<String, MaintenanceWindow>,
431    pub patch_baselines: BTreeMap<String, PatchBaseline>,
432    pub patch_groups: Vec<PatchGroup>,
433    pub associations: BTreeMap<String, SsmAssociation>,
434    pub ops_items: BTreeMap<String, SsmOpsItem>,
435    pub resource_policies: Vec<SsmResourcePolicy>,
436    pub service_settings: BTreeMap<String, SsmServiceSetting>,
437    pub default_patch_baseline_id: Option<String>,
438    pub ops_item_counter: u64,
439    pub maintenance_window_executions: Vec<MaintenanceWindowExecution>,
440    pub inventory_entries: BTreeMap<String, InventoryEntry>, // instance_id -> entry
441    pub inventory_deletions: Vec<InventoryDeletion>,
442    pub compliance_items: Vec<ComplianceItem>,
443    pub resource_data_syncs: BTreeMap<String, ResourceDataSync>,
444    pub mw_execution_counter: u64,
445    pub inventory_deletion_counter: u64,
446    pub ops_item_related_items: Vec<OpsItemRelatedItem>,
447    pub ops_item_related_item_counter: u64,
448    pub ops_item_events: Vec<OpsItemEvent>,
449    pub ops_metadata: BTreeMap<String, OpsMetadataEntry>,
450    pub automation_executions: BTreeMap<String, AutomationExecution>,
451    pub automation_execution_counter: u64,
452    pub sessions: BTreeMap<String, SsmSession>,
453    pub session_counter: u64,
454    pub activations: BTreeMap<String, SsmActivation>,
455    pub activation_counter: u64,
456    pub managed_instances: BTreeMap<String, ManagedInstance>,
457    pub execution_previews: BTreeMap<String, ExecutionPreview>,
458    pub execution_preview_counter: u64,
459    /// Local log of parameter-policy notification events. Real AWS sends
460    /// these to EventBridge; we record them in-memory so tests can
461    /// inspect notification fan-out via the admin endpoint. Defaults to
462    /// empty when deserializing snapshots from before this field
463    /// existed.
464    #[serde(default)]
465    pub parameter_policy_events: Vec<ParameterPolicyEvent>,
466}
467
468/// One emission of a parameter-policy notification (Expiration/
469/// ExpirationNotification/NoChangeNotification). Captured at PutParameter
470/// time and at read time when an Expiration ages out a parameter.
471#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
472pub struct ParameterPolicyEvent {
473    pub parameter_name: String,
474    pub parameter_arn: String,
475    pub event_type: String,
476    pub message: String,
477    pub created_at: DateTime<Utc>,
478}
479
480impl SsmState {
481    pub fn new(account_id: &str, region: &str) -> Self {
482        let mut state = Self {
483            account_id: account_id.to_string(),
484            region: region.to_string(),
485            parameters: BTreeMap::new(),
486            documents: BTreeMap::new(),
487            commands: Vec::new(),
488            maintenance_windows: BTreeMap::new(),
489            patch_baselines: BTreeMap::new(),
490            patch_groups: Vec::new(),
491            associations: BTreeMap::new(),
492            ops_items: BTreeMap::new(),
493            resource_policies: Vec::new(),
494            service_settings: BTreeMap::new(),
495            default_patch_baseline_id: None,
496            ops_item_counter: 0,
497            maintenance_window_executions: Vec::new(),
498            inventory_entries: BTreeMap::new(),
499            inventory_deletions: Vec::new(),
500            compliance_items: Vec::new(),
501            resource_data_syncs: BTreeMap::new(),
502            mw_execution_counter: 0,
503            inventory_deletion_counter: 0,
504            ops_item_related_items: Vec::new(),
505            ops_item_related_item_counter: 0,
506            ops_item_events: Vec::new(),
507            ops_metadata: BTreeMap::new(),
508            automation_executions: BTreeMap::new(),
509            automation_execution_counter: 0,
510            sessions: BTreeMap::new(),
511            session_counter: 0,
512            activations: BTreeMap::new(),
513            activation_counter: 0,
514            managed_instances: BTreeMap::new(),
515            execution_previews: BTreeMap::new(),
516            execution_preview_counter: 0,
517            parameter_policy_events: Vec::new(),
518        };
519        state.seed_defaults();
520        state
521    }
522
523    pub fn reset(&mut self) {
524        self.parameters.clear();
525        self.documents.clear();
526        self.commands.clear();
527        self.maintenance_windows.clear();
528        self.patch_baselines.clear();
529        self.patch_groups.clear();
530        self.associations.clear();
531        self.ops_items.clear();
532        self.resource_policies.clear();
533        self.service_settings.clear();
534        self.default_patch_baseline_id = None;
535        self.ops_item_counter = 0;
536        self.maintenance_window_executions.clear();
537        self.inventory_entries.clear();
538        self.inventory_deletions.clear();
539        self.compliance_items.clear();
540        self.resource_data_syncs.clear();
541        self.mw_execution_counter = 0;
542        self.inventory_deletion_counter = 0;
543        self.ops_item_related_items.clear();
544        self.ops_item_related_item_counter = 0;
545        self.ops_item_events.clear();
546        self.ops_metadata.clear();
547        self.automation_executions.clear();
548        self.automation_execution_counter = 0;
549        self.sessions.clear();
550        self.session_counter = 0;
551        self.activations.clear();
552        self.activation_counter = 0;
553        self.managed_instances.clear();
554        self.execution_previews.clear();
555        self.execution_preview_counter = 0;
556        self.parameter_policy_events.clear();
557        self.seed_defaults();
558    }
559
560    fn seed_defaults(&mut self) {
561        let now = chrono::Utc::now();
562
563        // Seed region parameters
564        let regions: &[(&str, &str)] = &[
565            ("af-south-1", "Africa (Cape Town)"),
566            ("ap-east-1", "Asia Pacific (Hong Kong)"),
567            ("ap-northeast-1", "Asia Pacific (Tokyo)"),
568            ("ap-northeast-2", "Asia Pacific (Seoul)"),
569            ("ap-northeast-3", "Asia Pacific (Osaka)"),
570            ("ap-south-1", "Asia Pacific (Mumbai)"),
571            ("ap-south-2", "Asia Pacific (Hyderabad)"),
572            ("ap-southeast-1", "Asia Pacific (Singapore)"),
573            ("ap-southeast-2", "Asia Pacific (Sydney)"),
574            ("ap-southeast-3", "Asia Pacific (Jakarta)"),
575            ("ca-central-1", "Canada (Central)"),
576            ("eu-central-1", "Europe (Frankfurt)"),
577            ("eu-central-2", "Europe (Zurich)"),
578            ("eu-north-1", "Europe (Stockholm)"),
579            ("eu-south-1", "Europe (Milan)"),
580            ("eu-south-2", "Europe (Spain)"),
581            ("eu-west-1", "Europe (Ireland)"),
582            ("eu-west-2", "Europe (London)"),
583            ("eu-west-3", "Europe (Paris)"),
584            ("me-central-1", "Middle East (UAE)"),
585            ("me-south-1", "Middle East (Bahrain)"),
586            ("sa-east-1", "South America (Sao Paulo)"),
587            ("us-east-1", "US East (N. Virginia)"),
588            ("us-east-2", "US East (Ohio)"),
589            ("us-west-1", "US West (N. California)"),
590            ("us-west-2", "US West (Oregon)"),
591        ];
592
593        for (region_code, long_name) in regions {
594            let base_path = format!("/aws/service/global-infrastructure/regions/{region_code}");
595            self.insert_default_param(&base_path, region_code, now);
596            self.insert_default_param(&format!("{base_path}/longName"), long_name, now);
597            self.insert_default_param(&format!("{base_path}/domain"), "amazonaws.com", now);
598            self.insert_default_param(&format!("{base_path}/geolocationRegion"), region_code, now);
599            let country = match region_code.split('-').next().unwrap_or("") {
600                "us" => "US",
601                "eu" => "DE",
602                "ap" => "JP",
603                "sa" => "BR",
604                "ca" => "CA",
605                "me" => "BH",
606                "af" => "ZA",
607                "il" => "IL",
608                _ => "US",
609            };
610            self.insert_default_param(&format!("{base_path}/geolocationCountry"), country, now);
611            self.insert_default_param(&format!("{base_path}/partition"), "aws", now);
612        }
613
614        // Seed service parameters
615        let services = [
616            "acm",
617            "apigateway",
618            "autoscaling",
619            "cloudformation",
620            "cloudfront",
621            "cloudwatch",
622            "codebuild",
623            "codecommit",
624            "codedeploy",
625            "dynamodb",
626            "ec2",
627            "ecr",
628            "ecs",
629            "eks",
630            "elasticache",
631            "elasticbeanstalk",
632            "elasticloadbalancing",
633            "es",
634            "events",
635            "firehose",
636            "iam",
637            "kinesis",
638            "kms",
639            "lambda",
640            "logs",
641            "rds",
642            "redshift",
643            "route53",
644            "s3",
645            "ses",
646            "sns",
647            "sqs",
648            "ssm",
649            "sts",
650        ];
651        for svc in &services {
652            let name = format!("/aws/service/global-infrastructure/services/{svc}");
653            self.insert_default_param(&name, svc, now);
654        }
655
656        // Seed AMI parameters (10 entries per region)
657        let ami_names = [
658            "al2023-ami-kernel-default-x86_64",
659            "al2023-ami-kernel-default-arm64",
660            "al2023-ami-minimal-kernel-default-x86_64",
661            "al2023-ami-minimal-kernel-default-arm64",
662            "amzn2-ami-hvm-x86_64-gp2",
663            "amzn2-ami-hvm-arm64-gp2",
664            "amzn2-ami-kernel-5.10-hvm-x86_64-gp2",
665            "amzn2-ami-kernel-5.10-hvm-arm64-gp2",
666            "amzn2-ami-minimal-hvm-x86_64-ebs",
667            "amzn2-ami-minimal-hvm-arm64-ebs",
668        ];
669
670        // Generate region-specific AMI IDs using a simple hash
671        for (i, ami_name) in ami_names.iter().enumerate() {
672            let name = format!("/aws/service/ami-amazon-linux-latest/{ami_name}");
673            let ami_id = format!(
674                "ami-{:017x}",
675                // Simple region-specific hash
676                {
677                    let mut h: u64 = 0xcbf29ce484222325;
678                    for b in self.region.as_bytes() {
679                        h ^= *b as u64;
680                        h = h.wrapping_mul(0x100000001b3);
681                    }
682                    for b in ami_name.as_bytes() {
683                        h ^= *b as u64;
684                        h = h.wrapping_mul(0x100000001b3);
685                    }
686                    h.wrapping_add(i as u64)
687                }
688            );
689            self.insert_default_param(&name, &ami_id, now);
690        }
691    }
692
693    fn insert_default_param(&mut self, name: &str, value: &str, now: DateTime<Utc>) {
694        let arn = if name.starts_with('/') {
695            format!(
696                "arn:aws:ssm:{}:{}:parameter{}",
697                self.region, self.account_id, name
698            )
699        } else {
700            format!(
701                "arn:aws:ssm:{}:{}:parameter/{}",
702                self.region, self.account_id, name
703            )
704        };
705        self.parameters.insert(
706            name.to_string(),
707            SsmParameter {
708                name: name.to_string(),
709                value: value.to_string(),
710                param_type: "String".to_string(),
711                version: 1,
712                arn,
713                last_modified: now,
714                history: Vec::new(),
715                tags: BTreeMap::new(),
716                labels: BTreeMap::new(),
717                description: None,
718                allowed_pattern: None,
719                key_id: None,
720                data_type: "text".to_string(),
721                tier: "Standard".to_string(),
722                policies: None,
723                expiration_notified: false,
724                no_change_notified: false,
725            },
726        );
727    }
728}
729
730#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
731pub struct MaintenanceWindowExecution {
732    pub window_execution_id: String,
733    pub window_id: String,
734    pub status: String,
735    pub start_time: DateTime<Utc>,
736    pub end_time: Option<DateTime<Utc>>,
737    pub tasks: Vec<MaintenanceWindowExecutionTask>,
738}
739
740#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
741pub struct MaintenanceWindowExecutionTask {
742    pub task_execution_id: String,
743    pub window_execution_id: String,
744    pub task_arn: String,
745    pub task_type: String,
746    pub status: String,
747    pub start_time: DateTime<Utc>,
748    pub end_time: Option<DateTime<Utc>>,
749    pub invocations: Vec<MaintenanceWindowExecutionTaskInvocation>,
750}
751
752#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
753pub struct MaintenanceWindowExecutionTaskInvocation {
754    pub invocation_id: String,
755    pub task_execution_id: String,
756    pub window_execution_id: String,
757    pub execution_id: Option<String>,
758    pub status: String,
759    pub start_time: DateTime<Utc>,
760    pub end_time: Option<DateTime<Utc>>,
761    pub parameters: Option<String>,
762    pub owner_information: Option<String>,
763    pub window_target_id: Option<String>,
764    pub status_details: Option<String>,
765}
766
767#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
768pub struct InventoryItem {
769    pub type_name: String,
770    pub schema_version: String,
771    pub capture_time: String,
772    pub content: Vec<BTreeMap<String, String>>,
773    pub content_hash: Option<String>,
774    pub context: Option<BTreeMap<String, String>>,
775}
776
777#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
778pub struct InventoryEntry {
779    pub instance_id: String,
780    pub items: Vec<InventoryItem>,
781}
782
783#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
784pub struct InventoryDeletion {
785    pub deletion_id: String,
786    pub type_name: String,
787    pub deletion_start_time: DateTime<Utc>,
788    pub last_status: String,
789    pub last_status_message: String,
790    pub deletion_summary: serde_json::Value,
791    pub last_status_update_time: DateTime<Utc>,
792}
793
794#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
795pub struct ComplianceItem {
796    pub resource_id: String,
797    pub resource_type: String,
798    pub compliance_type: String,
799    pub severity: String,
800    pub status: String,
801    pub title: Option<String>,
802    pub id: Option<String>,
803    pub details: BTreeMap<String, String>,
804    pub execution_summary: serde_json::Value,
805}
806
807#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
808pub struct ResourceDataSync {
809    pub sync_name: String,
810    pub sync_type: Option<String>,
811    pub sync_source: Option<serde_json::Value>,
812    pub s3_destination: Option<serde_json::Value>,
813    pub created_date: DateTime<Utc>,
814    pub last_sync_time: Option<DateTime<Utc>>,
815    pub last_successful_sync_time: Option<DateTime<Utc>>,
816    pub last_status: String,
817    pub sync_last_modified_time: DateTime<Utc>,
818}
819
820pub type SharedSsmState = Arc<RwLock<fakecloud_core::multi_account::MultiAccountState<SsmState>>>;
821
822impl fakecloud_core::multi_account::AccountState for SsmState {
823    fn new_for_account(account_id: &str, region: &str, _endpoint: &str) -> Self {
824        Self::new(account_id, region)
825    }
826}
827
828/// On-disk snapshot envelope for SSM state. Versioned so format
829/// changes fail loudly on upgrade.
830#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
831pub struct SsmSnapshot {
832    pub schema_version: u32,
833    #[serde(default)]
834    pub accounts: Option<fakecloud_core::multi_account::MultiAccountState<SsmState>>,
835    #[serde(default)]
836    pub state: Option<SsmState>,
837}
838
839pub const SSM_SNAPSHOT_SCHEMA_VERSION: u32 = 2;
840
841#[cfg(test)]
842mod tests {
843    use super::*;
844
845    #[test]
846    fn new_initializes() {
847        let state = SsmState::new("123456789012", "us-east-1");
848        assert_eq!(state.account_id, "123456789012");
849        assert_eq!(state.region, "us-east-1");
850    }
851
852    #[test]
853    fn new_seeds_default_region_parameters() {
854        let state = SsmState::new("123456789012", "us-east-1");
855        let region_key = "/aws/service/global-infrastructure/regions/us-east-1";
856        assert!(state.parameters.contains_key(region_key));
857        let long_key = format!("{region_key}/longName");
858        assert!(state.parameters.contains_key(&long_key));
859    }
860
861    #[test]
862    fn new_seeds_default_service_parameters() {
863        let state = SsmState::new("123456789012", "us-east-1");
864        let key = "/aws/service/global-infrastructure/services/lambda";
865        assert!(state.parameters.contains_key(key));
866    }
867
868    #[test]
869    fn reset_reseeds_defaults() {
870        let mut state = SsmState::new("123456789012", "us-east-1");
871        state.parameters.clear();
872        state.documents.clear();
873        state.ops_item_counter = 42;
874        state.reset();
875        // Defaults re-seeded
876        let key = "/aws/service/global-infrastructure/services/s3";
877        assert!(state.parameters.contains_key(key));
878        assert_eq!(state.ops_item_counter, 0);
879    }
880
881    #[test]
882    fn reset_clears_ephemeral_counters() {
883        let mut state = SsmState::new("123456789012", "us-east-1");
884        state.mw_execution_counter = 7;
885        state.automation_execution_counter = 3;
886        state.session_counter = 9;
887        state.activation_counter = 2;
888        state.execution_preview_counter = 5;
889        state.reset();
890        assert_eq!(state.mw_execution_counter, 0);
891        assert_eq!(state.automation_execution_counter, 0);
892        assert_eq!(state.session_counter, 0);
893        assert_eq!(state.activation_counter, 0);
894        assert_eq!(state.execution_preview_counter, 0);
895    }
896}