Skip to main content

awsim_ecs/
lib.rs

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    // -----------------------------------------------------------------------
43    // Clusters
44    // -----------------------------------------------------------------------
45
46    #[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    // -----------------------------------------------------------------------
114    // Task Definitions
115    // -----------------------------------------------------------------------
116
117    #[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    // -----------------------------------------------------------------------
223    // Services
224    // -----------------------------------------------------------------------
225
226    #[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    // -----------------------------------------------------------------------
384    // Tasks
385    // -----------------------------------------------------------------------
386
387    #[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}