1mod 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 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 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 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 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 let err = block_on(svc.handle("DeleteVault", json!({ "vaultName": "long-term" }), &ctx))
245 .unwrap_err();
246 assert_eq!(err.code, "InvalidParameterValueException");
247
248 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 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}