Skip to main content

awsim_glacier/
lib.rs

1//! Amazon S3 Glacier emulator. Vaults, archives (single-shot upload), and a
2//! fast-forwarded job lifecycle (jobs land in Succeeded immediately so callers
3//! don't have to poll).
4
5mod operations;
6pub mod state;
7
8pub use state::GlacierState;
9
10use std::sync::Arc;
11
12use async_trait::async_trait;
13use awsim_core::{
14    AccountRegionStore, AwsError, Protocol, RequestContext, RouteDefinition, ServiceHandler,
15};
16use serde_json::Value;
17use tracing::debug;
18
19pub struct GlacierService {
20    store: AccountRegionStore<GlacierState>,
21}
22
23impl GlacierService {
24    pub fn new() -> Self {
25        Self {
26            store: AccountRegionStore::new(),
27        }
28    }
29
30    pub fn store(&self) -> AccountRegionStore<GlacierState> {
31        self.store.clone()
32    }
33
34    fn get_state(&self, ctx: &RequestContext) -> Arc<GlacierState> {
35        self.store.get(&ctx.account_id, &ctx.region)
36    }
37}
38
39impl Default for GlacierService {
40    fn default() -> Self {
41        Self::new()
42    }
43}
44
45#[async_trait]
46impl ServiceHandler for GlacierService {
47    fn service_name(&self) -> &str {
48        "glacier"
49    }
50
51    fn signing_name(&self) -> &str {
52        "glacier"
53    }
54
55    fn protocol(&self) -> Protocol {
56        Protocol::RestJson1
57    }
58
59    fn routes(&self) -> Vec<RouteDefinition> {
60        vec![
61            // Vaults
62            RouteDefinition {
63                method: "PUT",
64                path_pattern: "/{accountId}/vaults/{vaultName}",
65                operation: "CreateVault",
66                required_query_param: None,
67            },
68            RouteDefinition {
69                method: "GET",
70                path_pattern: "/{accountId}/vaults/{vaultName}",
71                operation: "DescribeVault",
72                required_query_param: None,
73            },
74            RouteDefinition {
75                method: "GET",
76                path_pattern: "/{accountId}/vaults",
77                operation: "ListVaults",
78                required_query_param: None,
79            },
80            RouteDefinition {
81                method: "DELETE",
82                path_pattern: "/{accountId}/vaults/{vaultName}",
83                operation: "DeleteVault",
84                required_query_param: None,
85            },
86            // Archives
87            RouteDefinition {
88                method: "POST",
89                path_pattern: "/{accountId}/vaults/{vaultName}/archives",
90                operation: "UploadArchive",
91                required_query_param: None,
92            },
93            RouteDefinition {
94                method: "DELETE",
95                path_pattern: "/{accountId}/vaults/{vaultName}/archives/{archiveId}",
96                operation: "DeleteArchive",
97                required_query_param: None,
98            },
99            // Jobs
100            RouteDefinition {
101                method: "POST",
102                path_pattern: "/{accountId}/vaults/{vaultName}/jobs",
103                operation: "InitiateJob",
104                required_query_param: None,
105            },
106            RouteDefinition {
107                method: "GET",
108                path_pattern: "/{accountId}/vaults/{vaultName}/jobs/{jobId}",
109                operation: "DescribeJob",
110                required_query_param: None,
111            },
112            RouteDefinition {
113                method: "GET",
114                path_pattern: "/{accountId}/vaults/{vaultName}/jobs",
115                operation: "ListJobs",
116                required_query_param: None,
117            },
118            // Notifications
119            RouteDefinition {
120                method: "PUT",
121                path_pattern: "/{accountId}/vaults/{vaultName}/notification-configuration",
122                operation: "SetVaultNotifications",
123                required_query_param: None,
124            },
125            RouteDefinition {
126                method: "GET",
127                path_pattern: "/{accountId}/vaults/{vaultName}/notification-configuration",
128                operation: "GetVaultNotifications",
129                required_query_param: None,
130            },
131            RouteDefinition {
132                method: "DELETE",
133                path_pattern: "/{accountId}/vaults/{vaultName}/notification-configuration",
134                operation: "DeleteVaultNotifications",
135                required_query_param: None,
136            },
137        ]
138    }
139
140    async fn handle(
141        &self,
142        operation: &str,
143        input: Value,
144        ctx: &RequestContext,
145    ) -> Result<Value, AwsError> {
146        debug!(operation, "Glacier request");
147        let state = self.get_state(ctx);
148        match operation {
149            "CreateVault" => operations::create_vault(&state, &input, ctx),
150            "DescribeVault" => operations::describe_vault(&state, &input, ctx),
151            "ListVaults" => operations::list_vaults(&state, &input, ctx),
152            "DeleteVault" => operations::delete_vault(&state, &input, ctx),
153            "UploadArchive" => operations::upload_archive(&state, &input, ctx),
154            "DeleteArchive" => operations::delete_archive(&state, &input, ctx),
155            "InitiateJob" => operations::initiate_job(&state, &input, ctx),
156            "DescribeJob" => operations::describe_job(&state, &input, ctx),
157            "ListJobs" => operations::list_jobs(&state, &input, ctx),
158            "SetVaultNotifications" => operations::set_vault_notifications(&state, &input, ctx),
159            "GetVaultNotifications" => operations::get_vault_notifications(&state, &input, ctx),
160            "DeleteVaultNotifications" => {
161                operations::delete_vault_notifications(&state, &input, ctx)
162            }
163            _ => Err(AwsError::unknown_operation(operation)),
164        }
165    }
166
167    fn snapshot(&self) -> Option<Vec<u8>> {
168        let mut all = state::GlacierSnapshot {
169            vaults: vec![],
170            archives: vec![],
171            jobs: vec![],
172        };
173        for (_, st) in self.store.iter_all() {
174            let s = st.to_snapshot();
175            all.vaults.extend(s.vaults);
176            all.archives.extend(s.archives);
177            all.jobs.extend(s.jobs);
178        }
179        serde_json::to_vec(&all).ok()
180    }
181
182    fn restore(&self, data: &[u8]) -> Result<(), String> {
183        let snap: state::GlacierSnapshot =
184            serde_json::from_slice(data).map_err(|e| e.to_string())?;
185        let st = self.store.get("000000000000", "us-east-1");
186        st.restore_from_snapshot(snap);
187        Ok(())
188    }
189}
190
191#[cfg(test)]
192mod tests {
193    use super::*;
194    use base64::{Engine as _, engine::general_purpose::STANDARD as B64};
195    use serde_json::json;
196
197    fn ctx() -> RequestContext {
198        RequestContext::new("glacier", "us-east-1")
199    }
200
201    fn block_on<F: std::future::Future>(f: F) -> F::Output {
202        use std::task::{Context, Poll, RawWaker, RawWakerVTable, Waker};
203        fn noop_clone(_: *const ()) -> RawWaker {
204            noop_raw_waker()
205        }
206        fn noop(_: *const ()) {}
207        fn noop_raw_waker() -> RawWaker {
208            static VTABLE: RawWakerVTable = RawWakerVTable::new(noop_clone, noop, noop, noop);
209            RawWaker::new(std::ptr::null(), &VTABLE)
210        }
211        let waker = unsafe { Waker::from_raw(noop_raw_waker()) };
212        let mut cx = Context::from_waker(&waker);
213        let mut fut = std::pin::pin!(f);
214        loop {
215            if let Poll::Ready(v) = fut.as_mut().poll(&mut cx) {
216                return v;
217            }
218        }
219    }
220
221    #[test]
222    fn vault_archive_job_lifecycle() {
223        let svc = GlacierService::new();
224        let ctx = ctx();
225
226        block_on(svc.handle("CreateVault", json!({ "vaultName": "long-term" }), &ctx)).unwrap();
227
228        let body = b"some archived bytes";
229        let upload = block_on(svc.handle(
230            "UploadArchive",
231            json!({ "vaultName": "long-term", "body": B64.encode(body) }),
232            &ctx,
233        ))
234        .unwrap();
235        let archive_id = upload["ArchiveId"].as_str().unwrap().to_string();
236        assert!(!upload["Checksum"].as_str().unwrap().is_empty());
237
238        let v = block_on(svc.handle("DescribeVault", json!({ "vaultName": "long-term" }), &ctx))
239            .unwrap();
240        assert_eq!(v["NumberOfArchives"], 1);
241        assert_eq!(v["SizeInBytes"], body.len());
242
243        // Vault delete blocked
244        let err = block_on(svc.handle("DeleteVault", json!({ "vaultName": "long-term" }), &ctx))
245            .unwrap_err();
246        assert_eq!(err.code, "InvalidParameterValueException");
247
248        // Job
249        let job = block_on(svc.handle(
250            "InitiateJob",
251            json!({
252                "vaultName": "long-term",
253                "jobParameters": { "Type": "archive-retrieval", "ArchiveId": archive_id }
254            }),
255            &ctx,
256        ))
257        .unwrap();
258        let job_id = job["JobId"].as_str().unwrap();
259        let described = block_on(svc.handle(
260            "DescribeJob",
261            json!({ "vaultName": "long-term", "jobId": job_id }),
262            &ctx,
263        ))
264        .unwrap();
265        assert_eq!(described["StatusCode"], "Succeeded");
266        assert_eq!(described["Completed"], true);
267
268        // Delete archive then vault
269        block_on(svc.handle(
270            "DeleteArchive",
271            json!({ "vaultName": "long-term", "archiveId": archive_id }),
272            &ctx,
273        ))
274        .unwrap();
275        block_on(svc.handle("DeleteVault", json!({ "vaultName": "long-term" }), &ctx)).unwrap();
276    }
277}