Skip to main content

fakecloud_backup/
service.rs

1//! AWS Backup restJson1 service handler.
2//!
3//! Implements the Backup control plane: backup plans + selections + versions,
4//! backup vaults (standard / logically-air-gapped / restore-access) with their
5//! notifications, access policies and lock configuration, recovery points,
6//! backup / copy / restore / scan jobs (progressed synthetically to a terminal
7//! state so Describe/List show completed work), frameworks, report plans and
8//! report jobs, legal holds, restore-testing plans + selections, tiering
9//! configurations, protected resources, tags, and the account-scoped global /
10//! region settings.
11//!
12//! No real backup engine runs: `StartBackupJob` records a job and a synthetic
13//! recovery point that `DescribeRecoveryPoint` then resolves, mirroring the
14//! eventual-consistency of the real service without moving any bytes.
15
16use async_trait::async_trait;
17use chrono::{DateTime, Utc};
18use http::{Method, StatusCode};
19use serde_json::{json, Map, Value};
20use std::sync::Arc;
21use tokio::sync::Mutex as AsyncMutex;
22
23use fakecloud_core::pagination::paginate_checked;
24use fakecloud_core::service::{AwsRequest, AwsResponse, AwsService, AwsServiceError};
25use fakecloud_persistence::SnapshotStore;
26
27use crate::state::{
28    framework_arn, legal_hold_arn, plan_arn, recovery_point_arn, report_plan_arn,
29    restore_testing_plan_arn, tiering_configuration_arn, vault_arn, PlanRecord, PlanVersion,
30    RestoreTestingPlanRecord, RestoreTestingSelectionRecord, SelectionRecord, SharedBackupState,
31    TagMap, VaultRecord,
32};
33
34/// The resource types AWS Backup can protect (a representative catalogue,
35/// returned by `GetSupportedResourceTypes` and used to seed region settings).
36const SUPPORTED_RESOURCE_TYPES: &[&str] = &[
37    "Aurora",
38    "CloudFormation",
39    "DocumentDB",
40    "DynamoDB",
41    "EBS",
42    "EC2",
43    "EFS",
44    "FSx",
45    "Neptune",
46    "RDS",
47    "Redshift",
48    "S3",
49    "SAP HANA on Amazon EC2",
50    "Storage Gateway",
51    "Timestream",
52    "VirtualMachine",
53];
54
55pub const BACKUP_ACTIONS: &[&str] = &[
56    "AssociateBackupVaultMpaApprovalTeam",
57    "CancelLegalHold",
58    "CreateBackupPlan",
59    "CreateBackupSelection",
60    "CreateBackupVault",
61    "CreateFramework",
62    "CreateLegalHold",
63    "CreateLogicallyAirGappedBackupVault",
64    "CreateReportPlan",
65    "CreateRestoreAccessBackupVault",
66    "CreateRestoreTestingPlan",
67    "CreateRestoreTestingSelection",
68    "CreateTieringConfiguration",
69    "DeleteBackupPlan",
70    "DeleteBackupSelection",
71    "DeleteBackupVault",
72    "DeleteBackupVaultAccessPolicy",
73    "DeleteBackupVaultLockConfiguration",
74    "DeleteBackupVaultNotifications",
75    "DeleteFramework",
76    "DeleteRecoveryPoint",
77    "DeleteReportPlan",
78    "DeleteRestoreTestingPlan",
79    "DeleteRestoreTestingSelection",
80    "DeleteTieringConfiguration",
81    "DescribeBackupJob",
82    "DescribeBackupVault",
83    "DescribeCopyJob",
84    "DescribeFramework",
85    "DescribeGlobalSettings",
86    "DescribeProtectedResource",
87    "DescribeRecoveryPoint",
88    "DescribeRegionSettings",
89    "DescribeReportJob",
90    "DescribeReportPlan",
91    "DescribeRestoreJob",
92    "DescribeScanJob",
93    "DisassociateBackupVaultMpaApprovalTeam",
94    "DisassociateRecoveryPoint",
95    "DisassociateRecoveryPointFromParent",
96    "ExportBackupPlanTemplate",
97    "GetBackupPlan",
98    "GetBackupPlanFromJSON",
99    "GetBackupPlanFromTemplate",
100    "GetBackupSelection",
101    "GetBackupVaultAccessPolicy",
102    "GetBackupVaultNotifications",
103    "GetLegalHold",
104    "GetPITRMalwareScanResults",
105    "GetRecoveryPointIndexDetails",
106    "GetRecoveryPointRestoreMetadata",
107    "GetRestoreJobMetadata",
108    "GetRestoreTestingInferredMetadata",
109    "GetRestoreTestingPlan",
110    "GetRestoreTestingSelection",
111    "GetSupportedResourceTypes",
112    "GetTieringConfiguration",
113    "ListBackupJobSummaries",
114    "ListBackupJobs",
115    "ListBackupPlanTemplates",
116    "ListBackupPlanVersions",
117    "ListBackupPlans",
118    "ListBackupSelections",
119    "ListBackupVaults",
120    "ListCopyJobSummaries",
121    "ListCopyJobs",
122    "ListFrameworks",
123    "ListIndexedRecoveryPoints",
124    "ListLegalHolds",
125    "ListProtectedResources",
126    "ListProtectedResourcesByBackupVault",
127    "ListRecoveryPointsByBackupVault",
128    "ListRecoveryPointsByLegalHold",
129    "ListRecoveryPointsByResource",
130    "ListReportJobs",
131    "ListReportPlans",
132    "ListRestoreAccessBackupVaults",
133    "ListRestoreJobSummaries",
134    "ListRestoreJobs",
135    "ListRestoreJobsByProtectedResource",
136    "ListRestoreTestingPlans",
137    "ListRestoreTestingSelections",
138    "ListScanJobSummaries",
139    "ListScanJobs",
140    "ListTags",
141    "ListTieringConfigurations",
142    "PutBackupVaultAccessPolicy",
143    "PutBackupVaultLockConfiguration",
144    "PutBackupVaultNotifications",
145    "PutRestoreValidationResult",
146    "RevokeRestoreAccessBackupVault",
147    "StartBackupJob",
148    "StartCopyJob",
149    "StartReportJob",
150    "StartRestoreJob",
151    "StartScanJob",
152    "StopBackupJob",
153    "TagResource",
154    "UntagResource",
155    "UpdateBackupPlan",
156    "UpdateFramework",
157    "UpdateGlobalSettings",
158    "UpdateRecoveryPointIndexSettings",
159    "UpdateRecoveryPointLifecycle",
160    "UpdateRegionSettings",
161    "UpdateReportPlan",
162    "UpdateRestoreTestingPlan",
163    "UpdateRestoreTestingSelection",
164    "UpdateTieringConfiguration",
165];
166
167pub struct BackupService {
168    state: SharedBackupState,
169    snapshot_store: Option<Arc<dyn SnapshotStore>>,
170    snapshot_lock: Arc<AsyncMutex<()>>,
171}
172
173/// The decoded path labels for a route, in URI order.
174type Labels = Vec<String>;
175
176impl BackupService {
177    pub fn new(state: SharedBackupState) -> Self {
178        Self {
179            state,
180            snapshot_store: None,
181            snapshot_lock: Arc::new(AsyncMutex::new(())),
182        }
183    }
184
185    pub fn with_snapshot_store(mut self, store: Arc<dyn SnapshotStore>) -> Self {
186        self.snapshot_store = Some(store);
187        self
188    }
189
190    async fn save_snapshot(&self) {
191        crate::persistence::save_snapshot(
192            &self.state,
193            self.snapshot_store.clone(),
194            &self.snapshot_lock,
195        )
196        .await;
197    }
198
199    /// Persist hook for the CloudFormation provisioner; `None` in memory mode.
200    pub fn snapshot_hook(&self) -> Option<fakecloud_persistence::SnapshotHook> {
201        let store = self.snapshot_store.clone()?;
202        let state = self.state.clone();
203        let lock = self.snapshot_lock.clone();
204        Some(Arc::new(move || {
205            let state = state.clone();
206            let store = store.clone();
207            let lock = lock.clone();
208            Box::pin(async move {
209                crate::persistence::save_snapshot(&state, Some(store), &lock).await;
210            })
211        }))
212    }
213
214    /// Route a request to `(action, path-labels)` by method + path segments,
215    /// preserving internal empty segments so precise slice matches don't
216    /// collapse (mirrors the EKS handler's `resolve_action`).
217    fn resolve_action(req: &AwsRequest) -> Option<(&'static str, Labels)> {
218        let raw = req.raw_path.split('?').next().unwrap_or(&req.raw_path);
219        // Preserve internal AND trailing empty segments (only the leading empty
220        // from the leading `/` is dropped) so an omitted/empty path label
221        // (e.g. `/audit/frameworks/` for an empty `FrameworkName`) still routes
222        // to the intended op, which then rejects the empty identifier with a
223        // declared error instead of mis-routing to the collection endpoint.
224        let trimmed = raw.strip_prefix('/').unwrap_or(raw);
225        let segs: Vec<&str> = if trimmed.is_empty() {
226            Vec::new()
227        } else {
228            trimmed.split('/').collect()
229        };
230        let m = &req.method;
231        let d = |s: &str| decode(s);
232        let one = |a| Some((a, Vec::new()));
233        macro_rules! l {
234            ($a:expr, $($x:expr),*) => { Some(($a, vec![$($x),*])) };
235        }
236        match (m, segs.as_slice()) {
237            // ---- backup plans ----
238            (&Method::PUT, ["backup", "plans"]) => one("CreateBackupPlan"),
239            (&Method::GET, ["backup", "plans"]) => one("ListBackupPlans"),
240            (&Method::GET, ["backup", "plans", id]) => l!("GetBackupPlan", d(id)),
241            (&Method::POST, ["backup", "plans", id]) => l!("UpdateBackupPlan", d(id)),
242            (&Method::DELETE, ["backup", "plans", id]) => l!("DeleteBackupPlan", d(id)),
243            (&Method::GET, ["backup", "plans", id, "versions"]) => {
244                l!("ListBackupPlanVersions", d(id))
245            }
246            (&Method::PUT, ["backup", "plans", id, "selections"]) => {
247                l!("CreateBackupSelection", d(id))
248            }
249            (&Method::GET, ["backup", "plans", id, "selections"]) => {
250                l!("ListBackupSelections", d(id))
251            }
252            (&Method::GET, ["backup", "plans", id, "selections", sid]) => {
253                l!("GetBackupSelection", d(id), d(sid))
254            }
255            (&Method::DELETE, ["backup", "plans", id, "selections", sid]) => {
256                l!("DeleteBackupSelection", d(id), d(sid))
257            }
258            (&Method::GET, ["backup", "plans", id, "toTemplate"]) => {
259                l!("ExportBackupPlanTemplate", d(id))
260            }
261            (&Method::POST, ["backup", "template", "json", "toPlan"]) => {
262                one("GetBackupPlanFromJSON")
263            }
264            (&Method::GET, ["backup", "template", "plans"]) => one("ListBackupPlanTemplates"),
265            (&Method::GET, ["backup", "template", "plans", tid, "toPlan"]) => {
266                l!("GetBackupPlanFromTemplate", d(tid))
267            }
268            // ---- backup vaults ----
269            (&Method::GET, ["backup-vaults"]) => one("ListBackupVaults"),
270            (&Method::PUT, ["backup-vaults", name]) => l!("CreateBackupVault", d(name)),
271            (&Method::GET, ["backup-vaults", name]) => l!("DescribeBackupVault", d(name)),
272            (&Method::DELETE, ["backup-vaults", name]) => l!("DeleteBackupVault", d(name)),
273            (&Method::PUT, ["backup-vaults", name, "access-policy"]) => {
274                l!("PutBackupVaultAccessPolicy", d(name))
275            }
276            (&Method::GET, ["backup-vaults", name, "access-policy"]) => {
277                l!("GetBackupVaultAccessPolicy", d(name))
278            }
279            (&Method::DELETE, ["backup-vaults", name, "access-policy"]) => {
280                l!("DeleteBackupVaultAccessPolicy", d(name))
281            }
282            (&Method::PUT, ["backup-vaults", name, "vault-lock"]) => {
283                l!("PutBackupVaultLockConfiguration", d(name))
284            }
285            (&Method::DELETE, ["backup-vaults", name, "vault-lock"]) => {
286                l!("DeleteBackupVaultLockConfiguration", d(name))
287            }
288            (&Method::PUT, ["backup-vaults", name, "notification-configuration"]) => {
289                l!("PutBackupVaultNotifications", d(name))
290            }
291            (&Method::GET, ["backup-vaults", name, "notification-configuration"]) => {
292                l!("GetBackupVaultNotifications", d(name))
293            }
294            (&Method::DELETE, ["backup-vaults", name, "notification-configuration"]) => {
295                l!("DeleteBackupVaultNotifications", d(name))
296            }
297            (&Method::PUT, ["backup-vaults", name, "mpaApprovalTeam"]) => {
298                l!("AssociateBackupVaultMpaApprovalTeam", d(name))
299            }
300            (&Method::POST, ["backup-vaults", name, "mpaApprovalTeam"]) => {
301                l!("DisassociateBackupVaultMpaApprovalTeam", d(name))
302            }
303            (&Method::GET, ["backup-vaults", name, "recovery-points"]) => {
304                l!("ListRecoveryPointsByBackupVault", d(name))
305            }
306            (&Method::GET, ["backup-vaults", name, "recovery-points", rp]) => {
307                l!("DescribeRecoveryPoint", d(name), d(rp))
308            }
309            (&Method::DELETE, ["backup-vaults", name, "recovery-points", rp]) => {
310                l!("DeleteRecoveryPoint", d(name), d(rp))
311            }
312            (&Method::POST, ["backup-vaults", name, "recovery-points", rp]) => {
313                l!("UpdateRecoveryPointLifecycle", d(name), d(rp))
314            }
315            (&Method::GET, ["backup-vaults", name, "recovery-points", rp, "index"]) => {
316                l!("GetRecoveryPointIndexDetails", d(name), d(rp))
317            }
318            (&Method::POST, ["backup-vaults", name, "recovery-points", rp, "index"]) => {
319                l!("UpdateRecoveryPointIndexSettings", d(name), d(rp))
320            }
321            (&Method::GET, ["backup-vaults", name, "recovery-points", rp, "restore-metadata"]) => {
322                l!("GetRecoveryPointRestoreMetadata", d(name), d(rp))
323            }
324            (&Method::POST, ["backup-vaults", name, "recovery-points", rp, "disassociate"]) => {
325                l!("DisassociateRecoveryPoint", d(name), d(rp))
326            }
327            (
328                &Method::DELETE,
329                ["backup-vaults", name, "recovery-points", rp, "parentAssociation"],
330            ) => {
331                l!("DisassociateRecoveryPointFromParent", d(name), d(rp))
332            }
333            (&Method::GET, ["backup-vaults", name, "resources"]) => {
334                l!("ListProtectedResourcesByBackupVault", d(name))
335            }
336            // ---- logically air-gapped / restore-access vaults ----
337            (&Method::PUT, ["logically-air-gapped-backup-vaults", name]) => {
338                l!("CreateLogicallyAirGappedBackupVault", d(name))
339            }
340            (
341                &Method::GET,
342                ["logically-air-gapped-backup-vaults", name, "restore-access-backup-vaults"],
343            ) => {
344                l!("ListRestoreAccessBackupVaults", d(name))
345            }
346            (
347                &Method::DELETE,
348                ["logically-air-gapped-backup-vaults", name, "restore-access-backup-vaults", arn],
349            ) => {
350                l!("RevokeRestoreAccessBackupVault", d(name), d(arn))
351            }
352            (&Method::PUT, ["restore-access-backup-vaults"]) => {
353                one("CreateRestoreAccessBackupVault")
354            }
355            // ---- legal holds ----
356            (&Method::POST, ["legal-holds"]) => one("CreateLegalHold"),
357            (&Method::GET, ["legal-holds"]) => one("ListLegalHolds"),
358            (&Method::GET, ["legal-holds", id]) => l!("GetLegalHold", d(id)),
359            (&Method::DELETE, ["legal-holds", id]) => l!("CancelLegalHold", d(id)),
360            (&Method::GET, ["legal-holds", id, "recovery-points"]) => {
361                l!("ListRecoveryPointsByLegalHold", d(id))
362            }
363            // ---- audit: frameworks / report plans / report jobs / summaries ----
364            (&Method::POST, ["audit", "frameworks"]) => one("CreateFramework"),
365            (&Method::GET, ["audit", "frameworks"]) => one("ListFrameworks"),
366            (&Method::GET, ["audit", "frameworks", name]) => l!("DescribeFramework", d(name)),
367            (&Method::PUT, ["audit", "frameworks", name]) => l!("UpdateFramework", d(name)),
368            (&Method::DELETE, ["audit", "frameworks", name]) => l!("DeleteFramework", d(name)),
369            (&Method::POST, ["audit", "report-plans"]) => one("CreateReportPlan"),
370            (&Method::GET, ["audit", "report-plans"]) => one("ListReportPlans"),
371            (&Method::GET, ["audit", "report-plans", name]) => l!("DescribeReportPlan", d(name)),
372            (&Method::PUT, ["audit", "report-plans", name]) => l!("UpdateReportPlan", d(name)),
373            (&Method::DELETE, ["audit", "report-plans", name]) => l!("DeleteReportPlan", d(name)),
374            (&Method::GET, ["audit", "report-jobs"]) => one("ListReportJobs"),
375            (&Method::GET, ["audit", "report-jobs", id]) => l!("DescribeReportJob", d(id)),
376            (&Method::POST, ["audit", "report-jobs", name]) => l!("StartReportJob", d(name)),
377            (&Method::GET, ["audit", "backup-job-summaries"]) => one("ListBackupJobSummaries"),
378            (&Method::GET, ["audit", "copy-job-summaries"]) => one("ListCopyJobSummaries"),
379            (&Method::GET, ["audit", "restore-job-summaries"]) => one("ListRestoreJobSummaries"),
380            (&Method::GET, ["audit", "scan-job-summaries"]) => one("ListScanJobSummaries"),
381            // ---- backup / copy / restore / scan jobs ----
382            (&Method::GET, ["backup-jobs"]) => one("ListBackupJobs"),
383            (&Method::PUT, ["backup-jobs"]) => one("StartBackupJob"),
384            (&Method::GET, ["backup-jobs", id]) => l!("DescribeBackupJob", d(id)),
385            (&Method::POST, ["backup-jobs", id]) => l!("StopBackupJob", d(id)),
386            (&Method::GET, ["copy-jobs"]) => one("ListCopyJobs"),
387            (&Method::PUT, ["copy-jobs"]) => one("StartCopyJob"),
388            (&Method::GET, ["copy-jobs", id]) => l!("DescribeCopyJob", d(id)),
389            (&Method::GET, ["restore-jobs"]) => one("ListRestoreJobs"),
390            (&Method::PUT, ["restore-jobs"]) => one("StartRestoreJob"),
391            (&Method::GET, ["restore-jobs", id]) => l!("DescribeRestoreJob", d(id)),
392            (&Method::GET, ["restore-jobs", id, "metadata"]) => l!("GetRestoreJobMetadata", d(id)),
393            (&Method::PUT, ["restore-jobs", id, "validations"]) => {
394                l!("PutRestoreValidationResult", d(id))
395            }
396            (&Method::GET, ["scan", "jobs"]) => one("ListScanJobs"),
397            (&Method::GET, ["scan", "jobs", id]) => l!("DescribeScanJob", d(id)),
398            (&Method::PUT, ["scan", "job"]) => one("StartScanJob"),
399            (&Method::GET, ["scan", "pitr-malware-scan-results"]) => {
400                one("GetPITRMalwareScanResults")
401            }
402            // ---- protected resources ----
403            (&Method::GET, ["resources"]) => one("ListProtectedResources"),
404            (&Method::GET, ["resources", arn]) => l!("DescribeProtectedResource", d(arn)),
405            (&Method::GET, ["resources", arn, "recovery-points"]) => {
406                l!("ListRecoveryPointsByResource", d(arn))
407            }
408            (&Method::GET, ["resources", arn, "restore-jobs"]) => {
409                l!("ListRestoreJobsByProtectedResource", d(arn))
410            }
411            // ---- restore testing ----
412            (&Method::PUT, ["restore-testing", "plans"]) => one("CreateRestoreTestingPlan"),
413            (&Method::GET, ["restore-testing", "plans"]) => one("ListRestoreTestingPlans"),
414            (&Method::GET, ["restore-testing", "inferred-metadata"]) => {
415                one("GetRestoreTestingInferredMetadata")
416            }
417            (&Method::GET, ["restore-testing", "plans", name]) => {
418                l!("GetRestoreTestingPlan", d(name))
419            }
420            (&Method::PUT, ["restore-testing", "plans", name]) => {
421                l!("UpdateRestoreTestingPlan", d(name))
422            }
423            (&Method::DELETE, ["restore-testing", "plans", name]) => {
424                l!("DeleteRestoreTestingPlan", d(name))
425            }
426            (&Method::PUT, ["restore-testing", "plans", name, "selections"]) => {
427                l!("CreateRestoreTestingSelection", d(name))
428            }
429            (&Method::GET, ["restore-testing", "plans", name, "selections"]) => {
430                l!("ListRestoreTestingSelections", d(name))
431            }
432            (&Method::GET, ["restore-testing", "plans", name, "selections", sname]) => {
433                l!("GetRestoreTestingSelection", d(name), d(sname))
434            }
435            (&Method::PUT, ["restore-testing", "plans", name, "selections", sname]) => {
436                l!("UpdateRestoreTestingSelection", d(name), d(sname))
437            }
438            (&Method::DELETE, ["restore-testing", "plans", name, "selections", sname]) => {
439                l!("DeleteRestoreTestingSelection", d(name), d(sname))
440            }
441            // ---- tiering configurations ----
442            (&Method::PUT, ["tiering-configurations"]) => one("CreateTieringConfiguration"),
443            (&Method::GET, ["tiering-configurations"]) => one("ListTieringConfigurations"),
444            (&Method::GET, ["tiering-configurations", name]) => {
445                l!("GetTieringConfiguration", d(name))
446            }
447            (&Method::PUT, ["tiering-configurations", name]) => {
448                l!("UpdateTieringConfiguration", d(name))
449            }
450            (&Method::DELETE, ["tiering-configurations", name]) => {
451                l!("DeleteTieringConfiguration", d(name))
452            }
453            // ---- misc ----
454            (&Method::GET, ["indexes", "recovery-point"]) => one("ListIndexedRecoveryPoints"),
455            (&Method::GET, ["global-settings"]) => one("DescribeGlobalSettings"),
456            (&Method::PUT, ["global-settings"]) => one("UpdateGlobalSettings"),
457            (&Method::GET, ["account-settings"]) => one("DescribeRegionSettings"),
458            (&Method::PUT, ["account-settings"]) => one("UpdateRegionSettings"),
459            (&Method::GET, ["supported-resource-types"]) => one("GetSupportedResourceTypes"),
460            (&Method::GET, ["tags", arn]) => l!("ListTags", d(arn)),
461            (&Method::POST, ["tags", arn]) => l!("TagResource", d(arn)),
462            (&Method::POST, ["untag", arn]) => l!("UntagResource", d(arn)),
463            _ => None,
464        }
465    }
466}
467
468const MUTATING: &[&str] = &[
469    "CreateBackupPlan",
470    "CreateBackupSelection",
471    "CreateBackupVault",
472    "CreateFramework",
473    "CreateLegalHold",
474    "CreateLogicallyAirGappedBackupVault",
475    "CreateReportPlan",
476    "CreateRestoreAccessBackupVault",
477    "CreateRestoreTestingPlan",
478    "CreateRestoreTestingSelection",
479    "CreateTieringConfiguration",
480    "DeleteBackupPlan",
481    "DeleteBackupSelection",
482    "DeleteBackupVault",
483    "DeleteBackupVaultAccessPolicy",
484    "DeleteBackupVaultLockConfiguration",
485    "DeleteBackupVaultNotifications",
486    "DeleteFramework",
487    "DeleteRecoveryPoint",
488    "DeleteReportPlan",
489    "DeleteRestoreTestingPlan",
490    "DeleteRestoreTestingSelection",
491    "DeleteTieringConfiguration",
492    "AssociateBackupVaultMpaApprovalTeam",
493    "DisassociateBackupVaultMpaApprovalTeam",
494    "DisassociateRecoveryPoint",
495    "DisassociateRecoveryPointFromParent",
496    "CancelLegalHold",
497    "PutBackupVaultAccessPolicy",
498    "PutBackupVaultLockConfiguration",
499    "PutBackupVaultNotifications",
500    "PutRestoreValidationResult",
501    "RevokeRestoreAccessBackupVault",
502    "StartBackupJob",
503    "StartCopyJob",
504    "StartReportJob",
505    "StartRestoreJob",
506    "StartScanJob",
507    "StopBackupJob",
508    "TagResource",
509    "UntagResource",
510    "UpdateBackupPlan",
511    "UpdateFramework",
512    "UpdateGlobalSettings",
513    "UpdateRecoveryPointIndexSettings",
514    "UpdateRecoveryPointLifecycle",
515    "UpdateRegionSettings",
516    "UpdateReportPlan",
517    "UpdateRestoreTestingPlan",
518    "UpdateRestoreTestingSelection",
519    "UpdateTieringConfiguration",
520    // Describe settles job/recovery-point lifecycle, which persists.
521    "DescribeBackupJob",
522    "DescribeCopyJob",
523    "DescribeRestoreJob",
524];
525
526#[async_trait]
527impl AwsService for BackupService {
528    fn service_name(&self) -> &str {
529        "backup"
530    }
531
532    async fn handle(&self, req: AwsRequest) -> Result<AwsResponse, AwsServiceError> {
533        let (action, labels) = Self::resolve_action(&req).ok_or_else(|| {
534            AwsServiceError::aws_error(
535                StatusCode::NOT_FOUND,
536                "UnknownOperationException",
537                format!("Unknown operation: {} {}", req.method, req.raw_path),
538            )
539        })?;
540
541        let result = self.dispatch(action, &labels, &req);
542
543        if MUTATING.contains(&action)
544            && matches!(result.as_ref(), Ok(resp) if resp.status.is_success())
545        {
546            self.save_snapshot().await;
547        }
548        result
549    }
550
551    fn supported_actions(&self) -> &[&str] {
552        BACKUP_ACTIONS
553    }
554}
555
556impl BackupService {
557    #[allow(clippy::too_many_lines)]
558    fn dispatch(
559        &self,
560        action: &str,
561        l: &[String],
562        req: &AwsRequest,
563    ) -> Result<AwsResponse, AwsServiceError> {
564        validate_query_constraints(action, req)?;
565        match action {
566            "CreateBackupPlan" => self.create_backup_plan(req),
567            "GetBackupPlan" => self.get_backup_plan(req, &l[0]),
568            "UpdateBackupPlan" => self.update_backup_plan(req, &l[0]),
569            "DeleteBackupPlan" => self.delete_backup_plan(req, &l[0]),
570            "ListBackupPlans" => self.list_backup_plans(req),
571            "ListBackupPlanVersions" => self.list_backup_plan_versions(req, &l[0]),
572            "ListBackupPlanTemplates" => self.list_backup_plan_templates(req),
573            "GetBackupPlanFromJSON" => self.get_backup_plan_from_json(req),
574            "GetBackupPlanFromTemplate" => self.get_backup_plan_from_template(req, &l[0]),
575            "ExportBackupPlanTemplate" => self.export_backup_plan_template(req, &l[0]),
576            "CreateBackupSelection" => self.create_backup_selection(req, &l[0]),
577            "GetBackupSelection" => self.get_backup_selection(req, &l[0], &l[1]),
578            "DeleteBackupSelection" => self.delete_backup_selection(req, &l[0], &l[1]),
579            "ListBackupSelections" => self.list_backup_selections(req, &l[0]),
580            "CreateBackupVault" => self.create_backup_vault(req, &l[0], "BACKUP_VAULT"),
581            "CreateLogicallyAirGappedBackupVault" => self.create_lag_vault(req, &l[0]),
582            "CreateRestoreAccessBackupVault" => self.create_restore_access_vault(req),
583            "DescribeBackupVault" => self.describe_backup_vault(req, &l[0]),
584            "DeleteBackupVault" => self.delete_backup_vault(req, &l[0]),
585            "ListBackupVaults" => self.list_backup_vaults(req),
586            "ListRestoreAccessBackupVaults" => self.list_restore_access_vaults(req, &l[0]),
587            "RevokeRestoreAccessBackupVault" => self.revoke_restore_access_vault(req, &l[0], &l[1]),
588            "PutBackupVaultAccessPolicy" => self.put_vault_access_policy(req, &l[0]),
589            "GetBackupVaultAccessPolicy" => self.get_vault_access_policy(req, &l[0]),
590            "DeleteBackupVaultAccessPolicy" => self.delete_vault_access_policy(req, &l[0]),
591            "PutBackupVaultLockConfiguration" => self.put_vault_lock(req, &l[0]),
592            "DeleteBackupVaultLockConfiguration" => self.delete_vault_lock(req, &l[0]),
593            "PutBackupVaultNotifications" => self.put_vault_notifications(req, &l[0]),
594            "GetBackupVaultNotifications" => self.get_vault_notifications(req, &l[0]),
595            "DeleteBackupVaultNotifications" => self.delete_vault_notifications(req, &l[0]),
596            "AssociateBackupVaultMpaApprovalTeam" => self.associate_mpa(req, &l[0]),
597            "DisassociateBackupVaultMpaApprovalTeam" => self.disassociate_mpa(req, &l[0]),
598            "DescribeRecoveryPoint" => self.describe_recovery_point(req, &l[0], &l[1]),
599            "DeleteRecoveryPoint" => self.delete_recovery_point(req, &l[0], &l[1]),
600            "UpdateRecoveryPointLifecycle" => {
601                self.update_recovery_point_lifecycle(req, &l[0], &l[1])
602            }
603            "GetRecoveryPointIndexDetails" => self.get_rp_index_details(req, &l[0], &l[1]),
604            "UpdateRecoveryPointIndexSettings" => self.update_rp_index_settings(req, &l[0], &l[1]),
605            "GetRecoveryPointRestoreMetadata" => self.get_rp_restore_metadata(req, &l[0], &l[1]),
606            "DisassociateRecoveryPoint" => self.disassociate_recovery_point(req, &l[0], &l[1]),
607            "DisassociateRecoveryPointFromParent" => self.disassociate_rp_parent(req, &l[0], &l[1]),
608            "ListRecoveryPointsByBackupVault" => self.list_rp_by_vault(req, &l[0]),
609            "ListRecoveryPointsByResource" => self.list_rp_by_resource(req, &l[0]),
610            "ListRecoveryPointsByLegalHold" => self.list_rp_by_legal_hold(req, &l[0]),
611            "ListIndexedRecoveryPoints" => self.list_indexed_recovery_points(req),
612            "ListProtectedResources" => self.list_protected_resources(req),
613            "ListProtectedResourcesByBackupVault" => self.list_protected_by_vault(req, &l[0]),
614            "DescribeProtectedResource" => self.describe_protected_resource(req, &l[0]),
615            // jobs
616            "StartBackupJob" => self.start_backup_job(req),
617            "DescribeBackupJob" => self.describe_backup_job(req, &l[0]),
618            "StopBackupJob" => self.stop_backup_job(req, &l[0]),
619            "ListBackupJobs" => self.list_backup_jobs(req),
620            "StartCopyJob" => self.start_copy_job(req),
621            "DescribeCopyJob" => self.describe_copy_job(req, &l[0]),
622            "ListCopyJobs" => self.list_copy_jobs(req),
623            "StartRestoreJob" => self.start_restore_job(req),
624            "DescribeRestoreJob" => self.describe_restore_job(req, &l[0]),
625            "ListRestoreJobs" => self.list_restore_jobs(req),
626            "ListRestoreJobsByProtectedResource" => self.list_restore_jobs_by_resource(req, &l[0]),
627            "GetRestoreJobMetadata" => self.get_restore_job_metadata(req, &l[0]),
628            "PutRestoreValidationResult" => self.put_restore_validation(req, &l[0]),
629            "StartScanJob" => self.start_scan_job(req),
630            "DescribeScanJob" => self.describe_scan_job(req, &l[0]),
631            "ListScanJobs" => self.list_scan_jobs(req),
632            "GetPITRMalwareScanResults" => self.get_pitr_malware_scan_results(req),
633            "ListBackupJobSummaries" => self.list_job_summaries(req, "BackupJobSummaries"),
634            "ListCopyJobSummaries" => self.list_job_summaries(req, "CopyJobSummaries"),
635            "ListRestoreJobSummaries" => self.list_job_summaries(req, "RestoreJobSummaries"),
636            "ListScanJobSummaries" => self.list_job_summaries(req, "ScanJobSummaries"),
637            // frameworks
638            "CreateFramework" => self.create_framework(req),
639            "DescribeFramework" => self.describe_framework(req, &l[0]),
640            "UpdateFramework" => self.update_framework(req, &l[0]),
641            "DeleteFramework" => self.delete_framework(req, &l[0]),
642            "ListFrameworks" => self.list_frameworks(req),
643            // report plans / jobs
644            "CreateReportPlan" => self.create_report_plan(req),
645            "DescribeReportPlan" => self.describe_report_plan(req, &l[0]),
646            "UpdateReportPlan" => self.update_report_plan(req, &l[0]),
647            "DeleteReportPlan" => self.delete_report_plan(req, &l[0]),
648            "ListReportPlans" => self.list_report_plans(req),
649            "StartReportJob" => self.start_report_job(req, &l[0]),
650            "DescribeReportJob" => self.describe_report_job(req, &l[0]),
651            "ListReportJobs" => self.list_report_jobs(req),
652            // legal holds
653            "CreateLegalHold" => self.create_legal_hold(req),
654            "GetLegalHold" => self.get_legal_hold(req, &l[0]),
655            "CancelLegalHold" => self.cancel_legal_hold(req, &l[0]),
656            "ListLegalHolds" => self.list_legal_holds(req),
657            // restore testing
658            "CreateRestoreTestingPlan" => self.create_rtp(req),
659            "GetRestoreTestingPlan" => self.get_rtp(req, &l[0]),
660            "UpdateRestoreTestingPlan" => self.update_rtp(req, &l[0]),
661            "DeleteRestoreTestingPlan" => self.delete_rtp(req, &l[0]),
662            "ListRestoreTestingPlans" => self.list_rtp(req),
663            "CreateRestoreTestingSelection" => self.create_rts(req, &l[0]),
664            "GetRestoreTestingSelection" => self.get_rts(req, &l[0], &l[1]),
665            "UpdateRestoreTestingSelection" => self.update_rts(req, &l[0], &l[1]),
666            "DeleteRestoreTestingSelection" => self.delete_rts(req, &l[0], &l[1]),
667            "ListRestoreTestingSelections" => self.list_rts(req, &l[0]),
668            "GetRestoreTestingInferredMetadata" => self.get_rt_inferred_metadata(req),
669            // tiering
670            "CreateTieringConfiguration" => self.create_tiering(req),
671            "GetTieringConfiguration" => self.get_tiering(req, &l[0]),
672            "UpdateTieringConfiguration" => self.update_tiering(req, &l[0]),
673            "DeleteTieringConfiguration" => self.delete_tiering(req, &l[0]),
674            "ListTieringConfigurations" => self.list_tiering(req),
675            // settings + tags
676            "DescribeGlobalSettings" => self.describe_global_settings(req),
677            "UpdateGlobalSettings" => self.update_global_settings(req),
678            "DescribeRegionSettings" => self.describe_region_settings(req),
679            "UpdateRegionSettings" => self.update_region_settings(req),
680            "GetSupportedResourceTypes" => Ok(ok(json!({
681                "ResourceTypes": SUPPORTED_RESOURCE_TYPES,
682            }))),
683            "ListTags" => self.list_tags(req, &l[0]),
684            "TagResource" => self.tag_resource(req, &l[0]),
685            "UntagResource" => self.untag_resource(req, &l[0]),
686            _ => Err(AwsServiceError::action_not_implemented("backup", action)),
687        }
688    }
689}
690
691include!("handlers.rs");