Skip to main content

aws_lite_rs/api/
ecs.rs

1//! Amazon Elastic Container Service API client.
2//!
3//! Thin wrapper over generated ops. All URL construction and HTTP methods
4//! are in `ops::ecs::EcsOps`. This layer adds:
5//! - Ergonomic method signatures
6
7use 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
19/// Client for the Amazon Elastic Container Service API
20pub struct EcsClient<'a> {
21    ops: EcsOps<'a>,
22}
23
24impl<'a> EcsClient<'a> {
25    /// Create a new Amazon Elastic Container Service API client
26    pub(crate) fn new(client: &'a AwsHttpClient) -> Self {
27        Self {
28            ops: EcsOps::new(client),
29        }
30    }
31
32    /// Returns a list of existing clusters.
33    pub async fn list_clusters(&self, body: &ListClustersRequest) -> Result<ListClustersResponse> {
34        self.ops.list_clusters(body).await
35    }
36
37    /// Describes one or more of your clusters. For CLI examples, see describe-clusters.rst on GitHub.
38    pub async fn describe_clusters(
39        &self,
40        body: &DescribeClustersRequest,
41    ) -> Result<DescribeClustersResponse> {
42        self.ops.describe_clusters(body).await
43    }
44
45    /// Returns a list of services. You can filter the results by cluster, launch type, and scheduling strategy.
46    pub async fn list_services(&self, body: &ListServicesRequest) -> Result<ListServicesResponse> {
47        self.ops.list_services(body).await
48    }
49
50    /// Describes the specified services running in your cluster.
51    pub async fn describe_services(
52        &self,
53        body: &DescribeServicesRequest,
54    ) -> Result<DescribeServicesResponse> {
55        self.ops.describe_services(body).await
56    }
57
58    /// Describes a task definition. You can specify a family and revision to find information about a specific task definition,
59    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    /// Modifies the parameters of a service. On March 21, 2024, a change was made to resolve the task definition revision befor
67    pub async fn update_service(
68        &self,
69        body: &UpdateServiceRequest,
70    ) -> Result<UpdateServiceResponse> {
71        self.ops.update_service(body).await
72    }
73
74    /// Deregisters the specified task definition by family and revision. Upon deregistration, the task definition is marked as
75    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}