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, region: &str, cluster_name: &str, service_name: &str) -> String {
201 if self.arn_format_disabled("serviceLongArnFormat") {
202 format!(
204 "arn:aws:ecs:{}:{}:service/{}",
205 region, self.account_id, service_name
206 )
207 } else {
208 format!(
209 "arn:aws:ecs:{}:{}:service/{}/{}",
210 region, self.account_id, cluster_name, service_name
211 )
212 }
213 }
214
215 pub fn task_arn(&self, region: &str, cluster_name: &str, task_id: &str) -> String {
216 if self.arn_format_disabled("taskLongArnFormat") {
217 format!(
218 "arn:aws:ecs:{}:{}:task/{}",
219 region, self.account_id, task_id
220 )
221 } else {
222 format!(
223 "arn:aws:ecs:{}:{}:task/{}/{}",
224 region, self.account_id, cluster_name, task_id
225 )
226 }
227 }
228
229 pub fn container_instance_arn(
230 &self,
231 region: &str,
232 cluster_name: &str,
233 instance_id: &str,
234 ) -> String {
235 if self.arn_format_disabled("containerInstanceLongArnFormat") {
236 format!(
237 "arn:aws:ecs:{}:{}:container-instance/{}",
238 region, self.account_id, instance_id
239 )
240 } else {
241 format!(
242 "arn:aws:ecs:{}:{}:container-instance/{}/{}",
243 region, self.account_id, cluster_name, instance_id
244 )
245 }
246 }
247
248 pub fn effective_account_setting(
253 &self,
254 name: &str,
255 principal_arn: Option<&str>,
256 ) -> Option<String> {
257 if let Some(arn) = principal_arn {
258 if let Some(p) = self.principal_account_settings.get(arn) {
259 if let Some(v) = p.get(name) {
260 return Some(v.clone());
261 }
262 }
263 }
264 self.account_setting_defaults.get(name).cloned()
265 }
266
267 fn arn_format_disabled(&self, setting_name: &str) -> bool {
272 matches!(
273 self.effective_account_setting(setting_name, None)
274 .as_deref(),
275 Some("disabled")
276 )
277 }
278
279 pub fn push_event(&mut self, event: LifecycleEvent) {
281 const MAX_EVENTS: usize = 1024;
282 if self.events.len() >= MAX_EVENTS {
283 self.events.drain(0..self.events.len() - MAX_EVENTS + 1);
284 }
285 self.events.push(event);
286 }
287
288 pub fn cluster_arn(&self, region: &str, cluster_name: &str) -> String {
289 format!(
290 "arn:aws:ecs:{}:{}:cluster/{}",
291 region, self.account_id, cluster_name
292 )
293 }
294
295 pub fn task_definition_arn(&self, region: &str, family: &str, revision: i32) -> String {
296 format!(
297 "arn:aws:ecs:{}:{}:task-definition/{}:{}",
298 region, self.account_id, family, revision
299 )
300 }
301
302 pub fn resolve_cluster_name(input: Option<&str>) -> String {
306 let raw = input.unwrap_or("").trim();
307 if raw.is_empty() {
308 return "default".to_string();
309 }
310 if let Some(name) = raw.rsplit_once('/').map(|(_, n)| n) {
311 return name.to_string();
312 }
313 raw.to_string()
314 }
315
316 pub fn allocate_revision(&mut self, family: &str) -> i32 {
319 let next = self.next_revision.entry(family.to_string()).or_insert(0);
320 *next += 1;
321 *next
322 }
323}
324
325#[derive(Clone, Debug, Serialize, Deserialize)]
326pub struct Cluster {
327 pub cluster_name: String,
328 pub cluster_arn: String,
329 pub status: String,
330 pub registered_container_instances_count: i32,
331 pub running_tasks_count: i32,
332 pub pending_tasks_count: i32,
333 pub active_services_count: i32,
334 #[serde(default)]
335 pub statistics: Vec<Value>,
336 #[serde(default)]
337 pub tags: Vec<TagEntry>,
338 #[serde(default)]
339 pub settings: Vec<Value>,
340 pub configuration: Option<Value>,
341 #[serde(default)]
342 pub capacity_providers: Vec<String>,
343 #[serde(default)]
344 pub default_capacity_provider_strategy: Vec<Value>,
345 #[serde(default)]
346 pub attachments: Vec<Value>,
347 pub attachments_status: Option<String>,
348 pub service_connect_defaults: Option<Value>,
349 pub created_at: DateTime<Utc>,
350}
351
352impl Cluster {
353 pub fn new(cluster_name: &str, cluster_arn: String) -> Self {
354 Self {
355 cluster_name: cluster_name.to_string(),
356 cluster_arn,
357 status: "ACTIVE".to_string(),
358 registered_container_instances_count: 0,
359 running_tasks_count: 0,
360 pending_tasks_count: 0,
361 active_services_count: 0,
362 statistics: Vec::new(),
363 tags: Vec::new(),
364 settings: Vec::new(),
365 configuration: None,
366 capacity_providers: Vec::new(),
367 default_capacity_provider_strategy: Vec::new(),
368 attachments: Vec::new(),
369 attachments_status: None,
370 service_connect_defaults: None,
371 created_at: Utc::now(),
372 }
373 }
374}
375
376#[derive(Clone, Debug, Serialize, Deserialize)]
377pub struct TagEntry {
378 pub key: String,
379 pub value: String,
380}
381
382#[derive(Clone, Debug, Serialize, Deserialize)]
383pub struct TaskDefinition {
384 pub family: String,
385 pub revision: i32,
386 pub task_definition_arn: String,
387 #[serde(default)]
392 pub container_definitions: Vec<Value>,
393 pub status: String,
394 pub task_role_arn: Option<String>,
395 pub execution_role_arn: Option<String>,
396 pub network_mode: Option<String>,
397 #[serde(default)]
398 pub requires_compatibilities: Vec<String>,
399 #[serde(default)]
400 pub compatibilities: Vec<String>,
401 pub cpu: Option<String>,
402 pub memory: Option<String>,
403 pub pid_mode: Option<String>,
404 pub ipc_mode: Option<String>,
405 #[serde(default)]
406 pub volumes: Vec<Value>,
407 #[serde(default)]
408 pub placement_constraints: Vec<Value>,
409 pub proxy_configuration: Option<Value>,
410 #[serde(default)]
411 pub inference_accelerators: Vec<Value>,
412 pub ephemeral_storage: Option<Value>,
413 pub runtime_platform: Option<Value>,
414 #[serde(default)]
415 pub requires_attributes: Vec<Value>,
416 pub registered_at: DateTime<Utc>,
417 pub registered_by: Option<String>,
418 pub deregistered_at: Option<DateTime<Utc>>,
419 #[serde(default)]
420 pub tags: Vec<TagEntry>,
421 pub enable_fault_injection: Option<bool>,
422}
423
424#[derive(Clone, Debug, Serialize, Deserialize)]
425pub struct Task {
426 pub task_arn: String,
427 pub task_id: String,
428 pub cluster_arn: String,
429 pub cluster_name: String,
430 pub task_definition_arn: String,
431 pub family: String,
432 pub revision: i32,
433 #[serde(default)]
436 pub container_instance_arn: Option<String>,
437 #[serde(default)]
442 pub capacity_provider_name: Option<String>,
443 pub last_status: String,
446 pub desired_status: String,
449 pub launch_type: String,
450 pub platform_version: Option<String>,
451 pub cpu: Option<String>,
452 pub memory: Option<String>,
453 #[serde(default)]
454 pub containers: Vec<Container>,
455 #[serde(default)]
456 pub overrides: Value,
457 pub started_by: Option<String>,
458 pub group: Option<String>,
459 pub connectivity: String,
460 pub stop_code: Option<String>,
461 pub stopped_reason: Option<String>,
462 pub created_at: DateTime<Utc>,
463 pub started_at: Option<DateTime<Utc>>,
464 pub stopping_at: Option<DateTime<Utc>>,
465 pub stopped_at: Option<DateTime<Utc>>,
466 pub pull_started_at: Option<DateTime<Utc>>,
467 pub pull_stopped_at: Option<DateTime<Utc>>,
468 pub connectivity_at: Option<DateTime<Utc>>,
469 pub started_by_ref_id: Option<String>,
470 pub execution_role_arn: Option<String>,
471 pub task_role_arn: Option<String>,
472 #[serde(default)]
473 pub tags: Vec<TagEntry>,
474 pub awslogs: Option<AwsLogsConfig>,
478 #[serde(default)]
482 pub captured_logs: String,
483 pub protection: Option<TaskProtection>,
486 #[serde(default)]
490 pub enable_execute_command: bool,
491 #[serde(default)]
494 pub attachments: Vec<TaskAttachment>,
495 #[serde(default)]
498 pub volume_configurations: Vec<Value>,
499 #[serde(default)]
502 pub task_set_arn: Option<String>,
503}
504
505#[derive(Clone, Debug, Serialize, Deserialize)]
506pub struct TaskAttachment {
507 pub id: String,
508 #[serde(rename = "type")]
509 pub attachment_type: String,
510 pub status: String,
511 #[serde(default)]
512 pub details: Vec<AttachmentDetail>,
513}
514
515#[derive(Clone, Debug, Serialize, Deserialize)]
516pub struct AttachmentDetail {
517 pub name: String,
518 pub value: String,
519}
520
521#[derive(Clone, Debug, Serialize, Deserialize)]
522pub struct TaskProtection {
523 pub enabled: bool,
524 pub expiration: Option<DateTime<Utc>>,
525}
526
527#[derive(Clone, Debug, Serialize, Deserialize)]
528pub struct Container {
529 pub container_arn: String,
530 pub name: String,
531 pub image: String,
532 pub task_arn: String,
533 pub last_status: String,
534 pub exit_code: Option<i64>,
535 pub reason: Option<String>,
536 pub runtime_id: Option<String>,
537 pub essential: bool,
538 pub cpu: Option<String>,
539 pub memory: Option<String>,
540 pub memory_reservation: Option<String>,
541 #[serde(default)]
542 pub network_bindings: Vec<Value>,
543 #[serde(default)]
544 pub network_interfaces: Vec<Value>,
545 pub health_status: Option<String>,
546 pub managed_agents: Option<Value>,
547 #[serde(default)]
551 pub image_digest: Option<String>,
552}
553
554#[derive(Clone, Debug, Serialize, Deserialize)]
555pub struct AwsLogsConfig {
556 pub group: String,
557 pub stream_prefix: Option<String>,
558 pub region: String,
559 pub container_name: String,
560}
561
562impl AwsLogsConfig {
563 pub fn stream_name(&self, task_id: &str) -> String {
564 match &self.stream_prefix {
565 Some(p) => format!("{}/{}/{}", p, self.container_name, task_id),
566 None => format!("{}/{}", self.container_name, task_id),
567 }
568 }
569}
570
571#[derive(Clone, Debug, Serialize, Deserialize)]
572pub struct LifecycleEvent {
573 pub at: DateTime<Utc>,
574 pub event_type: String,
575 pub task_arn: Option<String>,
576 pub cluster_arn: Option<String>,
577 pub last_status: Option<String>,
578 pub detail: Value,
579}
580
581#[derive(Clone, Debug, Serialize, Deserialize)]
582pub struct Service {
583 pub service_name: String,
584 pub service_arn: String,
585 pub cluster_name: String,
586 pub cluster_arn: String,
587 pub task_definition_arn: String,
588 pub family: String,
589 pub revision: i32,
590 pub desired_count: i32,
591 pub running_count: i32,
592 pub pending_count: i32,
593 pub launch_type: String,
594 pub status: String,
595 pub scheduling_strategy: String,
596 pub deployment_controller: String,
597 pub minimum_healthy_percent: Option<i32>,
598 pub maximum_percent: Option<i32>,
599 pub circuit_breaker: Option<CircuitBreakerConfig>,
601 #[serde(default)]
602 pub deployments: Vec<Deployment>,
603 #[serde(default)]
604 pub load_balancers: Vec<Value>,
605 #[serde(default)]
606 pub service_registries: Vec<Value>,
607 #[serde(default)]
608 pub placement_constraints: Vec<Value>,
609 #[serde(default)]
610 pub placement_strategy: Vec<Value>,
611 #[serde(default)]
612 pub network_configuration: Option<Value>,
613 #[serde(default)]
614 pub tags: Vec<TagEntry>,
615 pub created_at: DateTime<Utc>,
616 pub created_by: Option<String>,
617 pub role_arn: Option<String>,
618 #[serde(default)]
621 pub platform_version: Option<String>,
622 #[serde(default)]
625 pub health_check_grace_period_seconds: Option<i32>,
626 #[serde(default)]
628 pub enable_execute_command: bool,
629 #[serde(default)]
632 pub enable_ecs_managed_tags: bool,
633 #[serde(default)]
637 pub propagate_tags: Option<String>,
638 #[serde(default)]
642 pub capacity_provider_strategy: Vec<Value>,
643 #[serde(default)]
646 pub availability_zone_rebalancing: Option<String>,
647 #[serde(default)]
650 pub volume_configurations: Vec<Value>,
651 #[serde(default)]
655 pub service_connect_configuration: Option<Value>,
656}
657
658#[derive(Clone, Debug, Serialize, Deserialize)]
659pub struct CircuitBreakerConfig {
660 pub enable: bool,
661 pub rollback: bool,
662}
663
664#[derive(Clone, Debug, Serialize, Deserialize)]
665pub struct Deployment {
666 pub deployment_id: String,
667 pub status: String,
668 pub task_definition_arn: String,
669 pub desired_count: i32,
670 pub pending_count: i32,
671 pub running_count: i32,
672 pub failed_tasks: i32,
673 pub created_at: DateTime<Utc>,
674 pub updated_at: DateTime<Utc>,
675 pub launch_type: String,
676 pub rollout_state: String,
677 pub rollout_state_reason: Option<String>,
678 #[serde(default)]
682 pub lifecycle_hooks: Vec<Value>,
683 #[serde(default)]
687 pub pending_hook_id: Option<String>,
688 #[serde(default)]
692 pub lifecycle_stage: Option<String>,
693}
694
695#[derive(Clone, Debug, Serialize, Deserialize)]
696pub struct ContainerInstance {
697 pub container_instance_arn: String,
698 pub ec2_instance_id: Option<String>,
699 pub cluster_name: String,
700 pub cluster_arn: String,
701 pub status: String,
702 pub version: i64,
703 pub version_info: Option<Value>,
704 pub agent_connected: bool,
705 pub agent_update_status: Option<String>,
706 pub remaining_resources: Vec<Value>,
707 pub registered_resources: Vec<Value>,
708 pub running_tasks_count: i32,
709 pub pending_tasks_count: i32,
710 pub registered_at: DateTime<Utc>,
711 #[serde(default)]
712 pub attributes: Vec<AttributeRef>,
713 #[serde(default)]
714 pub tags: Vec<TagEntry>,
715 pub capacity_provider_name: Option<String>,
716 pub health_status: Option<Value>,
717}
718
719#[derive(Clone, Debug, Serialize, Deserialize)]
720pub struct AttributeRef {
721 pub name: String,
722 pub value: Option<String>,
723 pub target_type: Option<String>,
724 pub target_id: Option<String>,
725}
726
727#[derive(Clone, Debug, Serialize, Deserialize)]
728pub struct Attribute {
729 pub cluster_name: String,
730 pub target_type: String,
731 pub target_id: String,
732 pub name: String,
733 pub value: Option<String>,
734}
735
736#[derive(Clone, Debug, Serialize, Deserialize)]
737pub struct CapacityProvider {
738 pub name: String,
739 pub arn: String,
740 pub status: String,
741 pub auto_scaling_group_provider: Option<Value>,
742 pub update_status: Option<String>,
743 pub update_status_reason: Option<String>,
744 pub created_at: DateTime<Utc>,
745 #[serde(default)]
746 pub tags: Vec<TagEntry>,
747}
748
749#[derive(Clone, Debug, Serialize, Deserialize)]
750pub struct TaskSet {
751 pub task_set_id: String,
752 pub task_set_arn: String,
753 pub service_arn: String,
754 pub cluster_arn: String,
755 pub service_name: String,
756 pub cluster_name: String,
757 pub external_id: Option<String>,
758 pub status: String,
759 pub task_definition: String,
760 pub computed_desired_count: i32,
761 pub pending_count: i32,
762 pub running_count: i32,
763 pub launch_type: Option<String>,
764 pub platform_version: Option<String>,
765 pub scale: Option<Value>,
766 pub stability_status: String,
767 pub created_at: DateTime<Utc>,
768 pub updated_at: DateTime<Utc>,
769 #[serde(default)]
770 pub load_balancers: Vec<Value>,
771 #[serde(default)]
772 pub service_registries: Vec<Value>,
773 #[serde(default)]
774 pub capacity_provider_strategy: Vec<Value>,
775 #[serde(default)]
776 pub tags: Vec<TagEntry>,
777}
778
779#[derive(Clone, Debug, Serialize, Deserialize)]
783pub struct DaemonTaskDefinition {
784 pub family: String,
785 pub revision: i32,
786 pub task_definition_arn: String,
787 pub status: String,
788 pub container_definitions: Vec<Value>,
789 pub task_role_arn: Option<String>,
790 pub execution_role_arn: Option<String>,
791 pub cpu: Option<String>,
792 pub memory: Option<String>,
793 #[serde(default)]
794 pub volumes: Vec<Value>,
795 pub registered_at: DateTime<Utc>,
796 pub deregistered_at: Option<DateTime<Utc>>,
797 #[serde(default)]
798 pub tags: Vec<TagEntry>,
799}
800
801#[derive(Clone, Debug, Serialize, Deserialize)]
805pub struct Daemon {
806 pub daemon_name: String,
807 pub daemon_arn: String,
808 pub cluster_arn: String,
809 pub cluster_name: String,
810 pub daemon_task_definition_arn: String,
811 pub status: String,
812 pub deployment_arn: String,
813 pub created_at: DateTime<Utc>,
814 pub updated_at: DateTime<Utc>,
815 #[serde(default)]
816 pub capacity_provider_arns: Vec<String>,
817 pub deployment_configuration: Option<Value>,
818 pub propagate_tags: Option<String>,
819 pub enable_ecs_managed_tags: bool,
820 pub enable_execute_command: bool,
821 pub client_token: Option<String>,
822 #[serde(default)]
823 pub tags: Vec<TagEntry>,
824 #[serde(default)]
826 pub deployment_history: Vec<String>,
827 #[serde(default)]
829 pub task_arns: Vec<String>,
830}
831
832#[derive(Clone, Debug, Serialize, Deserialize)]
836pub struct DaemonDeployment {
837 pub deployment_arn: String,
838 pub daemon_arn: String,
839 pub daemon_name: String,
840 pub cluster_arn: String,
841 pub task_definition_arn: String,
842 pub status: String,
843 pub revision: i64,
844 pub created_at: DateTime<Utc>,
845 pub updated_at: DateTime<Utc>,
846}
847
848#[derive(Clone, Debug, Serialize, Deserialize)]
851pub struct ExpressGatewayService {
852 pub service_name: String,
853 pub service_arn: String,
854 pub cluster_arn: String,
855 pub cluster_name: String,
856 pub status: String,
857 pub execution_role_arn: String,
858 pub infrastructure_role_arn: String,
859 pub task_role_arn: Option<String>,
860 pub primary_container: Value,
861 pub network_configuration: Option<Value>,
862 pub health_check_path: Option<String>,
863 pub cpu: Option<String>,
864 pub memory: Option<String>,
865 pub scaling_target: Option<Value>,
866 pub created_at: DateTime<Utc>,
867 pub updated_at: DateTime<Utc>,
868 #[serde(default)]
869 pub tags: Vec<TagEntry>,
870}
871
872impl EcsState {
873 pub fn daemon_key(cluster: &str, name: &str) -> String {
875 format!("{}/{}", cluster, name)
876 }
877
878 pub fn express_gateway_key(cluster: &str, name: &str) -> String {
880 format!("{}/{}", cluster, name)
881 }
882
883 pub fn allocate_daemon_revision(&mut self, family: &str) -> i32 {
885 let entry = self
886 .next_daemon_revision
887 .entry(family.to_string())
888 .or_insert(0);
889 *entry += 1;
890 *entry
891 }
892
893 pub fn daemon_arn(&self, region: &str, cluster: &str, name: &str) -> String {
896 fakecloud_aws::arn::Arn::new(
897 "ecs",
898 region,
899 &self.account_id,
900 &format!("daemon/{}/{}", cluster, name),
901 )
902 .to_string()
903 }
904
905 pub fn express_gateway_arn(&self, region: &str, cluster: &str, name: &str) -> String {
907 fakecloud_aws::arn::Arn::new(
908 "ecs",
909 region,
910 &self.account_id,
911 &format!("express-gateway-service/{}/{}", cluster, name),
912 )
913 .to_string()
914 }
915
916 pub fn daemon_task_definition_arn(&self, region: &str, family: &str, revision: i32) -> String {
918 fakecloud_aws::arn::Arn::new(
919 "ecs",
920 region,
921 &self.account_id,
922 &format!("daemon-task-definition/{}:{}", family, revision),
923 )
924 .to_string()
925 }
926
927 pub fn daemon_deployment_arn(
929 &self,
930 region: &str,
931 daemon_name: &str,
932 deployment_id: &str,
933 ) -> String {
934 fakecloud_aws::arn::Arn::new(
935 "ecs",
936 region,
937 &self.account_id,
938 &format!("daemon-deployment/{}/{}", daemon_name, deployment_id),
939 )
940 .to_string()
941 }
942}
943
944#[cfg(test)]
945mod tests {
946 use super::*;
947
948 #[test]
949 fn resolve_cluster_name_defaults_to_default() {
950 assert_eq!(EcsState::resolve_cluster_name(None), "default");
951 assert_eq!(EcsState::resolve_cluster_name(Some("")), "default");
952 assert_eq!(EcsState::resolve_cluster_name(Some(" ")), "default");
953 }
954
955 #[test]
956 fn resolve_cluster_name_strips_arn_prefix() {
957 assert_eq!(
958 EcsState::resolve_cluster_name(Some("arn:aws:ecs:us-east-1:111122223333:cluster/prod")),
959 "prod"
960 );
961 }
962
963 #[test]
964 fn resolve_cluster_name_passes_through_name() {
965 assert_eq!(EcsState::resolve_cluster_name(Some("prod")), "prod");
966 }
967
968 #[test]
969 fn allocate_revision_monotonic() {
970 let mut s = EcsState::new("111122223333", "us-east-1");
971 assert_eq!(s.allocate_revision("web"), 1);
972 assert_eq!(s.allocate_revision("web"), 2);
973 assert_eq!(s.allocate_revision("worker"), 1);
974 assert_eq!(s.allocate_revision("web"), 3);
975 }
976
977 #[test]
978 fn cluster_arn_format() {
979 let s = EcsState::new("111122223333", "us-east-1");
980 assert_eq!(
981 s.cluster_arn("us-east-1", "prod"),
982 "arn:aws:ecs:us-east-1:111122223333:cluster/prod"
983 );
984 }
985
986 #[test]
987 fn task_definition_arn_format() {
988 let s = EcsState::new("111122223333", "us-east-1");
989 assert_eq!(
990 s.task_definition_arn("us-east-1", "web", 3),
991 "arn:aws:ecs:us-east-1:111122223333:task-definition/web:3"
992 );
993 }
994
995 #[test]
996 fn task_arn_long_format_default() {
997 let s = EcsState::new("111122223333", "us-east-1");
998 assert_eq!(
999 s.task_arn("us-east-1", "prod", "abc123"),
1000 "arn:aws:ecs:us-east-1:111122223333:task/prod/abc123"
1001 );
1002 }
1003
1004 #[test]
1005 fn task_arn_short_when_disabled() {
1006 let mut s = EcsState::new("111122223333", "us-east-1");
1007 s.account_setting_defaults
1008 .insert("taskLongArnFormat".into(), "disabled".into());
1009 assert_eq!(
1010 s.task_arn("us-east-1", "prod", "abc123"),
1011 "arn:aws:ecs:us-east-1:111122223333:task/abc123"
1012 );
1013 }
1014
1015 #[test]
1016 fn service_arn_short_when_disabled() {
1017 let mut s = EcsState::new("111122223333", "us-east-1");
1018 s.account_setting_defaults
1019 .insert("serviceLongArnFormat".into(), "disabled".into());
1020 assert_eq!(
1021 s.service_arn("us-east-1", "prod", "web"),
1022 "arn:aws:ecs:us-east-1:111122223333:service/web"
1023 );
1024 }
1025
1026 #[test]
1027 fn container_instance_arn_short_when_disabled() {
1028 let mut s = EcsState::new("111122223333", "us-east-1");
1029 s.account_setting_defaults
1030 .insert("containerInstanceLongArnFormat".into(), "disabled".into());
1031 assert_eq!(
1032 s.container_instance_arn("us-east-1", "prod", "i-abc"),
1033 "arn:aws:ecs:us-east-1:111122223333:container-instance/i-abc"
1034 );
1035 }
1036
1037 #[test]
1038 fn principal_setting_overrides_default() {
1039 let mut s = EcsState::new("111122223333", "us-east-1");
1040 s.account_setting_defaults
1041 .insert("taskLongArnFormat".into(), "disabled".into());
1042 let principal = "arn:aws:iam::111122223333:user/alice".to_string();
1043 let mut p = BTreeMap::new();
1044 p.insert("taskLongArnFormat".into(), "enabled".into());
1045 s.principal_account_settings.insert(principal.clone(), p);
1046 assert_eq!(
1047 s.effective_account_setting("taskLongArnFormat", Some(&principal))
1048 .as_deref(),
1049 Some("enabled")
1050 );
1051 assert_eq!(
1053 s.effective_account_setting("taskLongArnFormat", None)
1054 .as_deref(),
1055 Some("disabled")
1056 );
1057 }
1058
1059 #[test]
1060 fn reset_clears_all() {
1061 let mut s = EcsState::new("111122223333", "us-east-1");
1062 s.clusters.insert(
1063 "prod".to_string(),
1064 Cluster::new("prod", s.cluster_arn("us-east-1", "prod")),
1065 );
1066 s.allocate_revision("web");
1067 s.account_setting_defaults
1068 .insert("serviceLongArnFormat".into(), "enabled".into());
1069 s.reset();
1070 assert!(s.clusters.is_empty());
1071 assert!(s.next_revision.is_empty());
1072 assert!(s.account_setting_defaults.is_empty());
1073 }
1074}