1mod associations;
2mod automation;
3mod commands;
4mod compliance;
5mod documents;
6mod instances;
7mod inventory;
8mod maintenance;
9mod misc;
10mod ops;
11mod parameters;
12mod patches;
13mod resource_sync;
14mod sessions;
15mod tags;
16
17use std::sync::Arc;
18
19use async_trait::async_trait;
20use http::StatusCode;
21use tokio::sync::Mutex as AsyncMutex;
22
23use fakecloud_core::service::{AwsRequest, AwsResponse, AwsService, AwsServiceError};
24use fakecloud_persistence::SnapshotStore;
25
26use crate::state::{SharedSsmState, SsmSnapshot, SSM_SNAPSHOT_SCHEMA_VERSION};
27
28use fakecloud_secretsmanager::SharedSecretsManagerState;
29
30const PARAMETER_VERSION_LIMIT: i64 = 100;
31
32pub struct SsmService {
33 state: SharedSsmState,
34 secretsmanager_state: Option<SharedSecretsManagerState>,
35 snapshot_store: Option<Arc<dyn SnapshotStore>>,
36 snapshot_lock: Arc<AsyncMutex<()>>,
37 pub(crate) kms_hook: Option<Arc<dyn fakecloud_core::delivery::KmsHook>>,
38}
39
40impl SsmService {
41 pub fn new(state: SharedSsmState) -> Self {
42 Self {
43 state,
44 secretsmanager_state: None,
45 snapshot_store: None,
46 snapshot_lock: Arc::new(AsyncMutex::new(())),
47 kms_hook: None,
48 }
49 }
50
51 pub fn with_secretsmanager(mut self, sm_state: SharedSecretsManagerState) -> Self {
52 self.secretsmanager_state = Some(sm_state);
53 self
54 }
55
56 pub fn with_snapshot_store(mut self, store: Arc<dyn SnapshotStore>) -> Self {
57 self.snapshot_store = Some(store);
58 self
59 }
60
61 pub fn with_kms_hook(mut self, hook: Arc<dyn fakecloud_core::delivery::KmsHook>) -> Self {
62 self.kms_hook = Some(hook);
63 self
64 }
65
66 pub fn set_command_status(&self, account_id: &str, command_id: &str, status: &str) -> bool {
73 let mut accounts = self.state.write();
74 let state = accounts.get_or_create(account_id);
75 if let Some(c) = state
76 .commands
77 .iter_mut()
78 .find(|c| c.command_id == command_id)
79 {
80 let now = chrono::Utc::now();
81 let details = commands::friendly_status_details(status);
82 for inv in c.invocations.iter_mut() {
83 inv.status = status.to_string();
84 inv.status_details = details.clone();
85 inv.response_code = match status {
86 "Success" => 0,
87 "Pending" | "InProgress" | "Delayed" => -1,
88 _ => 1,
89 };
90 inv.last_update_at = now;
91 }
92 c.status = status.to_string();
93 return true;
94 }
95 false
96 }
97
98 pub fn fail_command_invocation(
104 &self,
105 account_id: &str,
106 command_id: &str,
107 instance_id: Option<&str>,
108 status_details: Option<&str>,
109 standard_error_content: Option<&str>,
110 ) -> usize {
111 let mut accounts = self.state.write();
112 let state = accounts.get_or_create(account_id);
113 let Some(cmd) = state
114 .commands
115 .iter_mut()
116 .find(|c| c.command_id == command_id)
117 else {
118 return 0;
119 };
120 let now = chrono::Utc::now();
121 let mut updated = 0;
122 for inv in cmd.invocations.iter_mut() {
123 if let Some(iid) = instance_id {
124 if inv.instance_id != iid {
125 continue;
126 }
127 }
128 inv.status = "Failed".to_string();
129 inv.status_details = status_details
130 .map(|s| s.to_string())
131 .unwrap_or_else(|| commands::friendly_status_details("Failed"));
132 if let Some(err) = standard_error_content {
133 inv.standard_error_content = err.to_string();
134 }
135 inv.response_code = 1;
136 inv.last_update_at = now;
137 updated += 1;
138 }
139 if updated > 0 {
140 cmd.status = commands::aggregate_command_status(&cmd.invocations);
141 }
142 updated
143 }
144
145 pub fn parameter_policy_events(
151 &self,
152 account_id: &str,
153 ) -> Vec<crate::state::ParameterPolicyEvent> {
154 let mut accounts = self.state.write();
155 let state = accounts.get_or_create(account_id);
156 parameters::purge_expired_params(state);
157 parameters::tick_policy_notifications(state);
158 state.parameter_policy_events.clone()
159 }
160
161 pub fn clear_parameter_policy_events(&self, account_id: &str) {
164 let mut accounts = self.state.write();
165 let state = accounts.get_or_create(account_id);
166 state.parameter_policy_events.clear();
167 }
168
169 #[allow(clippy::too_many_arguments)]
176 pub fn inject_session(
177 &self,
178 account_id: &str,
179 target: &str,
180 status: Option<&str>,
181 owner: Option<&str>,
182 reason: Option<&str>,
183 session_id: Option<&str>,
184 ) -> String {
185 let now = chrono::Utc::now();
186 let mut accounts = self.state.write();
187 let state = accounts.get_or_create(account_id);
188 let id = match session_id {
189 Some(s) if !s.is_empty() => s.to_string(),
190 _ => {
191 state.session_counter += 1;
192 format!("session-{:012x}", state.session_counter)
193 }
194 };
195 let resolved_status = status.unwrap_or("Connected").to_string();
196 let end_date = if resolved_status == "Terminated" {
197 Some(now)
198 } else {
199 None
200 };
201 let resolved_owner = owner.map(|s| s.to_string()).unwrap_or_else(|| {
202 fakecloud_aws::arn::Arn::global("iam", &state.account_id, "root").to_string()
203 });
204 let session = crate::state::SsmSession {
205 session_id: id.clone(),
206 target: target.to_string(),
207 status: resolved_status,
208 start_date: now,
209 end_date,
210 owner: resolved_owner,
211 reason: reason.map(|s| s.to_string()),
212 };
213 state.sessions.insert(id.clone(), session);
214 id
215 }
216
217 async fn save_snapshot(&self) {
221 let Some(store) = self.snapshot_store.clone() else {
222 return;
223 };
224 let _guard = self.snapshot_lock.lock().await;
225 let snapshot = SsmSnapshot {
226 schema_version: SSM_SNAPSHOT_SCHEMA_VERSION,
227 state: None,
228 accounts: Some(self.state.read().clone()),
229 };
230 let join = tokio::task::spawn_blocking(move || -> std::io::Result<()> {
231 let bytes = serde_json::to_vec(&snapshot)
232 .map_err(|e| std::io::Error::new(std::io::ErrorKind::InvalidData, e.to_string()))?;
233 store.save(&bytes)
234 })
235 .await;
236 match join {
237 Ok(Ok(())) => {}
238 Ok(Err(err)) => tracing::error!(%err, "failed to write ssm snapshot"),
239 Err(err) => tracing::error!(%err, "ssm snapshot task panicked"),
240 }
241 }
242}
243
244#[async_trait]
245impl AwsService for SsmService {
246 fn service_name(&self) -> &str {
247 "ssm"
248 }
249
250 async fn handle(&self, req: AwsRequest) -> Result<AwsResponse, AwsServiceError> {
251 let mutates = !is_read_only_action(req.action.as_str());
252 let result = match req.action.as_str() {
253 "PutParameter" => self.put_parameter(&req),
254 "GetParameter" => self.get_parameter(&req),
255 "GetParameters" => self.get_parameters(&req),
256 "GetParametersByPath" => self.get_parameters_by_path(&req),
257 "DeleteParameter" => self.delete_parameter(&req),
258 "DeleteParameters" => self.delete_parameters(&req),
259 "DescribeParameters" => self.describe_parameters(&req),
260 "GetParameterHistory" => self.get_parameter_history(&req),
261 "AddTagsToResource" => self.add_tags_to_resource(&req),
262 "RemoveTagsFromResource" => self.remove_tags_from_resource(&req),
263 "ListTagsForResource" => self.list_tags_for_resource(&req),
264 "LabelParameterVersion" => self.label_parameter_version(&req),
265 "UnlabelParameterVersion" => self.unlabel_parameter_version(&req),
266 "CreateDocument" => self.create_document(&req),
267 "GetDocument" => self.get_document(&req),
268 "DeleteDocument" => self.delete_document(&req),
269 "UpdateDocument" => self.update_document(&req),
270 "DescribeDocument" => self.describe_document(&req),
271 "UpdateDocumentDefaultVersion" => self.update_document_default_version(&req),
272 "ListDocuments" => self.list_documents(&req),
273 "DescribeDocumentPermission" => self.describe_document_permission(&req),
274 "ModifyDocumentPermission" => self.modify_document_permission(&req),
275 "SendCommand" => self.send_command(&req),
276 "ListCommands" => self.list_commands(&req),
277 "GetCommandInvocation" => self.get_command_invocation(&req),
278 "ListCommandInvocations" => self.list_command_invocations(&req),
279 "CancelCommand" => self.cancel_command(&req),
280 "CreateMaintenanceWindow" => self.create_maintenance_window(&req),
281 "DescribeMaintenanceWindows" => self.describe_maintenance_windows(&req),
282 "GetMaintenanceWindow" => self.get_maintenance_window(&req),
283 "DeleteMaintenanceWindow" => self.delete_maintenance_window(&req),
284 "UpdateMaintenanceWindow" => self.update_maintenance_window(&req),
285 "RegisterTargetWithMaintenanceWindow" => {
286 self.register_target_with_maintenance_window(&req)
287 }
288 "DeregisterTargetFromMaintenanceWindow" => {
289 self.deregister_target_from_maintenance_window(&req)
290 }
291 "DescribeMaintenanceWindowTargets" => self.describe_maintenance_window_targets(&req),
292 "RegisterTaskWithMaintenanceWindow" => self.register_task_with_maintenance_window(&req),
293 "DeregisterTaskFromMaintenanceWindow" => {
294 self.deregister_task_from_maintenance_window(&req)
295 }
296 "DescribeMaintenanceWindowTasks" => self.describe_maintenance_window_tasks(&req),
297 "CreatePatchBaseline" => self.create_patch_baseline(&req),
298 "DeletePatchBaseline" => self.delete_patch_baseline(&req),
299 "DescribePatchBaselines" => self.describe_patch_baselines(&req),
300 "GetPatchBaseline" => self.get_patch_baseline(&req),
301 "RegisterPatchBaselineForPatchGroup" => {
302 self.register_patch_baseline_for_patch_group(&req)
303 }
304 "DeregisterPatchBaselineForPatchGroup" => {
305 self.deregister_patch_baseline_for_patch_group(&req)
306 }
307 "GetPatchBaselineForPatchGroup" => self.get_patch_baseline_for_patch_group(&req),
308 "DescribePatchGroups" => self.describe_patch_groups(&req),
309 "CreateAssociation" => self.create_association(&req),
311 "DescribeAssociation" => self.describe_association(&req),
312 "DeleteAssociation" => self.delete_association(&req),
313 "ListAssociations" => self.list_associations(&req),
314 "UpdateAssociation" => self.update_association(&req),
315 "ListAssociationVersions" => self.list_association_versions(&req),
316 "UpdateAssociationStatus" => self.update_association_status(&req),
317 "StartAssociationsOnce" => self.start_associations_once(&req),
318 "CreateAssociationBatch" => self.create_association_batch(&req),
319 "DescribeAssociationExecutions" => self.describe_association_executions(&req),
320 "DescribeAssociationExecutionTargets" => {
321 self.describe_association_execution_targets(&req)
322 }
323 "CreateOpsItem" => self.create_ops_item(&req),
325 "GetOpsItem" => self.get_ops_item(&req),
326 "UpdateOpsItem" => self.update_ops_item(&req),
327 "DeleteOpsItem" => self.delete_ops_item(&req),
328 "DescribeOpsItems" => self.describe_ops_items(&req),
329 "ListDocumentVersions" => self.list_document_versions(&req),
331 "ListDocumentMetadataHistory" => self.list_document_metadata_history(&req),
332 "UpdateDocumentMetadata" => self.update_document_metadata(&req),
333 "PutResourcePolicy" => self.put_resource_policy(&req),
335 "GetResourcePolicies" => self.get_resource_policies(&req),
336 "DeleteResourcePolicy" => self.delete_resource_policy(&req),
337 "PutInventory" => self.put_inventory(&req),
339 "GetInventory" => self.get_inventory(&req),
340 "GetInventorySchema" => self.get_inventory_schema(&req),
341 "ListInventoryEntries" => self.list_inventory_entries(&req),
342 "DeleteInventory" => self.delete_inventory(&req),
343 "DescribeInventoryDeletions" => self.describe_inventory_deletions(&req),
344 "PutComplianceItems" => self.put_compliance_items(&req),
346 "ListComplianceItems" => self.list_compliance_items(&req),
347 "ListComplianceSummaries" => self.list_compliance_summaries(&req),
348 "ListResourceComplianceSummaries" => self.list_resource_compliance_summaries(&req),
349 "UpdateMaintenanceWindowTarget" => self.update_maintenance_window_target(&req),
351 "UpdateMaintenanceWindowTask" => self.update_maintenance_window_task(&req),
352 "GetMaintenanceWindowTask" => self.get_maintenance_window_task(&req),
353 "GetMaintenanceWindowExecution" => self.get_maintenance_window_execution(&req),
354 "GetMaintenanceWindowExecutionTask" => self.get_maintenance_window_execution_task(&req),
355 "GetMaintenanceWindowExecutionTaskInvocation" => {
356 self.get_maintenance_window_execution_task_invocation(&req)
357 }
358 "DescribeMaintenanceWindowExecutions" => {
359 self.describe_maintenance_window_executions(&req)
360 }
361 "DescribeMaintenanceWindowExecutionTasks" => {
362 self.describe_maintenance_window_execution_tasks(&req)
363 }
364 "DescribeMaintenanceWindowExecutionTaskInvocations" => {
365 self.describe_maintenance_window_execution_task_invocations(&req)
366 }
367 "DescribeMaintenanceWindowSchedule" => self.describe_maintenance_window_schedule(&req),
368 "DescribeMaintenanceWindowsForTarget" => {
369 self.describe_maintenance_windows_for_target(&req)
370 }
371 "CancelMaintenanceWindowExecution" => self.cancel_maintenance_window_execution(&req),
372 "UpdatePatchBaseline" => self.update_patch_baseline(&req),
374 "DescribeInstancePatchStates" => self.describe_instance_patch_states(&req),
375 "DescribeInstancePatchStatesForPatchGroup" => {
376 self.describe_instance_patch_states_for_patch_group(&req)
377 }
378 "DescribeInstancePatches" => self.describe_instance_patches(&req),
379 "DescribeEffectivePatchesForPatchBaseline" => {
380 self.describe_effective_patches_for_patch_baseline(&req)
381 }
382 "GetDeployablePatchSnapshotForInstance" => {
383 self.get_deployable_patch_snapshot_for_instance(&req)
384 }
385 "CreateResourceDataSync" => self.create_resource_data_sync(&req),
387 "DeleteResourceDataSync" => self.delete_resource_data_sync(&req),
388 "ListResourceDataSync" => self.list_resource_data_sync(&req),
389 "UpdateResourceDataSync" => self.update_resource_data_sync(&req),
390 "AssociateOpsItemRelatedItem" => self.associate_ops_item_related_item(&req),
392 "DisassociateOpsItemRelatedItem" => self.disassociate_ops_item_related_item(&req),
393 "ListOpsItemRelatedItems" => self.list_ops_item_related_items(&req),
394 "ListOpsItemEvents" => self.list_ops_item_events(&req),
395 "CreateOpsMetadata" => self.create_ops_metadata(&req),
397 "GetOpsMetadata" => self.get_ops_metadata(&req),
398 "UpdateOpsMetadata" => self.update_ops_metadata(&req),
399 "DeleteOpsMetadata" => self.delete_ops_metadata(&req),
400 "ListOpsMetadata" => self.list_ops_metadata(&req),
401 "GetOpsSummary" => self.get_ops_summary(&req),
403 "StartAutomationExecution" => self.start_automation_execution(&req),
405 "StopAutomationExecution" => self.stop_automation_execution(&req),
406 "GetAutomationExecution" => self.get_automation_execution(&req),
407 "DescribeAutomationExecutions" => self.describe_automation_executions(&req),
408 "DescribeAutomationStepExecutions" => self.describe_automation_step_executions(&req),
409 "SendAutomationSignal" => self.send_automation_signal(&req),
410 "StartChangeRequestExecution" => self.start_change_request_execution(&req),
411 "StartExecutionPreview" => self.start_execution_preview(&req),
412 "GetExecutionPreview" => self.get_execution_preview(&req),
413 "StartSession" => self.start_session(&req),
415 "ResumeSession" => self.resume_session(&req),
416 "TerminateSession" => self.terminate_session(&req),
417 "DescribeSessions" => self.describe_sessions(&req),
418 "StartAccessRequest" => self.start_access_request(&req),
419 "GetAccessToken" => self.get_access_token(&req),
420 "CreateActivation" => self.create_activation(&req),
422 "DeleteActivation" => self.delete_activation(&req),
423 "DescribeActivations" => self.describe_activations(&req),
424 "DeregisterManagedInstance" => self.deregister_managed_instance(&req),
425 "DescribeInstanceInformation" => self.describe_instance_information(&req),
426 "DescribeInstanceProperties" => self.describe_instance_properties(&req),
427 "UpdateManagedInstanceRole" => self.update_managed_instance_role(&req),
428 "ListNodes" => self.list_nodes(&req),
430 "ListNodesSummary" => self.list_nodes_summary(&req),
431 "DescribeEffectiveInstanceAssociations" => {
432 self.describe_effective_instance_associations(&req)
433 }
434 "DescribeInstanceAssociationsStatus" => {
435 self.describe_instance_associations_status(&req)
436 }
437 "GetConnectionStatus" => self.get_connection_status(&req),
440 "GetCalendarState" => self.get_calendar_state(&req),
441 "DescribePatchGroupState" => self.describe_patch_group_state(&req),
442 "DescribePatchProperties" => self.describe_patch_properties(&req),
443 "GetDefaultPatchBaseline" => self.get_default_patch_baseline(&req),
444 "RegisterDefaultPatchBaseline" => self.register_default_patch_baseline(&req),
445 "DescribeAvailablePatches" => self.describe_available_patches(&req),
446 "GetServiceSetting" => self.get_service_setting(&req),
447 "ResetServiceSetting" => self.reset_service_setting(&req),
448 "UpdateServiceSetting" => self.update_service_setting(&req),
449 _ => Err(AwsServiceError::action_not_implemented("ssm", &req.action)),
450 };
451 if mutates && matches!(result.as_ref(), Ok(resp) if resp.status.is_success()) {
452 self.save_snapshot().await;
453 }
454 result
455 }
456
457 fn supported_actions(&self) -> &[&str] {
458 &[
459 "PutParameter",
460 "GetParameter",
461 "GetParameters",
462 "GetParametersByPath",
463 "DeleteParameter",
464 "DeleteParameters",
465 "DescribeParameters",
466 "GetParameterHistory",
467 "AddTagsToResource",
468 "RemoveTagsFromResource",
469 "ListTagsForResource",
470 "LabelParameterVersion",
471 "UnlabelParameterVersion",
472 "CreateDocument",
473 "GetDocument",
474 "DeleteDocument",
475 "UpdateDocument",
476 "DescribeDocument",
477 "UpdateDocumentDefaultVersion",
478 "ListDocuments",
479 "DescribeDocumentPermission",
480 "ModifyDocumentPermission",
481 "SendCommand",
482 "ListCommands",
483 "GetCommandInvocation",
484 "ListCommandInvocations",
485 "CancelCommand",
486 "CreateMaintenanceWindow",
487 "DescribeMaintenanceWindows",
488 "GetMaintenanceWindow",
489 "DeleteMaintenanceWindow",
490 "UpdateMaintenanceWindow",
491 "RegisterTargetWithMaintenanceWindow",
492 "DeregisterTargetFromMaintenanceWindow",
493 "DescribeMaintenanceWindowTargets",
494 "RegisterTaskWithMaintenanceWindow",
495 "DeregisterTaskFromMaintenanceWindow",
496 "DescribeMaintenanceWindowTasks",
497 "CreatePatchBaseline",
498 "DeletePatchBaseline",
499 "DescribePatchBaselines",
500 "GetPatchBaseline",
501 "RegisterPatchBaselineForPatchGroup",
502 "DeregisterPatchBaselineForPatchGroup",
503 "GetPatchBaselineForPatchGroup",
504 "DescribePatchGroups",
505 "CreateAssociation",
507 "DescribeAssociation",
508 "DeleteAssociation",
509 "ListAssociations",
510 "UpdateAssociation",
511 "ListAssociationVersions",
512 "UpdateAssociationStatus",
513 "StartAssociationsOnce",
514 "CreateAssociationBatch",
515 "DescribeAssociationExecutions",
516 "DescribeAssociationExecutionTargets",
517 "CreateOpsItem",
519 "GetOpsItem",
520 "UpdateOpsItem",
521 "DeleteOpsItem",
522 "DescribeOpsItems",
523 "ListDocumentVersions",
525 "ListDocumentMetadataHistory",
526 "UpdateDocumentMetadata",
527 "PutResourcePolicy",
529 "GetResourcePolicies",
530 "DeleteResourcePolicy",
531 "PutInventory",
533 "GetInventory",
534 "GetInventorySchema",
535 "ListInventoryEntries",
536 "DeleteInventory",
537 "DescribeInventoryDeletions",
538 "PutComplianceItems",
540 "ListComplianceItems",
541 "ListComplianceSummaries",
542 "ListResourceComplianceSummaries",
543 "UpdateMaintenanceWindowTarget",
545 "UpdateMaintenanceWindowTask",
546 "GetMaintenanceWindowTask",
547 "GetMaintenanceWindowExecution",
548 "GetMaintenanceWindowExecutionTask",
549 "GetMaintenanceWindowExecutionTaskInvocation",
550 "DescribeMaintenanceWindowExecutions",
551 "DescribeMaintenanceWindowExecutionTasks",
552 "DescribeMaintenanceWindowExecutionTaskInvocations",
553 "DescribeMaintenanceWindowSchedule",
554 "DescribeMaintenanceWindowsForTarget",
555 "CancelMaintenanceWindowExecution",
556 "UpdatePatchBaseline",
558 "DescribeInstancePatchStates",
559 "DescribeInstancePatchStatesForPatchGroup",
560 "DescribeInstancePatches",
561 "DescribeEffectivePatchesForPatchBaseline",
562 "GetDeployablePatchSnapshotForInstance",
563 "CreateResourceDataSync",
565 "DeleteResourceDataSync",
566 "ListResourceDataSync",
567 "UpdateResourceDataSync",
568 "AssociateOpsItemRelatedItem",
570 "DisassociateOpsItemRelatedItem",
571 "ListOpsItemRelatedItems",
572 "ListOpsItemEvents",
573 "CreateOpsMetadata",
575 "GetOpsMetadata",
576 "UpdateOpsMetadata",
577 "DeleteOpsMetadata",
578 "ListOpsMetadata",
579 "GetOpsSummary",
581 "StartAutomationExecution",
583 "StopAutomationExecution",
584 "GetAutomationExecution",
585 "DescribeAutomationExecutions",
586 "DescribeAutomationStepExecutions",
587 "SendAutomationSignal",
588 "StartChangeRequestExecution",
589 "StartExecutionPreview",
590 "GetExecutionPreview",
591 "StartSession",
593 "ResumeSession",
594 "TerminateSession",
595 "DescribeSessions",
596 "StartAccessRequest",
597 "GetAccessToken",
598 "CreateActivation",
600 "DeleteActivation",
601 "DescribeActivations",
602 "DeregisterManagedInstance",
603 "DescribeInstanceInformation",
604 "DescribeInstanceProperties",
605 "UpdateManagedInstanceRole",
606 "ListNodes",
608 "ListNodesSummary",
609 "DescribeEffectiveInstanceAssociations",
610 "DescribeInstanceAssociationsStatus",
611 "GetConnectionStatus",
613 "GetCalendarState",
614 "DescribePatchGroupState",
615 "DescribePatchProperties",
616 "GetDefaultPatchBaseline",
617 "RegisterDefaultPatchBaseline",
618 "DescribeAvailablePatches",
619 "GetServiceSetting",
620 "ResetServiceSetting",
621 "UpdateServiceSetting",
622 ]
623 }
624}
625
626mod helpers;
627pub(crate) use helpers::*;
628
629#[cfg(test)]
630mod tests;