1use crate::{
8 AwsHttpClient, Result,
9 ops::ecs::EcsOps,
10 types::ecs::{
11 DeregisterTaskDefinitionRequest, DeregisterTaskDefinitionResponse, DescribeClustersRequest,
12 DescribeClustersResponse, DescribeServicesRequest, DescribeServicesResponse,
13 DescribeTaskDefinitionRequest, DescribeTaskDefinitionResponse, ListClustersRequest,
14 ListClustersResponse, ListServicesRequest, ListServicesResponse, UpdateServiceRequest,
15 UpdateServiceResponse,
16 },
17};
18
19pub struct EcsClient<'a> {
21 ops: EcsOps<'a>,
22}
23
24impl<'a> EcsClient<'a> {
25 pub(crate) fn new(client: &'a AwsHttpClient) -> Self {
27 Self {
28 ops: EcsOps::new(client),
29 }
30 }
31
32 pub async fn list_clusters(&self, body: &ListClustersRequest) -> Result<ListClustersResponse> {
34 self.ops.list_clusters(body).await
35 }
36
37 pub async fn describe_clusters(
39 &self,
40 body: &DescribeClustersRequest,
41 ) -> Result<DescribeClustersResponse> {
42 self.ops.describe_clusters(body).await
43 }
44
45 pub async fn list_services(&self, body: &ListServicesRequest) -> Result<ListServicesResponse> {
47 self.ops.list_services(body).await
48 }
49
50 pub async fn describe_services(
52 &self,
53 body: &DescribeServicesRequest,
54 ) -> Result<DescribeServicesResponse> {
55 self.ops.describe_services(body).await
56 }
57
58 pub async fn describe_task_definition(
60 &self,
61 body: &DescribeTaskDefinitionRequest,
62 ) -> Result<DescribeTaskDefinitionResponse> {
63 self.ops.describe_task_definition(body).await
64 }
65
66 pub async fn update_service(
68 &self,
69 body: &UpdateServiceRequest,
70 ) -> Result<UpdateServiceResponse> {
71 self.ops.update_service(body).await
72 }
73
74 pub async fn deregister_task_definition(
76 &self,
77 body: &DeregisterTaskDefinitionRequest,
78 ) -> Result<DeregisterTaskDefinitionResponse> {
79 self.ops.deregister_task_definition(body).await
80 }
81}
82
83#[cfg(test)]
84mod tests {
85 use super::*;
86
87 #[tokio::test]
88 async fn list_clusters_returns_arns() {
89 let mut mock = crate::MockClient::new();
90 mock.expect_post("/").returning_json(serde_json::json!({
91 "clusterArns": [
92 "arn:aws:ecs:us-east-1:123456789012:cluster/my-cluster",
93 "arn:aws:ecs:us-east-1:123456789012:cluster/default"
94 ]
95 }));
96 let client = crate::AwsHttpClient::from_mock(mock);
97 let result = client
98 .ecs()
99 .list_clusters(&ListClustersRequest::default())
100 .await
101 .unwrap();
102 assert_eq!(result.cluster_arns.len(), 2);
103 assert!(result.cluster_arns[0].contains("my-cluster"));
104 assert!(result.cluster_arns[1].contains("default"));
105 }
106
107 #[tokio::test]
108 async fn describe_clusters_returns_details() {
109 let mut mock = crate::MockClient::new();
110 mock.expect_post("/").returning_json(serde_json::json!({
111 "clusters": [{
112 "clusterArn": "arn:aws:ecs:us-east-1:123456789012:cluster/my-cluster",
113 "clusterName": "my-cluster",
114 "status": "ACTIVE",
115 "registeredContainerInstancesCount": 3,
116 "runningTasksCount": 10,
117 "pendingTasksCount": 0,
118 "activeServicesCount": 2
119 }],
120 "failures": []
121 }));
122 let client = crate::AwsHttpClient::from_mock(mock);
123 let result = client
124 .ecs()
125 .describe_clusters(&DescribeClustersRequest {
126 clusters: vec!["my-cluster".into()],
127 ..Default::default()
128 })
129 .await
130 .unwrap();
131 assert_eq!(result.clusters.len(), 1);
132 let c = &result.clusters[0];
133 assert_eq!(c.cluster_name.as_deref(), Some("my-cluster"));
134 assert_eq!(c.status.as_deref(), Some("ACTIVE"));
135 assert_eq!(c.running_tasks_count, Some(10));
136 assert_eq!(c.active_services_count, Some(2));
137 assert_eq!(c.registered_container_instances_count, Some(3));
138 assert!(result.failures.is_empty());
139 }
140
141 #[tokio::test]
142 async fn describe_services_returns_service_details() {
143 let mut mock = crate::MockClient::new();
144 mock.expect_post("/").returning_json(serde_json::json!({
145 "services": [{
146 "serviceArn": "arn:aws:ecs:us-east-1:123456789012:service/my-cluster/my-svc",
147 "serviceName": "my-svc",
148 "clusterArn": "arn:aws:ecs:us-east-1:123456789012:cluster/my-cluster",
149 "status": "ACTIVE",
150 "desiredCount": 3,
151 "runningCount": 3,
152 "pendingCount": 0,
153 "launchType": "FARGATE",
154 "taskDefinition": "arn:aws:ecs:us-east-1:123456789012:task-definition/my-task:5",
155 "schedulingStrategy": "REPLICA",
156 "deployments": [{
157 "id": "ecs-svc/123",
158 "status": "PRIMARY",
159 "taskDefinition": "arn:aws:ecs:us-east-1:123456789012:task-definition/my-task:5",
160 "desiredCount": 3,
161 "runningCount": 3,
162 "pendingCount": 0,
163 "failedTasks": 0,
164 "createdAt": 1700000000.0,
165 "updatedAt": 1700000100.0,
166 "rolloutState": "COMPLETED"
167 }],
168 "createdAt": 1700000000.0
169 }],
170 "failures": []
171 }));
172 let client = crate::AwsHttpClient::from_mock(mock);
173 let result = client
174 .ecs()
175 .describe_services(&DescribeServicesRequest {
176 cluster: Some("my-cluster".into()),
177 services: vec!["my-svc".into()],
178 ..Default::default()
179 })
180 .await
181 .unwrap();
182 assert_eq!(result.services.len(), 1);
183 let svc = &result.services[0];
184 assert_eq!(svc.service_name.as_deref(), Some("my-svc"));
185 assert_eq!(svc.status.as_deref(), Some("ACTIVE"));
186 assert_eq!(svc.desired_count, Some(3));
187 assert_eq!(svc.running_count, Some(3));
188 assert_eq!(svc.launch_type.as_deref(), Some("FARGATE"));
189 assert_eq!(svc.deployments.len(), 1);
190 let dep = &svc.deployments[0];
191 assert_eq!(dep.status.as_deref(), Some("PRIMARY"));
192 assert_eq!(dep.rollout_state.as_deref(), Some("COMPLETED"));
193 }
194
195 #[tokio::test]
196 async fn describe_task_definition_returns_details() {
197 let mut mock = crate::MockClient::new();
198 mock.expect_post("/").returning_json(serde_json::json!({
199 "taskDefinition": {
200 "taskDefinitionArn": "arn:aws:ecs:us-east-1:123456789012:task-definition/my-task:5",
201 "family": "my-task",
202 "revision": 5,
203 "cpu": "512",
204 "memory": "1024",
205 "networkMode": "awsvpc",
206 "status": "ACTIVE",
207 "requiresCompatibilities": ["FARGATE"],
208 "containerDefinitions": [{
209 "name": "web",
210 "image": "nginx:latest",
211 "cpu": 256,
212 "memory": 512,
213 "essential": true,
214 "portMappings": [{
215 "containerPort": 80,
216 "protocol": "tcp"
217 }]
218 }],
219 "registeredAt": 1700000000.0
220 }
221 }));
222 let client = crate::AwsHttpClient::from_mock(mock);
223 let result = client
224 .ecs()
225 .describe_task_definition(&DescribeTaskDefinitionRequest {
226 task_definition: "my-task:5".into(),
227 ..Default::default()
228 })
229 .await
230 .unwrap();
231 let td = result.task_definition.as_ref().unwrap();
232 assert_eq!(td.family.as_deref(), Some("my-task"));
233 assert_eq!(td.revision, Some(5));
234 assert_eq!(td.cpu.as_deref(), Some("512"));
235 assert_eq!(td.memory.as_deref(), Some("1024"));
236 assert_eq!(td.network_mode.as_deref(), Some("awsvpc"));
237 assert_eq!(td.requires_compatibilities, vec!["FARGATE"]);
238 assert_eq!(td.container_definitions.len(), 1);
239 let c = &td.container_definitions[0];
240 assert_eq!(c.name.as_deref(), Some("web"));
241 assert_eq!(c.image.as_deref(), Some("nginx:latest"));
242 assert_eq!(c.cpu, Some(256));
243 assert_eq!(c.essential, Some(true));
244 assert_eq!(c.port_mappings.len(), 1);
245 assert_eq!(c.port_mappings[0].container_port, Some(80));
246 }
247
248 #[tokio::test]
249 async fn list_services_returns_arns() {
250 let mut mock = crate::MockClient::new();
251 mock.expect_post("/").returning_json(serde_json::json!({
252 "serviceArns": [
253 "arn:aws:ecs:us-east-1:123456789012:service/my-cluster/svc-a",
254 "arn:aws:ecs:us-east-1:123456789012:service/my-cluster/svc-b"
255 ]
256 }));
257 let client = crate::AwsHttpClient::from_mock(mock);
258 let result = client
259 .ecs()
260 .list_services(&ListServicesRequest {
261 cluster: Some("my-cluster".into()),
262 ..Default::default()
263 })
264 .await
265 .unwrap();
266 assert_eq!(result.service_arns.len(), 2);
267 assert!(result.service_arns[0].contains("svc-a"));
268 assert!(result.service_arns[1].contains("svc-b"));
269 }
270
271 #[tokio::test]
272 async fn update_service_forces_new_deployment() {
273 let mut mock = crate::MockClient::new();
274 mock.expect_post("/").returning_json(serde_json::json!({
275 "service": {
276 "serviceArn": "arn:aws:ecs:us-east-1:123456789012:service/my-cluster/my-svc",
277 "serviceName": "my-svc",
278 "clusterArn": "arn:aws:ecs:us-east-1:123456789012:cluster/my-cluster",
279 "status": "ACTIVE",
280 "desiredCount": 3,
281 "runningCount": 3,
282 "pendingCount": 0,
283 "launchType": "FARGATE",
284 "taskDefinition": "arn:aws:ecs:us-east-1:123456789012:task-definition/my-task:5",
285 "deployments": [
286 {
287 "id": "ecs-svc/new",
288 "status": "PRIMARY",
289 "taskDefinition": "arn:aws:ecs:us-east-1:123456789012:task-definition/my-task:5",
290 "desiredCount": 3,
291 "runningCount": 0,
292 "pendingCount": 3,
293 "createdAt": 1700000200.0,
294 "updatedAt": 1700000200.0,
295 "rolloutState": "IN_PROGRESS"
296 },
297 {
298 "id": "ecs-svc/old",
299 "status": "ACTIVE",
300 "taskDefinition": "arn:aws:ecs:us-east-1:123456789012:task-definition/my-task:5",
301 "desiredCount": 3,
302 "runningCount": 3,
303 "pendingCount": 0,
304 "createdAt": 1700000000.0,
305 "updatedAt": 1700000100.0,
306 "rolloutState": "COMPLETED"
307 }
308 ],
309 "createdAt": 1700000000.0
310 }
311 }));
312 let client = crate::AwsHttpClient::from_mock(mock);
313 let result = client
314 .ecs()
315 .update_service(&UpdateServiceRequest {
316 cluster: Some("my-cluster".into()),
317 service: "my-svc".into(),
318 force_new_deployment: Some(true),
319 ..Default::default()
320 })
321 .await
322 .unwrap();
323 let svc = result.service.as_ref().unwrap();
324 assert_eq!(svc.service_name.as_deref(), Some("my-svc"));
325 assert_eq!(svc.deployments.len(), 2);
326 assert_eq!(svc.deployments[0].status.as_deref(), Some("PRIMARY"));
327 assert_eq!(
328 svc.deployments[0].rollout_state.as_deref(),
329 Some("IN_PROGRESS")
330 );
331 }
332
333 #[tokio::test]
334 async fn deregister_task_definition_returns_inactive() {
335 let mut mock = crate::MockClient::new();
336 mock.expect_post("/").returning_json(serde_json::json!({
337 "taskDefinition": {
338 "taskDefinitionArn": "arn:aws:ecs:us-east-1:123456789012:task-definition/my-task:5",
339 "family": "my-task",
340 "revision": 5,
341 "cpu": "512",
342 "memory": "1024",
343 "networkMode": "awsvpc",
344 "status": "INACTIVE",
345 "requiresCompatibilities": ["FARGATE"],
346 "containerDefinitions": [{
347 "name": "web",
348 "image": "nginx:latest",
349 "cpu": 256,
350 "memory": 512,
351 "essential": true,
352 "portMappings": [{
353 "containerPort": 80,
354 "protocol": "tcp"
355 }]
356 }],
357 "registeredAt": 1700000000.0,
358 "deregisteredAt": 1700001000.0
359 }
360 }));
361 let client = crate::AwsHttpClient::from_mock(mock);
362 let result = client
363 .ecs()
364 .deregister_task_definition(&DeregisterTaskDefinitionRequest {
365 task_definition: "my-task:5".into(),
366 })
367 .await
368 .unwrap();
369 let td = result.task_definition.as_ref().unwrap();
370 assert_eq!(td.family.as_deref(), Some("my-task"));
371 assert_eq!(td.revision, Some(5));
372 assert_eq!(td.status.as_deref(), Some("INACTIVE"));
373 assert!(td.deregistered_at.is_some());
374 }
375}