1pub mod error;
2mod handler;
3mod operations;
4mod state;
5
6pub use handler::EcsService;
7
8#[cfg(test)]
9mod tests {
10 use awsim_core::RequestContext;
11 use serde_json::json;
12
13 use super::handler::EcsService;
14 use awsim_core::ServiceHandler;
15
16 fn ctx() -> RequestContext {
17 RequestContext::new("ecs", "us-east-1")
18 }
19
20 fn block_on<F: std::future::Future>(f: F) -> F::Output {
21 use std::task::{Context, Poll, RawWaker, RawWakerVTable, Waker};
22
23 fn noop_clone(_: *const ()) -> RawWaker {
24 noop_raw_waker()
25 }
26 fn noop(_: *const ()) {}
27 fn noop_raw_waker() -> RawWaker {
28 static VTABLE: RawWakerVTable = RawWakerVTable::new(noop_clone, noop, noop, noop);
29 RawWaker::new(std::ptr::null(), &VTABLE)
30 }
31 let waker = unsafe { Waker::from_raw(noop_raw_waker()) };
32 let mut cx = Context::from_waker(&waker);
33 let mut fut = std::pin::pin!(f);
34 loop {
35 match fut.as_mut().poll(&mut cx) {
36 Poll::Ready(v) => return v,
37 Poll::Pending => {}
38 }
39 }
40 }
41
42 #[test]
47 fn test_create_cluster() {
48 let svc = EcsService::new();
49 let ctx = ctx();
50 let result = block_on(svc.handle(
51 "CreateCluster",
52 json!({ "clusterName": "my-cluster" }),
53 &ctx,
54 ))
55 .unwrap();
56 let arn = result["cluster"]["clusterArn"].as_str().unwrap();
57 assert!(arn.contains("my-cluster"), "arn={arn}");
58 }
59
60 #[test]
61 fn test_create_cluster_idempotent() {
62 let svc = EcsService::new();
63 let ctx = ctx();
64 let r1 =
65 block_on(svc.handle("CreateCluster", json!({ "clusterName": "idem" }), &ctx)).unwrap();
66 let r2 =
67 block_on(svc.handle("CreateCluster", json!({ "clusterName": "idem" }), &ctx)).unwrap();
68 assert_eq!(r1["cluster"]["clusterArn"], r2["cluster"]["clusterArn"]);
69 }
70
71 #[test]
72 fn test_list_clusters() {
73 let svc = EcsService::new();
74 let ctx = ctx();
75 block_on(svc.handle("CreateCluster", json!({ "clusterName": "c1" }), &ctx)).unwrap();
76 block_on(svc.handle("CreateCluster", json!({ "clusterName": "c2" }), &ctx)).unwrap();
77 let result = block_on(svc.handle("ListClusters", json!({}), &ctx)).unwrap();
78 assert_eq!(result["clusterArns"].as_array().unwrap().len(), 2);
79 }
80
81 #[test]
82 fn test_describe_clusters() {
83 let svc = EcsService::new();
84 let ctx = ctx();
85 block_on(svc.handle("CreateCluster", json!({ "clusterName": "dc" }), &ctx)).unwrap();
86 let result =
87 block_on(svc.handle("DescribeClusters", json!({ "clusters": ["dc"] }), &ctx)).unwrap();
88 assert_eq!(result["clusters"].as_array().unwrap().len(), 1);
89 assert_eq!(result["failures"].as_array().unwrap().len(), 0);
90 }
91
92 #[test]
93 fn test_describe_clusters_missing() {
94 let svc = EcsService::new();
95 let ctx = ctx();
96 let result =
97 block_on(svc.handle("DescribeClusters", json!({ "clusters": ["ghost"] }), &ctx))
98 .unwrap();
99 assert_eq!(result["clusters"].as_array().unwrap().len(), 0);
100 assert_eq!(result["failures"].as_array().unwrap().len(), 1);
101 }
102
103 #[test]
104 fn test_delete_cluster() {
105 let svc = EcsService::new();
106 let ctx = ctx();
107 block_on(svc.handle("CreateCluster", json!({ "clusterName": "todelete" }), &ctx)).unwrap();
108 block_on(svc.handle("DeleteCluster", json!({ "cluster": "todelete" }), &ctx)).unwrap();
109 let list = block_on(svc.handle("ListClusters", json!({}), &ctx)).unwrap();
110 assert_eq!(list["clusterArns"].as_array().unwrap().len(), 0);
111 }
112
113 #[test]
118 fn test_register_task_definition() {
119 let svc = EcsService::new();
120 let ctx = ctx();
121 let result = block_on(svc.handle(
122 "RegisterTaskDefinition",
123 json!({
124 "family": "web",
125 "containerDefinitions": [
126 { "name": "nginx", "image": "nginx:latest", "cpu": 256, "memory": 512, "essential": true }
127 ],
128 "networkMode": "awsvpc",
129 "requiresCompatibilities": ["FARGATE"],
130 "cpu": "256",
131 "memory": "512",
132 }),
133 &ctx,
134 ))
135 .unwrap();
136 let td = &result["taskDefinition"];
137 assert_eq!(td["family"].as_str().unwrap(), "web");
138 assert_eq!(td["revision"].as_u64().unwrap(), 1);
139 let arn = td["taskDefinitionArn"].as_str().unwrap();
140 assert!(arn.contains("web:1"), "arn={arn}");
141 }
142
143 #[test]
144 fn test_register_task_definition_multiple_revisions() {
145 let svc = EcsService::new();
146 let ctx = ctx();
147 let r1 = block_on(svc.handle(
148 "RegisterTaskDefinition",
149 json!({ "family": "worker", "containerDefinitions": [] }),
150 &ctx,
151 ))
152 .unwrap();
153 let r2 = block_on(svc.handle(
154 "RegisterTaskDefinition",
155 json!({ "family": "worker", "containerDefinitions": [] }),
156 &ctx,
157 ))
158 .unwrap();
159 assert_eq!(r1["taskDefinition"]["revision"].as_u64().unwrap(), 1);
160 assert_eq!(r2["taskDefinition"]["revision"].as_u64().unwrap(), 2);
161 }
162
163 #[test]
164 fn test_describe_task_definition() {
165 let svc = EcsService::new();
166 let ctx = ctx();
167 block_on(svc.handle(
168 "RegisterTaskDefinition",
169 json!({ "family": "api", "containerDefinitions": [] }),
170 &ctx,
171 ))
172 .unwrap();
173 let result = block_on(svc.handle(
174 "DescribeTaskDefinition",
175 json!({ "taskDefinition": "api:1" }),
176 &ctx,
177 ))
178 .unwrap();
179 assert_eq!(result["taskDefinition"]["family"].as_str().unwrap(), "api");
180 }
181
182 #[test]
183 fn test_list_task_definitions() {
184 let svc = EcsService::new();
185 let ctx = ctx();
186 block_on(svc.handle(
187 "RegisterTaskDefinition",
188 json!({ "family": "svc-a", "containerDefinitions": [] }),
189 &ctx,
190 ))
191 .unwrap();
192 block_on(svc.handle(
193 "RegisterTaskDefinition",
194 json!({ "family": "svc-b", "containerDefinitions": [] }),
195 &ctx,
196 ))
197 .unwrap();
198 let result = block_on(svc.handle("ListTaskDefinitions", json!({}), &ctx)).unwrap();
199 assert_eq!(result["taskDefinitionArns"].as_array().unwrap().len(), 2);
200 }
201
202 #[test]
203 fn test_deregister_task_definition() {
204 let svc = EcsService::new();
205 let ctx = ctx();
206 block_on(svc.handle(
207 "RegisterTaskDefinition",
208 json!({ "family": "old-svc", "containerDefinitions": [] }),
209 &ctx,
210 ))
211 .unwrap();
212 block_on(svc.handle(
213 "DeregisterTaskDefinition",
214 json!({ "taskDefinition": "old-svc:1" }),
215 &ctx,
216 ))
217 .unwrap();
218 let result = block_on(svc.handle("ListTaskDefinitions", json!({}), &ctx)).unwrap();
219 assert_eq!(result["taskDefinitionArns"].as_array().unwrap().len(), 0);
220 }
221
222 #[test]
227 fn test_create_and_list_service() {
228 let svc = EcsService::new();
229 let ctx = ctx();
230 block_on(svc.handle("CreateCluster", json!({ "clusterName": "prod" }), &ctx)).unwrap();
231 block_on(svc.handle(
232 "CreateService",
233 json!({
234 "cluster": "prod",
235 "serviceName": "web-svc",
236 "taskDefinition": "web:1",
237 "desiredCount": 2,
238 "launchType": "FARGATE"
239 }),
240 &ctx,
241 ))
242 .unwrap();
243 let list =
244 block_on(svc.handle("ListServices", json!({ "cluster": "prod" }), &ctx)).unwrap();
245 assert_eq!(list["serviceArns"].as_array().unwrap().len(), 1);
246 }
247
248 #[test]
249 fn test_create_service_round_trips_load_balancers_and_deployment_config() {
250 let svc = EcsService::new();
251 let ctx = ctx();
252 block_on(svc.handle("CreateCluster", json!({ "clusterName": "deploy" }), &ctx)).unwrap();
253 let create = block_on(svc.handle(
254 "CreateService",
255 json!({
256 "cluster": "deploy",
257 "serviceName": "edge",
258 "taskDefinition": "edge:1",
259 "desiredCount": 3,
260 "launchType": "FARGATE",
261 "loadBalancers": [{
262 "targetGroupArn": "arn:aws:elasticloadbalancing:us-east-1:000000000000:targetgroup/edge/abc",
263 "containerName": "edge",
264 "containerPort": 8080
265 }],
266 "deploymentConfiguration": {
267 "minimumHealthyPercent": 50,
268 "maximumPercent": 200
269 },
270 "deploymentController": { "type": "CODE_DEPLOY" },
271 "networkConfiguration": {
272 "awsvpcConfiguration": {
273 "subnets": ["subnet-1"],
274 "assignPublicIp": "ENABLED"
275 }
276 }
277 }),
278 &ctx,
279 ))
280 .unwrap();
281 let s = &create["service"];
282 assert_eq!(s["loadBalancers"][0]["containerName"], "edge");
283 assert_eq!(s["deploymentConfiguration"]["maximumPercent"], 200);
284 assert_eq!(s["deploymentController"]["type"], "CODE_DEPLOY");
285 assert_eq!(
286 s["networkConfiguration"]["awsvpcConfiguration"]["assignPublicIp"],
287 "ENABLED"
288 );
289 }
290
291 #[test]
292 fn test_create_service_rejects_invalid_deployment_controller_type() {
293 let svc = EcsService::new();
294 let ctx = ctx();
295 block_on(svc.handle("CreateCluster", json!({ "clusterName": "bad-dc" }), &ctx)).unwrap();
296 let err = block_on(svc.handle(
297 "CreateService",
298 json!({
299 "cluster": "bad-dc",
300 "serviceName": "x",
301 "taskDefinition": "x:1",
302 "deploymentController": { "type": "MAGIC" }
303 }),
304 &ctx,
305 ))
306 .unwrap_err();
307 assert_eq!(err.code, "InvalidParameterException");
308 }
309
310 #[test]
311 fn test_describe_services() {
312 let svc = EcsService::new();
313 let ctx = ctx();
314 block_on(svc.handle("CreateCluster", json!({ "clusterName": "staging" }), &ctx)).unwrap();
315 block_on(svc.handle(
316 "CreateService",
317 json!({
318 "cluster": "staging",
319 "serviceName": "api-svc",
320 "taskDefinition": "api:1",
321 "desiredCount": 1,
322 }),
323 &ctx,
324 ))
325 .unwrap();
326 let result = block_on(svc.handle(
327 "DescribeServices",
328 json!({ "cluster": "staging", "services": ["api-svc"] }),
329 &ctx,
330 ))
331 .unwrap();
332 assert_eq!(result["services"].as_array().unwrap().len(), 1);
333 assert_eq!(result["failures"].as_array().unwrap().len(), 0);
334 }
335
336 #[test]
337 fn test_update_service() {
338 let svc = EcsService::new();
339 let ctx = ctx();
340 block_on(svc.handle("CreateCluster", json!({ "clusterName": "test" }), &ctx)).unwrap();
341 block_on(svc.handle(
342 "CreateService",
343 json!({ "cluster": "test", "serviceName": "mysvc", "taskDefinition": "foo:1", "desiredCount": 1 }),
344 &ctx,
345 ))
346 .unwrap();
347 let result = block_on(svc.handle(
348 "UpdateService",
349 json!({ "cluster": "test", "service": "mysvc", "desiredCount": 5 }),
350 &ctx,
351 ))
352 .unwrap();
353 assert_eq!(result["service"]["desiredCount"].as_i64().unwrap(), 5);
354 }
355
356 #[test]
357 fn test_delete_service() {
358 let svc = EcsService::new();
359 let ctx = ctx();
360 block_on(svc.handle(
361 "CreateCluster",
362 json!({ "clusterName": "del-cluster" }),
363 &ctx,
364 ))
365 .unwrap();
366 block_on(svc.handle(
367 "CreateService",
368 json!({ "cluster": "del-cluster", "serviceName": "del-svc", "taskDefinition": "t:1", "desiredCount": 0 }),
369 &ctx,
370 ))
371 .unwrap();
372 block_on(svc.handle(
373 "DeleteService",
374 json!({ "cluster": "del-cluster", "service": "del-svc" }),
375 &ctx,
376 ))
377 .unwrap();
378 let list = block_on(svc.handle("ListServices", json!({ "cluster": "del-cluster" }), &ctx))
379 .unwrap();
380 assert_eq!(list["serviceArns"].as_array().unwrap().len(), 0);
381 }
382
383 #[test]
388 fn test_run_task() {
389 let svc = EcsService::new();
390 let ctx = ctx();
391 block_on(svc.handle(
392 "CreateCluster",
393 json!({ "clusterName": "run-cluster" }),
394 &ctx,
395 ))
396 .unwrap();
397 let result = block_on(svc.handle(
398 "RunTask",
399 json!({ "cluster": "run-cluster", "taskDefinition": "web:1", "count": 2 }),
400 &ctx,
401 ))
402 .unwrap();
403 let tasks = result["tasks"].as_array().unwrap();
404 assert_eq!(tasks.len(), 2);
405 assert_eq!(tasks[0]["lastStatus"].as_str().unwrap(), "RUNNING");
406 }
407
408 #[test]
409 fn test_stop_task() {
410 let svc = EcsService::new();
411 let ctx = ctx();
412 block_on(svc.handle(
413 "CreateCluster",
414 json!({ "clusterName": "stop-cluster" }),
415 &ctx,
416 ))
417 .unwrap();
418 let run = block_on(svc.handle(
419 "RunTask",
420 json!({ "cluster": "stop-cluster", "taskDefinition": "web:1", "count": 1 }),
421 &ctx,
422 ))
423 .unwrap();
424 let task_arn = run["tasks"][0]["taskArn"].as_str().unwrap().to_string();
425 let result = block_on(svc.handle(
426 "StopTask",
427 json!({ "cluster": "stop-cluster", "task": task_arn }),
428 &ctx,
429 ))
430 .unwrap();
431 assert_eq!(result["task"]["lastStatus"].as_str().unwrap(), "STOPPED");
432 }
433
434 #[test]
435 fn test_list_tasks() {
436 let svc = EcsService::new();
437 let ctx = ctx();
438 block_on(svc.handle(
439 "CreateCluster",
440 json!({ "clusterName": "list-cluster" }),
441 &ctx,
442 ))
443 .unwrap();
444 block_on(svc.handle(
445 "RunTask",
446 json!({ "cluster": "list-cluster", "taskDefinition": "web:1", "count": 3 }),
447 &ctx,
448 ))
449 .unwrap();
450 let result =
451 block_on(svc.handle("ListTasks", json!({ "cluster": "list-cluster" }), &ctx)).unwrap();
452 assert_eq!(result["taskArns"].as_array().unwrap().len(), 3);
453 }
454
455 #[test]
456 fn test_describe_tasks() {
457 let svc = EcsService::new();
458 let ctx = ctx();
459 block_on(svc.handle(
460 "CreateCluster",
461 json!({ "clusterName": "desc-cluster" }),
462 &ctx,
463 ))
464 .unwrap();
465 let run = block_on(svc.handle(
466 "RunTask",
467 json!({ "cluster": "desc-cluster", "taskDefinition": "web:1", "count": 1 }),
468 &ctx,
469 ))
470 .unwrap();
471 let task_arn = run["tasks"][0]["taskArn"].as_str().unwrap().to_string();
472 let result = block_on(svc.handle(
473 "DescribeTasks",
474 json!({ "cluster": "desc-cluster", "tasks": [task_arn] }),
475 &ctx,
476 ))
477 .unwrap();
478 assert_eq!(result["tasks"].as_array().unwrap().len(), 1);
479 assert_eq!(result["failures"].as_array().unwrap().len(), 0);
480 }
481
482 #[test]
483 fn test_unknown_operation() {
484 let svc = EcsService::new();
485 let ctx = ctx();
486 let err = block_on(svc.handle("NoSuchOp", json!({}), &ctx)).unwrap_err();
487 assert_eq!(err.code, "UnknownOperationException");
488 }
489}