1use std::collections::BTreeMap;
2use std::sync::Arc;
3
4use chrono::{DateTime, Utc};
5use parking_lot::RwLock;
6use serde::{Deserialize, Serialize};
7use serde_json::Value;
8
9pub type SharedEcsState = Arc<RwLock<fakecloud_core::multi_account::MultiAccountState<EcsState>>>;
10
11impl fakecloud_core::multi_account::AccountState for EcsState {
12 fn new_for_account(account_id: &str, region: &str, _endpoint: &str) -> Self {
13 Self::new(account_id, region)
14 }
15}
16
17pub const ECS_SNAPSHOT_SCHEMA_VERSION: u32 = 4;
18
19#[derive(Clone, Debug, Serialize, Deserialize)]
23pub struct EcsSnapshot {
24 pub schema_version: u32,
25 pub accounts: Option<fakecloud_core::multi_account::MultiAccountState<EcsState>>,
26}
27
28#[derive(Clone, Debug, Default, Serialize, Deserialize)]
29pub struct EcsState {
30 pub account_id: String,
31 pub region: String,
32 pub clusters: BTreeMap<String, Cluster>,
34 pub task_definitions: BTreeMap<String, BTreeMap<i32, TaskDefinition>>,
38 pub next_revision: BTreeMap<String, i32>,
41 pub account_setting_defaults: BTreeMap<String, String>,
44 pub principal_account_settings: BTreeMap<String, BTreeMap<String, String>>,
47 #[serde(default)]
49 pub tasks: BTreeMap<String, Task>,
50 #[serde(default)]
53 pub events: Vec<LifecycleEvent>,
54 #[serde(default)]
59 pub services: BTreeMap<String, Service>,
60 #[serde(default)]
65 pub container_instances: BTreeMap<String, ContainerInstance>,
66 #[serde(default)]
68 pub attributes: BTreeMap<String, Attribute>,
69 #[serde(default)]
71 pub capacity_providers: BTreeMap<String, CapacityProvider>,
72 #[serde(default)]
74 pub task_sets: BTreeMap<String, TaskSet>,
75 #[serde(default)]
80 pub daemon_task_definitions: BTreeMap<String, BTreeMap<i32, DaemonTaskDefinition>>,
81 #[serde(default)]
83 pub next_daemon_revision: BTreeMap<String, i32>,
84 #[serde(default)]
87 pub daemons: BTreeMap<String, Daemon>,
88 #[serde(default)]
91 pub daemon_deployments: BTreeMap<String, DaemonDeployment>,
92 #[serde(default)]
96 pub express_gateway_services: BTreeMap<String, ExpressGatewayService>,
97 #[serde(default)]
102 pub service_revisions: BTreeMap<String, ServiceRevision>,
103}
104
105#[derive(Clone, Debug, Serialize, Deserialize)]
109pub struct ServiceRevision {
110 pub service_revision_arn: String,
111 pub service_arn: String,
112 pub cluster_arn: String,
113 pub task_definition_arn: String,
114 pub launch_type: String,
115 pub created_at: DateTime<Utc>,
116}
117
118impl EcsState {
119 pub fn new(account_id: &str, region: &str) -> Self {
120 Self {
121 account_id: account_id.to_string(),
122 region: region.to_string(),
123 clusters: BTreeMap::new(),
124 task_definitions: BTreeMap::new(),
125 next_revision: BTreeMap::new(),
126 account_setting_defaults: BTreeMap::new(),
127 principal_account_settings: BTreeMap::new(),
128 tasks: BTreeMap::new(),
129 events: Vec::new(),
130 services: BTreeMap::new(),
131 container_instances: BTreeMap::new(),
132 attributes: BTreeMap::new(),
133 capacity_providers: BTreeMap::new(),
134 task_sets: BTreeMap::new(),
135 daemon_task_definitions: BTreeMap::new(),
136 next_daemon_revision: BTreeMap::new(),
137 daemons: BTreeMap::new(),
138 daemon_deployments: BTreeMap::new(),
139 express_gateway_services: BTreeMap::new(),
140 service_revisions: BTreeMap::new(),
141 }
142 }
143
144 pub fn reset(&mut self) {
145 self.clusters.clear();
146 self.task_definitions.clear();
147 self.next_revision.clear();
148 self.account_setting_defaults.clear();
149 self.principal_account_settings.clear();
150 self.tasks.clear();
151 self.events.clear();
152 self.services.clear();
153 self.container_instances.clear();
154 self.attributes.clear();
155 self.capacity_providers.clear();
156 self.task_sets.clear();
157 self.daemon_task_definitions.clear();
158 self.next_daemon_revision.clear();
159 self.daemons.clear();
160 self.daemon_deployments.clear();
161 self.express_gateway_services.clear();
162 self.service_revisions.clear();
163 }
164
165 pub fn record_service_revision(&mut self, service: &Service) -> String {
169 let n = self
170 .service_revisions
171 .values()
172 .filter(|r| r.service_arn == service.service_arn)
173 .count() as i32
174 + 1;
175 let arn = format!("{}:{}", service.service_arn, n);
176 self.service_revisions.insert(
177 arn.clone(),
178 ServiceRevision {
179 service_revision_arn: arn.clone(),
180 service_arn: service.service_arn.clone(),
181 cluster_arn: service.cluster_arn.clone(),
182 task_definition_arn: service.task_definition_arn.clone(),
183 launch_type: service.launch_type.clone(),
184 created_at: Utc::now(),
185 },
186 );
187 arn
188 }
189
190 pub fn service_key(cluster_name: &str, service_name: &str) -> String {
194 format!("{}/{}", cluster_name, service_name)
195 }
196
197 pub fn service_arn(&self, cluster_name: &str, service_name: &str) -> String {
198 if self.arn_format_disabled("serviceLongArnFormat") {
199 format!(
201 "arn:aws:ecs:{}:{}:service/{}",
202 self.region, self.account_id, service_name
203 )
204 } else {
205 format!(
206 "arn:aws:ecs:{}:{}:service/{}/{}",
207 self.region, self.account_id, cluster_name, service_name
208 )
209 }
210 }
211
212 pub fn task_arn(&self, cluster_name: &str, task_id: &str) -> String {
213 if self.arn_format_disabled("taskLongArnFormat") {
214 format!(
215 "arn:aws:ecs:{}:{}:task/{}",
216 self.region, self.account_id, task_id
217 )
218 } else {
219 format!(
220 "arn:aws:ecs:{}:{}:task/{}/{}",
221 self.region, self.account_id, cluster_name, task_id
222 )
223 }
224 }
225
226 pub fn container_instance_arn(&self, cluster_name: &str, instance_id: &str) -> String {
227 if self.arn_format_disabled("containerInstanceLongArnFormat") {
228 format!(
229 "arn:aws:ecs:{}:{}:container-instance/{}",
230 self.region, self.account_id, instance_id
231 )
232 } else {
233 format!(
234 "arn:aws:ecs:{}:{}:container-instance/{}/{}",
235 self.region, self.account_id, cluster_name, instance_id
236 )
237 }
238 }
239
240 pub fn effective_account_setting(
245 &self,
246 name: &str,
247 principal_arn: Option<&str>,
248 ) -> Option<String> {
249 if let Some(arn) = principal_arn {
250 if let Some(p) = self.principal_account_settings.get(arn) {
251 if let Some(v) = p.get(name) {
252 return Some(v.clone());
253 }
254 }
255 }
256 self.account_setting_defaults.get(name).cloned()
257 }
258
259 fn arn_format_disabled(&self, setting_name: &str) -> bool {
264 matches!(
265 self.effective_account_setting(setting_name, None)
266 .as_deref(),
267 Some("disabled")
268 )
269 }
270
271 pub fn push_event(&mut self, event: LifecycleEvent) {
273 const MAX_EVENTS: usize = 1024;
274 if self.events.len() >= MAX_EVENTS {
275 self.events.drain(0..self.events.len() - MAX_EVENTS + 1);
276 }
277 self.events.push(event);
278 }
279
280 pub fn cluster_arn(&self, cluster_name: &str) -> String {
281 format!(
282 "arn:aws:ecs:{}:{}:cluster/{}",
283 self.region, self.account_id, cluster_name
284 )
285 }
286
287 pub fn task_definition_arn(&self, family: &str, revision: i32) -> String {
288 format!(
289 "arn:aws:ecs:{}:{}:task-definition/{}:{}",
290 self.region, self.account_id, family, revision
291 )
292 }
293
294 pub fn resolve_cluster_name(input: Option<&str>) -> String {
298 let raw = input.unwrap_or("").trim();
299 if raw.is_empty() {
300 return "default".to_string();
301 }
302 if let Some(name) = raw.rsplit_once('/').map(|(_, n)| n) {
303 return name.to_string();
304 }
305 raw.to_string()
306 }
307
308 pub fn allocate_revision(&mut self, family: &str) -> i32 {
311 let next = self.next_revision.entry(family.to_string()).or_insert(0);
312 *next += 1;
313 *next
314 }
315}
316
317#[derive(Clone, Debug, Serialize, Deserialize)]
318pub struct Cluster {
319 pub cluster_name: String,
320 pub cluster_arn: String,
321 pub status: String,
322 pub registered_container_instances_count: i32,
323 pub running_tasks_count: i32,
324 pub pending_tasks_count: i32,
325 pub active_services_count: i32,
326 #[serde(default)]
327 pub statistics: Vec<Value>,
328 #[serde(default)]
329 pub tags: Vec<TagEntry>,
330 #[serde(default)]
331 pub settings: Vec<Value>,
332 pub configuration: Option<Value>,
333 #[serde(default)]
334 pub capacity_providers: Vec<String>,
335 #[serde(default)]
336 pub default_capacity_provider_strategy: Vec<Value>,
337 #[serde(default)]
338 pub attachments: Vec<Value>,
339 pub attachments_status: Option<String>,
340 pub service_connect_defaults: Option<Value>,
341 pub created_at: DateTime<Utc>,
342}
343
344impl Cluster {
345 pub fn new(cluster_name: &str, cluster_arn: String) -> Self {
346 Self {
347 cluster_name: cluster_name.to_string(),
348 cluster_arn,
349 status: "ACTIVE".to_string(),
350 registered_container_instances_count: 0,
351 running_tasks_count: 0,
352 pending_tasks_count: 0,
353 active_services_count: 0,
354 statistics: Vec::new(),
355 tags: Vec::new(),
356 settings: Vec::new(),
357 configuration: None,
358 capacity_providers: Vec::new(),
359 default_capacity_provider_strategy: Vec::new(),
360 attachments: Vec::new(),
361 attachments_status: None,
362 service_connect_defaults: None,
363 created_at: Utc::now(),
364 }
365 }
366}
367
368#[derive(Clone, Debug, Serialize, Deserialize)]
369pub struct TagEntry {
370 pub key: String,
371 pub value: String,
372}
373
374#[derive(Clone, Debug, Serialize, Deserialize)]
375pub struct TaskDefinition {
376 pub family: String,
377 pub revision: i32,
378 pub task_definition_arn: String,
379 #[serde(default)]
384 pub container_definitions: Vec<Value>,
385 pub status: String,
386 pub task_role_arn: Option<String>,
387 pub execution_role_arn: Option<String>,
388 pub network_mode: Option<String>,
389 #[serde(default)]
390 pub requires_compatibilities: Vec<String>,
391 #[serde(default)]
392 pub compatibilities: Vec<String>,
393 pub cpu: Option<String>,
394 pub memory: Option<String>,
395 pub pid_mode: Option<String>,
396 pub ipc_mode: Option<String>,
397 #[serde(default)]
398 pub volumes: Vec<Value>,
399 #[serde(default)]
400 pub placement_constraints: Vec<Value>,
401 pub proxy_configuration: Option<Value>,
402 #[serde(default)]
403 pub inference_accelerators: Vec<Value>,
404 pub ephemeral_storage: Option<Value>,
405 pub runtime_platform: Option<Value>,
406 #[serde(default)]
407 pub requires_attributes: Vec<Value>,
408 pub registered_at: DateTime<Utc>,
409 pub registered_by: Option<String>,
410 pub deregistered_at: Option<DateTime<Utc>>,
411 #[serde(default)]
412 pub tags: Vec<TagEntry>,
413 pub enable_fault_injection: Option<bool>,
414}
415
416#[derive(Clone, Debug, Serialize, Deserialize)]
417pub struct Task {
418 pub task_arn: String,
419 pub task_id: String,
420 pub cluster_arn: String,
421 pub cluster_name: String,
422 pub task_definition_arn: String,
423 pub family: String,
424 pub revision: i32,
425 #[serde(default)]
428 pub container_instance_arn: Option<String>,
429 #[serde(default)]
434 pub capacity_provider_name: Option<String>,
435 pub last_status: String,
438 pub desired_status: String,
441 pub launch_type: String,
442 pub platform_version: Option<String>,
443 pub cpu: Option<String>,
444 pub memory: Option<String>,
445 #[serde(default)]
446 pub containers: Vec<Container>,
447 #[serde(default)]
448 pub overrides: Value,
449 pub started_by: Option<String>,
450 pub group: Option<String>,
451 pub connectivity: String,
452 pub stop_code: Option<String>,
453 pub stopped_reason: Option<String>,
454 pub created_at: DateTime<Utc>,
455 pub started_at: Option<DateTime<Utc>>,
456 pub stopping_at: Option<DateTime<Utc>>,
457 pub stopped_at: Option<DateTime<Utc>>,
458 pub pull_started_at: Option<DateTime<Utc>>,
459 pub pull_stopped_at: Option<DateTime<Utc>>,
460 pub connectivity_at: Option<DateTime<Utc>>,
461 pub started_by_ref_id: Option<String>,
462 pub execution_role_arn: Option<String>,
463 pub task_role_arn: Option<String>,
464 #[serde(default)]
465 pub tags: Vec<TagEntry>,
466 pub awslogs: Option<AwsLogsConfig>,
470 #[serde(default)]
474 pub captured_logs: String,
475 pub protection: Option<TaskProtection>,
478 #[serde(default)]
482 pub enable_execute_command: bool,
483 #[serde(default)]
486 pub attachments: Vec<TaskAttachment>,
487 #[serde(default)]
490 pub volume_configurations: Vec<Value>,
491 #[serde(default)]
494 pub task_set_arn: Option<String>,
495}
496
497#[derive(Clone, Debug, Serialize, Deserialize)]
498pub struct TaskAttachment {
499 pub id: String,
500 #[serde(rename = "type")]
501 pub attachment_type: String,
502 pub status: String,
503 #[serde(default)]
504 pub details: Vec<AttachmentDetail>,
505}
506
507#[derive(Clone, Debug, Serialize, Deserialize)]
508pub struct AttachmentDetail {
509 pub name: String,
510 pub value: String,
511}
512
513#[derive(Clone, Debug, Serialize, Deserialize)]
514pub struct TaskProtection {
515 pub enabled: bool,
516 pub expiration: Option<DateTime<Utc>>,
517}
518
519#[derive(Clone, Debug, Serialize, Deserialize)]
520pub struct Container {
521 pub container_arn: String,
522 pub name: String,
523 pub image: String,
524 pub task_arn: String,
525 pub last_status: String,
526 pub exit_code: Option<i64>,
527 pub reason: Option<String>,
528 pub runtime_id: Option<String>,
529 pub essential: bool,
530 pub cpu: Option<String>,
531 pub memory: Option<String>,
532 pub memory_reservation: Option<String>,
533 #[serde(default)]
534 pub network_bindings: Vec<Value>,
535 #[serde(default)]
536 pub network_interfaces: Vec<Value>,
537 pub health_status: Option<String>,
538 pub managed_agents: Option<Value>,
539 #[serde(default)]
543 pub image_digest: Option<String>,
544}
545
546#[derive(Clone, Debug, Serialize, Deserialize)]
547pub struct AwsLogsConfig {
548 pub group: String,
549 pub stream_prefix: Option<String>,
550 pub region: String,
551 pub container_name: String,
552}
553
554impl AwsLogsConfig {
555 pub fn stream_name(&self, task_id: &str) -> String {
556 match &self.stream_prefix {
557 Some(p) => format!("{}/{}/{}", p, self.container_name, task_id),
558 None => format!("{}/{}", self.container_name, task_id),
559 }
560 }
561}
562
563#[derive(Clone, Debug, Serialize, Deserialize)]
564pub struct LifecycleEvent {
565 pub at: DateTime<Utc>,
566 pub event_type: String,
567 pub task_arn: Option<String>,
568 pub cluster_arn: Option<String>,
569 pub last_status: Option<String>,
570 pub detail: Value,
571}
572
573#[derive(Clone, Debug, Serialize, Deserialize)]
574pub struct Service {
575 pub service_name: String,
576 pub service_arn: String,
577 pub cluster_name: String,
578 pub cluster_arn: String,
579 pub task_definition_arn: String,
580 pub family: String,
581 pub revision: i32,
582 pub desired_count: i32,
583 pub running_count: i32,
584 pub pending_count: i32,
585 pub launch_type: String,
586 pub status: String,
587 pub scheduling_strategy: String,
588 pub deployment_controller: String,
589 pub minimum_healthy_percent: Option<i32>,
590 pub maximum_percent: Option<i32>,
591 pub circuit_breaker: Option<CircuitBreakerConfig>,
593 #[serde(default)]
594 pub deployments: Vec<Deployment>,
595 #[serde(default)]
596 pub load_balancers: Vec<Value>,
597 #[serde(default)]
598 pub service_registries: Vec<Value>,
599 #[serde(default)]
600 pub placement_constraints: Vec<Value>,
601 #[serde(default)]
602 pub placement_strategy: Vec<Value>,
603 #[serde(default)]
604 pub network_configuration: Option<Value>,
605 #[serde(default)]
606 pub tags: Vec<TagEntry>,
607 pub created_at: DateTime<Utc>,
608 pub created_by: Option<String>,
609 pub role_arn: Option<String>,
610 #[serde(default)]
613 pub platform_version: Option<String>,
614 #[serde(default)]
617 pub health_check_grace_period_seconds: Option<i32>,
618 #[serde(default)]
620 pub enable_execute_command: bool,
621 #[serde(default)]
624 pub enable_ecs_managed_tags: bool,
625 #[serde(default)]
629 pub propagate_tags: Option<String>,
630 #[serde(default)]
634 pub capacity_provider_strategy: Vec<Value>,
635 #[serde(default)]
638 pub availability_zone_rebalancing: Option<String>,
639 #[serde(default)]
642 pub volume_configurations: Vec<Value>,
643 #[serde(default)]
647 pub service_connect_configuration: Option<Value>,
648}
649
650#[derive(Clone, Debug, Serialize, Deserialize)]
651pub struct CircuitBreakerConfig {
652 pub enable: bool,
653 pub rollback: bool,
654}
655
656#[derive(Clone, Debug, Serialize, Deserialize)]
657pub struct Deployment {
658 pub deployment_id: String,
659 pub status: String,
660 pub task_definition_arn: String,
661 pub desired_count: i32,
662 pub pending_count: i32,
663 pub running_count: i32,
664 pub failed_tasks: i32,
665 pub created_at: DateTime<Utc>,
666 pub updated_at: DateTime<Utc>,
667 pub launch_type: String,
668 pub rollout_state: String,
669 pub rollout_state_reason: Option<String>,
670 #[serde(default)]
674 pub lifecycle_hooks: Vec<Value>,
675 #[serde(default)]
679 pub pending_hook_id: Option<String>,
680 #[serde(default)]
684 pub lifecycle_stage: Option<String>,
685}
686
687#[derive(Clone, Debug, Serialize, Deserialize)]
688pub struct ContainerInstance {
689 pub container_instance_arn: String,
690 pub ec2_instance_id: Option<String>,
691 pub cluster_name: String,
692 pub cluster_arn: String,
693 pub status: String,
694 pub version: i64,
695 pub version_info: Option<Value>,
696 pub agent_connected: bool,
697 pub agent_update_status: Option<String>,
698 pub remaining_resources: Vec<Value>,
699 pub registered_resources: Vec<Value>,
700 pub running_tasks_count: i32,
701 pub pending_tasks_count: i32,
702 pub registered_at: DateTime<Utc>,
703 #[serde(default)]
704 pub attributes: Vec<AttributeRef>,
705 #[serde(default)]
706 pub tags: Vec<TagEntry>,
707 pub capacity_provider_name: Option<String>,
708 pub health_status: Option<Value>,
709}
710
711#[derive(Clone, Debug, Serialize, Deserialize)]
712pub struct AttributeRef {
713 pub name: String,
714 pub value: Option<String>,
715 pub target_type: Option<String>,
716 pub target_id: Option<String>,
717}
718
719#[derive(Clone, Debug, Serialize, Deserialize)]
720pub struct Attribute {
721 pub cluster_name: String,
722 pub target_type: String,
723 pub target_id: String,
724 pub name: String,
725 pub value: Option<String>,
726}
727
728#[derive(Clone, Debug, Serialize, Deserialize)]
729pub struct CapacityProvider {
730 pub name: String,
731 pub arn: String,
732 pub status: String,
733 pub auto_scaling_group_provider: Option<Value>,
734 pub update_status: Option<String>,
735 pub update_status_reason: Option<String>,
736 pub created_at: DateTime<Utc>,
737 #[serde(default)]
738 pub tags: Vec<TagEntry>,
739}
740
741#[derive(Clone, Debug, Serialize, Deserialize)]
742pub struct TaskSet {
743 pub task_set_id: String,
744 pub task_set_arn: String,
745 pub service_arn: String,
746 pub cluster_arn: String,
747 pub service_name: String,
748 pub cluster_name: String,
749 pub external_id: Option<String>,
750 pub status: String,
751 pub task_definition: String,
752 pub computed_desired_count: i32,
753 pub pending_count: i32,
754 pub running_count: i32,
755 pub launch_type: Option<String>,
756 pub platform_version: Option<String>,
757 pub scale: Option<Value>,
758 pub stability_status: String,
759 pub created_at: DateTime<Utc>,
760 pub updated_at: DateTime<Utc>,
761 #[serde(default)]
762 pub load_balancers: Vec<Value>,
763 #[serde(default)]
764 pub service_registries: Vec<Value>,
765 #[serde(default)]
766 pub capacity_provider_strategy: Vec<Value>,
767 #[serde(default)]
768 pub tags: Vec<TagEntry>,
769}
770
771#[derive(Clone, Debug, Serialize, Deserialize)]
775pub struct DaemonTaskDefinition {
776 pub family: String,
777 pub revision: i32,
778 pub task_definition_arn: String,
779 pub status: String,
780 pub container_definitions: Vec<Value>,
781 pub task_role_arn: Option<String>,
782 pub execution_role_arn: Option<String>,
783 pub cpu: Option<String>,
784 pub memory: Option<String>,
785 #[serde(default)]
786 pub volumes: Vec<Value>,
787 pub registered_at: DateTime<Utc>,
788 pub deregistered_at: Option<DateTime<Utc>>,
789 #[serde(default)]
790 pub tags: Vec<TagEntry>,
791}
792
793#[derive(Clone, Debug, Serialize, Deserialize)]
797pub struct Daemon {
798 pub daemon_name: String,
799 pub daemon_arn: String,
800 pub cluster_arn: String,
801 pub cluster_name: String,
802 pub daemon_task_definition_arn: String,
803 pub status: String,
804 pub deployment_arn: String,
805 pub created_at: DateTime<Utc>,
806 pub updated_at: DateTime<Utc>,
807 #[serde(default)]
808 pub capacity_provider_arns: Vec<String>,
809 pub deployment_configuration: Option<Value>,
810 pub propagate_tags: Option<String>,
811 pub enable_ecs_managed_tags: bool,
812 pub enable_execute_command: bool,
813 pub client_token: Option<String>,
814 #[serde(default)]
815 pub tags: Vec<TagEntry>,
816 #[serde(default)]
818 pub deployment_history: Vec<String>,
819 #[serde(default)]
821 pub task_arns: Vec<String>,
822}
823
824#[derive(Clone, Debug, Serialize, Deserialize)]
828pub struct DaemonDeployment {
829 pub deployment_arn: String,
830 pub daemon_arn: String,
831 pub daemon_name: String,
832 pub cluster_arn: String,
833 pub task_definition_arn: String,
834 pub status: String,
835 pub revision: i64,
836 pub created_at: DateTime<Utc>,
837 pub updated_at: DateTime<Utc>,
838}
839
840#[derive(Clone, Debug, Serialize, Deserialize)]
843pub struct ExpressGatewayService {
844 pub service_name: String,
845 pub service_arn: String,
846 pub cluster_arn: String,
847 pub cluster_name: String,
848 pub status: String,
849 pub execution_role_arn: String,
850 pub infrastructure_role_arn: String,
851 pub task_role_arn: Option<String>,
852 pub primary_container: Value,
853 pub network_configuration: Option<Value>,
854 pub health_check_path: Option<String>,
855 pub cpu: Option<String>,
856 pub memory: Option<String>,
857 pub scaling_target: Option<Value>,
858 pub created_at: DateTime<Utc>,
859 pub updated_at: DateTime<Utc>,
860 #[serde(default)]
861 pub tags: Vec<TagEntry>,
862}
863
864impl EcsState {
865 pub fn daemon_key(cluster: &str, name: &str) -> String {
867 format!("{}/{}", cluster, name)
868 }
869
870 pub fn express_gateway_key(cluster: &str, name: &str) -> String {
872 format!("{}/{}", cluster, name)
873 }
874
875 pub fn allocate_daemon_revision(&mut self, family: &str) -> i32 {
877 let entry = self
878 .next_daemon_revision
879 .entry(family.to_string())
880 .or_insert(0);
881 *entry += 1;
882 *entry
883 }
884
885 pub fn daemon_arn(&self, cluster: &str, name: &str) -> String {
887 fakecloud_aws::arn::Arn::new(
888 "ecs",
889 &self.region,
890 &self.account_id,
891 &format!("daemon/{}/{}", cluster, name),
892 )
893 .to_string()
894 }
895
896 pub fn express_gateway_arn(&self, cluster: &str, name: &str) -> String {
898 fakecloud_aws::arn::Arn::new(
899 "ecs",
900 &self.region,
901 &self.account_id,
902 &format!("express-gateway-service/{}/{}", cluster, name),
903 )
904 .to_string()
905 }
906
907 pub fn daemon_task_definition_arn(&self, family: &str, revision: i32) -> String {
909 fakecloud_aws::arn::Arn::new(
910 "ecs",
911 &self.region,
912 &self.account_id,
913 &format!("daemon-task-definition/{}:{}", family, revision),
914 )
915 .to_string()
916 }
917
918 pub fn daemon_deployment_arn(&self, daemon_name: &str, deployment_id: &str) -> String {
920 fakecloud_aws::arn::Arn::new(
921 "ecs",
922 &self.region,
923 &self.account_id,
924 &format!("daemon-deployment/{}/{}", daemon_name, deployment_id),
925 )
926 .to_string()
927 }
928}
929
930#[cfg(test)]
931mod tests {
932 use super::*;
933
934 #[test]
935 fn resolve_cluster_name_defaults_to_default() {
936 assert_eq!(EcsState::resolve_cluster_name(None), "default");
937 assert_eq!(EcsState::resolve_cluster_name(Some("")), "default");
938 assert_eq!(EcsState::resolve_cluster_name(Some(" ")), "default");
939 }
940
941 #[test]
942 fn resolve_cluster_name_strips_arn_prefix() {
943 assert_eq!(
944 EcsState::resolve_cluster_name(Some("arn:aws:ecs:us-east-1:111122223333:cluster/prod")),
945 "prod"
946 );
947 }
948
949 #[test]
950 fn resolve_cluster_name_passes_through_name() {
951 assert_eq!(EcsState::resolve_cluster_name(Some("prod")), "prod");
952 }
953
954 #[test]
955 fn allocate_revision_monotonic() {
956 let mut s = EcsState::new("111122223333", "us-east-1");
957 assert_eq!(s.allocate_revision("web"), 1);
958 assert_eq!(s.allocate_revision("web"), 2);
959 assert_eq!(s.allocate_revision("worker"), 1);
960 assert_eq!(s.allocate_revision("web"), 3);
961 }
962
963 #[test]
964 fn cluster_arn_format() {
965 let s = EcsState::new("111122223333", "us-east-1");
966 assert_eq!(
967 s.cluster_arn("prod"),
968 "arn:aws:ecs:us-east-1:111122223333:cluster/prod"
969 );
970 }
971
972 #[test]
973 fn task_definition_arn_format() {
974 let s = EcsState::new("111122223333", "us-east-1");
975 assert_eq!(
976 s.task_definition_arn("web", 3),
977 "arn:aws:ecs:us-east-1:111122223333:task-definition/web:3"
978 );
979 }
980
981 #[test]
982 fn task_arn_long_format_default() {
983 let s = EcsState::new("111122223333", "us-east-1");
984 assert_eq!(
985 s.task_arn("prod", "abc123"),
986 "arn:aws:ecs:us-east-1:111122223333:task/prod/abc123"
987 );
988 }
989
990 #[test]
991 fn task_arn_short_when_disabled() {
992 let mut s = EcsState::new("111122223333", "us-east-1");
993 s.account_setting_defaults
994 .insert("taskLongArnFormat".into(), "disabled".into());
995 assert_eq!(
996 s.task_arn("prod", "abc123"),
997 "arn:aws:ecs:us-east-1:111122223333:task/abc123"
998 );
999 }
1000
1001 #[test]
1002 fn service_arn_short_when_disabled() {
1003 let mut s = EcsState::new("111122223333", "us-east-1");
1004 s.account_setting_defaults
1005 .insert("serviceLongArnFormat".into(), "disabled".into());
1006 assert_eq!(
1007 s.service_arn("prod", "web"),
1008 "arn:aws:ecs:us-east-1:111122223333:service/web"
1009 );
1010 }
1011
1012 #[test]
1013 fn container_instance_arn_short_when_disabled() {
1014 let mut s = EcsState::new("111122223333", "us-east-1");
1015 s.account_setting_defaults
1016 .insert("containerInstanceLongArnFormat".into(), "disabled".into());
1017 assert_eq!(
1018 s.container_instance_arn("prod", "i-abc"),
1019 "arn:aws:ecs:us-east-1:111122223333:container-instance/i-abc"
1020 );
1021 }
1022
1023 #[test]
1024 fn principal_setting_overrides_default() {
1025 let mut s = EcsState::new("111122223333", "us-east-1");
1026 s.account_setting_defaults
1027 .insert("taskLongArnFormat".into(), "disabled".into());
1028 let principal = "arn:aws:iam::111122223333:user/alice".to_string();
1029 let mut p = BTreeMap::new();
1030 p.insert("taskLongArnFormat".into(), "enabled".into());
1031 s.principal_account_settings.insert(principal.clone(), p);
1032 assert_eq!(
1033 s.effective_account_setting("taskLongArnFormat", Some(&principal))
1034 .as_deref(),
1035 Some("enabled")
1036 );
1037 assert_eq!(
1039 s.effective_account_setting("taskLongArnFormat", None)
1040 .as_deref(),
1041 Some("disabled")
1042 );
1043 }
1044
1045 #[test]
1046 fn reset_clears_all() {
1047 let mut s = EcsState::new("111122223333", "us-east-1");
1048 s.clusters.insert(
1049 "prod".to_string(),
1050 Cluster::new("prod", s.cluster_arn("prod")),
1051 );
1052 s.allocate_revision("web");
1053 s.account_setting_defaults
1054 .insert("serviceLongArnFormat".into(), "enabled".into());
1055 s.reset();
1056 assert!(s.clusters.is_empty());
1057 assert!(s.next_revision.is_empty());
1058 assert!(s.account_setting_defaults.is_empty());
1059 }
1060}