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}
98
99impl EcsState {
100 pub fn new(account_id: &str, region: &str) -> Self {
101 Self {
102 account_id: account_id.to_string(),
103 region: region.to_string(),
104 clusters: BTreeMap::new(),
105 task_definitions: BTreeMap::new(),
106 next_revision: BTreeMap::new(),
107 account_setting_defaults: BTreeMap::new(),
108 principal_account_settings: BTreeMap::new(),
109 tasks: BTreeMap::new(),
110 events: Vec::new(),
111 services: BTreeMap::new(),
112 container_instances: BTreeMap::new(),
113 attributes: BTreeMap::new(),
114 capacity_providers: BTreeMap::new(),
115 task_sets: BTreeMap::new(),
116 daemon_task_definitions: BTreeMap::new(),
117 next_daemon_revision: BTreeMap::new(),
118 daemons: BTreeMap::new(),
119 daemon_deployments: BTreeMap::new(),
120 express_gateway_services: BTreeMap::new(),
121 }
122 }
123
124 pub fn reset(&mut self) {
125 self.clusters.clear();
126 self.task_definitions.clear();
127 self.next_revision.clear();
128 self.account_setting_defaults.clear();
129 self.principal_account_settings.clear();
130 self.tasks.clear();
131 self.events.clear();
132 self.services.clear();
133 self.container_instances.clear();
134 self.attributes.clear();
135 self.capacity_providers.clear();
136 self.task_sets.clear();
137 self.daemon_task_definitions.clear();
138 self.next_daemon_revision.clear();
139 self.daemons.clear();
140 self.daemon_deployments.clear();
141 self.express_gateway_services.clear();
142 }
143
144 pub fn service_key(cluster_name: &str, service_name: &str) -> String {
148 format!("{}/{}", cluster_name, service_name)
149 }
150
151 pub fn service_arn(&self, cluster_name: &str, service_name: &str) -> String {
152 if self.arn_format_disabled("serviceLongArnFormat") {
153 format!(
155 "arn:aws:ecs:{}:{}:service/{}",
156 self.region, self.account_id, service_name
157 )
158 } else {
159 format!(
160 "arn:aws:ecs:{}:{}:service/{}/{}",
161 self.region, self.account_id, cluster_name, service_name
162 )
163 }
164 }
165
166 pub fn task_arn(&self, cluster_name: &str, task_id: &str) -> String {
167 if self.arn_format_disabled("taskLongArnFormat") {
168 format!(
169 "arn:aws:ecs:{}:{}:task/{}",
170 self.region, self.account_id, task_id
171 )
172 } else {
173 format!(
174 "arn:aws:ecs:{}:{}:task/{}/{}",
175 self.region, self.account_id, cluster_name, task_id
176 )
177 }
178 }
179
180 pub fn container_instance_arn(&self, cluster_name: &str, instance_id: &str) -> String {
181 if self.arn_format_disabled("containerInstanceLongArnFormat") {
182 format!(
183 "arn:aws:ecs:{}:{}:container-instance/{}",
184 self.region, self.account_id, instance_id
185 )
186 } else {
187 format!(
188 "arn:aws:ecs:{}:{}:container-instance/{}/{}",
189 self.region, self.account_id, cluster_name, instance_id
190 )
191 }
192 }
193
194 pub fn effective_account_setting(
199 &self,
200 name: &str,
201 principal_arn: Option<&str>,
202 ) -> Option<String> {
203 if let Some(arn) = principal_arn {
204 if let Some(p) = self.principal_account_settings.get(arn) {
205 if let Some(v) = p.get(name) {
206 return Some(v.clone());
207 }
208 }
209 }
210 self.account_setting_defaults.get(name).cloned()
211 }
212
213 fn arn_format_disabled(&self, setting_name: &str) -> bool {
218 matches!(
219 self.effective_account_setting(setting_name, None)
220 .as_deref(),
221 Some("disabled")
222 )
223 }
224
225 pub fn push_event(&mut self, event: LifecycleEvent) {
227 const MAX_EVENTS: usize = 1024;
228 if self.events.len() >= MAX_EVENTS {
229 self.events.drain(0..self.events.len() - MAX_EVENTS + 1);
230 }
231 self.events.push(event);
232 }
233
234 pub fn cluster_arn(&self, cluster_name: &str) -> String {
235 format!(
236 "arn:aws:ecs:{}:{}:cluster/{}",
237 self.region, self.account_id, cluster_name
238 )
239 }
240
241 pub fn task_definition_arn(&self, family: &str, revision: i32) -> String {
242 format!(
243 "arn:aws:ecs:{}:{}:task-definition/{}:{}",
244 self.region, self.account_id, family, revision
245 )
246 }
247
248 pub fn resolve_cluster_name(input: Option<&str>) -> String {
252 let raw = input.unwrap_or("").trim();
253 if raw.is_empty() {
254 return "default".to_string();
255 }
256 if let Some(name) = raw.rsplit_once('/').map(|(_, n)| n) {
257 return name.to_string();
258 }
259 raw.to_string()
260 }
261
262 pub fn allocate_revision(&mut self, family: &str) -> i32 {
265 let next = self.next_revision.entry(family.to_string()).or_insert(0);
266 *next += 1;
267 *next
268 }
269}
270
271#[derive(Clone, Debug, Serialize, Deserialize)]
272pub struct Cluster {
273 pub cluster_name: String,
274 pub cluster_arn: String,
275 pub status: String,
276 pub registered_container_instances_count: i32,
277 pub running_tasks_count: i32,
278 pub pending_tasks_count: i32,
279 pub active_services_count: i32,
280 #[serde(default)]
281 pub statistics: Vec<Value>,
282 #[serde(default)]
283 pub tags: Vec<TagEntry>,
284 #[serde(default)]
285 pub settings: Vec<Value>,
286 pub configuration: Option<Value>,
287 #[serde(default)]
288 pub capacity_providers: Vec<String>,
289 #[serde(default)]
290 pub default_capacity_provider_strategy: Vec<Value>,
291 #[serde(default)]
292 pub attachments: Vec<Value>,
293 pub attachments_status: Option<String>,
294 pub service_connect_defaults: Option<Value>,
295 pub created_at: DateTime<Utc>,
296}
297
298impl Cluster {
299 pub fn new(cluster_name: &str, cluster_arn: String) -> Self {
300 Self {
301 cluster_name: cluster_name.to_string(),
302 cluster_arn,
303 status: "ACTIVE".to_string(),
304 registered_container_instances_count: 0,
305 running_tasks_count: 0,
306 pending_tasks_count: 0,
307 active_services_count: 0,
308 statistics: Vec::new(),
309 tags: Vec::new(),
310 settings: Vec::new(),
311 configuration: None,
312 capacity_providers: Vec::new(),
313 default_capacity_provider_strategy: Vec::new(),
314 attachments: Vec::new(),
315 attachments_status: None,
316 service_connect_defaults: None,
317 created_at: Utc::now(),
318 }
319 }
320}
321
322#[derive(Clone, Debug, Serialize, Deserialize)]
323pub struct TagEntry {
324 pub key: String,
325 pub value: String,
326}
327
328#[derive(Clone, Debug, Serialize, Deserialize)]
329pub struct TaskDefinition {
330 pub family: String,
331 pub revision: i32,
332 pub task_definition_arn: String,
333 #[serde(default)]
338 pub container_definitions: Vec<Value>,
339 pub status: String,
340 pub task_role_arn: Option<String>,
341 pub execution_role_arn: Option<String>,
342 pub network_mode: Option<String>,
343 #[serde(default)]
344 pub requires_compatibilities: Vec<String>,
345 #[serde(default)]
346 pub compatibilities: Vec<String>,
347 pub cpu: Option<String>,
348 pub memory: Option<String>,
349 pub pid_mode: Option<String>,
350 pub ipc_mode: Option<String>,
351 #[serde(default)]
352 pub volumes: Vec<Value>,
353 #[serde(default)]
354 pub placement_constraints: Vec<Value>,
355 pub proxy_configuration: Option<Value>,
356 #[serde(default)]
357 pub inference_accelerators: Vec<Value>,
358 pub ephemeral_storage: Option<Value>,
359 pub runtime_platform: Option<Value>,
360 #[serde(default)]
361 pub requires_attributes: Vec<Value>,
362 pub registered_at: DateTime<Utc>,
363 pub registered_by: Option<String>,
364 pub deregistered_at: Option<DateTime<Utc>>,
365 #[serde(default)]
366 pub tags: Vec<TagEntry>,
367 pub enable_fault_injection: Option<bool>,
368}
369
370#[derive(Clone, Debug, Serialize, Deserialize)]
371pub struct Task {
372 pub task_arn: String,
373 pub task_id: String,
374 pub cluster_arn: String,
375 pub cluster_name: String,
376 pub task_definition_arn: String,
377 pub family: String,
378 pub revision: i32,
379 #[serde(default)]
382 pub container_instance_arn: Option<String>,
383 #[serde(default)]
388 pub capacity_provider_name: Option<String>,
389 pub last_status: String,
392 pub desired_status: String,
395 pub launch_type: String,
396 pub platform_version: Option<String>,
397 pub cpu: Option<String>,
398 pub memory: Option<String>,
399 #[serde(default)]
400 pub containers: Vec<Container>,
401 #[serde(default)]
402 pub overrides: Value,
403 pub started_by: Option<String>,
404 pub group: Option<String>,
405 pub connectivity: String,
406 pub stop_code: Option<String>,
407 pub stopped_reason: Option<String>,
408 pub created_at: DateTime<Utc>,
409 pub started_at: Option<DateTime<Utc>>,
410 pub stopping_at: Option<DateTime<Utc>>,
411 pub stopped_at: Option<DateTime<Utc>>,
412 pub pull_started_at: Option<DateTime<Utc>>,
413 pub pull_stopped_at: Option<DateTime<Utc>>,
414 pub connectivity_at: Option<DateTime<Utc>>,
415 pub started_by_ref_id: Option<String>,
416 pub execution_role_arn: Option<String>,
417 pub task_role_arn: Option<String>,
418 #[serde(default)]
419 pub tags: Vec<TagEntry>,
420 pub awslogs: Option<AwsLogsConfig>,
424 #[serde(default)]
428 pub captured_logs: String,
429 pub protection: Option<TaskProtection>,
432 #[serde(default)]
436 pub enable_execute_command: bool,
437 #[serde(default)]
440 pub attachments: Vec<TaskAttachment>,
441 #[serde(default)]
444 pub volume_configurations: Vec<Value>,
445 #[serde(default)]
448 pub task_set_arn: Option<String>,
449}
450
451#[derive(Clone, Debug, Serialize, Deserialize)]
452pub struct TaskAttachment {
453 pub id: String,
454 #[serde(rename = "type")]
455 pub attachment_type: String,
456 pub status: String,
457 #[serde(default)]
458 pub details: Vec<AttachmentDetail>,
459}
460
461#[derive(Clone, Debug, Serialize, Deserialize)]
462pub struct AttachmentDetail {
463 pub name: String,
464 pub value: String,
465}
466
467#[derive(Clone, Debug, Serialize, Deserialize)]
468pub struct TaskProtection {
469 pub enabled: bool,
470 pub expiration: Option<DateTime<Utc>>,
471}
472
473#[derive(Clone, Debug, Serialize, Deserialize)]
474pub struct Container {
475 pub container_arn: String,
476 pub name: String,
477 pub image: String,
478 pub task_arn: String,
479 pub last_status: String,
480 pub exit_code: Option<i64>,
481 pub reason: Option<String>,
482 pub runtime_id: Option<String>,
483 pub essential: bool,
484 pub cpu: Option<String>,
485 pub memory: Option<String>,
486 pub memory_reservation: Option<String>,
487 #[serde(default)]
488 pub network_bindings: Vec<Value>,
489 #[serde(default)]
490 pub network_interfaces: Vec<Value>,
491 pub health_status: Option<String>,
492 pub managed_agents: Option<Value>,
493 #[serde(default)]
497 pub image_digest: Option<String>,
498}
499
500#[derive(Clone, Debug, Serialize, Deserialize)]
501pub struct AwsLogsConfig {
502 pub group: String,
503 pub stream_prefix: Option<String>,
504 pub region: String,
505 pub container_name: String,
506}
507
508impl AwsLogsConfig {
509 pub fn stream_name(&self, task_id: &str) -> String {
510 match &self.stream_prefix {
511 Some(p) => format!("{}/{}/{}", p, self.container_name, task_id),
512 None => format!("{}/{}", self.container_name, task_id),
513 }
514 }
515}
516
517#[derive(Clone, Debug, Serialize, Deserialize)]
518pub struct LifecycleEvent {
519 pub at: DateTime<Utc>,
520 pub event_type: String,
521 pub task_arn: Option<String>,
522 pub cluster_arn: Option<String>,
523 pub last_status: Option<String>,
524 pub detail: Value,
525}
526
527#[derive(Clone, Debug, Serialize, Deserialize)]
528pub struct Service {
529 pub service_name: String,
530 pub service_arn: String,
531 pub cluster_name: String,
532 pub cluster_arn: String,
533 pub task_definition_arn: String,
534 pub family: String,
535 pub revision: i32,
536 pub desired_count: i32,
537 pub running_count: i32,
538 pub pending_count: i32,
539 pub launch_type: String,
540 pub status: String,
541 pub scheduling_strategy: String,
542 pub deployment_controller: String,
543 pub minimum_healthy_percent: Option<i32>,
544 pub maximum_percent: Option<i32>,
545 pub circuit_breaker: Option<CircuitBreakerConfig>,
547 #[serde(default)]
548 pub deployments: Vec<Deployment>,
549 #[serde(default)]
550 pub load_balancers: Vec<Value>,
551 #[serde(default)]
552 pub service_registries: Vec<Value>,
553 #[serde(default)]
554 pub placement_constraints: Vec<Value>,
555 #[serde(default)]
556 pub placement_strategy: Vec<Value>,
557 #[serde(default)]
558 pub network_configuration: Option<Value>,
559 #[serde(default)]
560 pub tags: Vec<TagEntry>,
561 pub created_at: DateTime<Utc>,
562 pub created_by: Option<String>,
563 pub role_arn: Option<String>,
564 #[serde(default)]
567 pub platform_version: Option<String>,
568 #[serde(default)]
571 pub health_check_grace_period_seconds: Option<i32>,
572 #[serde(default)]
574 pub enable_execute_command: bool,
575 #[serde(default)]
578 pub enable_ecs_managed_tags: bool,
579 #[serde(default)]
583 pub propagate_tags: Option<String>,
584 #[serde(default)]
588 pub capacity_provider_strategy: Vec<Value>,
589 #[serde(default)]
592 pub availability_zone_rebalancing: Option<String>,
593 #[serde(default)]
596 pub volume_configurations: Vec<Value>,
597}
598
599#[derive(Clone, Debug, Serialize, Deserialize)]
600pub struct CircuitBreakerConfig {
601 pub enable: bool,
602 pub rollback: bool,
603}
604
605#[derive(Clone, Debug, Serialize, Deserialize)]
606pub struct Deployment {
607 pub deployment_id: String,
608 pub status: String,
609 pub task_definition_arn: String,
610 pub desired_count: i32,
611 pub pending_count: i32,
612 pub running_count: i32,
613 pub failed_tasks: i32,
614 pub created_at: DateTime<Utc>,
615 pub updated_at: DateTime<Utc>,
616 pub launch_type: String,
617 pub rollout_state: String,
618 pub rollout_state_reason: Option<String>,
619 #[serde(default)]
623 pub lifecycle_hooks: Vec<Value>,
624 #[serde(default)]
628 pub pending_hook_id: Option<String>,
629 #[serde(default)]
633 pub lifecycle_stage: Option<String>,
634}
635
636#[derive(Clone, Debug, Serialize, Deserialize)]
637pub struct ContainerInstance {
638 pub container_instance_arn: String,
639 pub ec2_instance_id: Option<String>,
640 pub cluster_name: String,
641 pub cluster_arn: String,
642 pub status: String,
643 pub version: i64,
644 pub version_info: Option<Value>,
645 pub agent_connected: bool,
646 pub agent_update_status: Option<String>,
647 pub remaining_resources: Vec<Value>,
648 pub registered_resources: Vec<Value>,
649 pub running_tasks_count: i32,
650 pub pending_tasks_count: i32,
651 pub registered_at: DateTime<Utc>,
652 #[serde(default)]
653 pub attributes: Vec<AttributeRef>,
654 #[serde(default)]
655 pub tags: Vec<TagEntry>,
656 pub capacity_provider_name: Option<String>,
657 pub health_status: Option<Value>,
658}
659
660#[derive(Clone, Debug, Serialize, Deserialize)]
661pub struct AttributeRef {
662 pub name: String,
663 pub value: Option<String>,
664 pub target_type: Option<String>,
665 pub target_id: Option<String>,
666}
667
668#[derive(Clone, Debug, Serialize, Deserialize)]
669pub struct Attribute {
670 pub cluster_name: String,
671 pub target_type: String,
672 pub target_id: String,
673 pub name: String,
674 pub value: Option<String>,
675}
676
677#[derive(Clone, Debug, Serialize, Deserialize)]
678pub struct CapacityProvider {
679 pub name: String,
680 pub arn: String,
681 pub status: String,
682 pub auto_scaling_group_provider: Option<Value>,
683 pub update_status: Option<String>,
684 pub update_status_reason: Option<String>,
685 pub created_at: DateTime<Utc>,
686 #[serde(default)]
687 pub tags: Vec<TagEntry>,
688}
689
690#[derive(Clone, Debug, Serialize, Deserialize)]
691pub struct TaskSet {
692 pub task_set_id: String,
693 pub task_set_arn: String,
694 pub service_arn: String,
695 pub cluster_arn: String,
696 pub service_name: String,
697 pub cluster_name: String,
698 pub external_id: Option<String>,
699 pub status: String,
700 pub task_definition: String,
701 pub computed_desired_count: i32,
702 pub pending_count: i32,
703 pub running_count: i32,
704 pub launch_type: Option<String>,
705 pub platform_version: Option<String>,
706 pub scale: Option<Value>,
707 pub stability_status: String,
708 pub created_at: DateTime<Utc>,
709 pub updated_at: DateTime<Utc>,
710 #[serde(default)]
711 pub load_balancers: Vec<Value>,
712 #[serde(default)]
713 pub service_registries: Vec<Value>,
714 #[serde(default)]
715 pub capacity_provider_strategy: Vec<Value>,
716 #[serde(default)]
717 pub tags: Vec<TagEntry>,
718}
719
720#[derive(Clone, Debug, Serialize, Deserialize)]
724pub struct DaemonTaskDefinition {
725 pub family: String,
726 pub revision: i32,
727 pub task_definition_arn: String,
728 pub status: String,
729 pub container_definitions: Vec<Value>,
730 pub task_role_arn: Option<String>,
731 pub execution_role_arn: Option<String>,
732 pub cpu: Option<String>,
733 pub memory: Option<String>,
734 #[serde(default)]
735 pub volumes: Vec<Value>,
736 pub registered_at: DateTime<Utc>,
737 pub deregistered_at: Option<DateTime<Utc>>,
738 #[serde(default)]
739 pub tags: Vec<TagEntry>,
740}
741
742#[derive(Clone, Debug, Serialize, Deserialize)]
746pub struct Daemon {
747 pub daemon_name: String,
748 pub daemon_arn: String,
749 pub cluster_arn: String,
750 pub cluster_name: String,
751 pub daemon_task_definition_arn: String,
752 pub status: String,
753 pub deployment_arn: String,
754 pub created_at: DateTime<Utc>,
755 pub updated_at: DateTime<Utc>,
756 #[serde(default)]
757 pub capacity_provider_arns: Vec<String>,
758 pub deployment_configuration: Option<Value>,
759 pub propagate_tags: Option<String>,
760 pub enable_ecs_managed_tags: bool,
761 pub enable_execute_command: bool,
762 pub client_token: Option<String>,
763 #[serde(default)]
764 pub tags: Vec<TagEntry>,
765 #[serde(default)]
767 pub deployment_history: Vec<String>,
768 #[serde(default)]
770 pub task_arns: Vec<String>,
771}
772
773#[derive(Clone, Debug, Serialize, Deserialize)]
777pub struct DaemonDeployment {
778 pub deployment_arn: String,
779 pub daemon_arn: String,
780 pub daemon_name: String,
781 pub cluster_arn: String,
782 pub task_definition_arn: String,
783 pub status: String,
784 pub revision: i64,
785 pub created_at: DateTime<Utc>,
786 pub updated_at: DateTime<Utc>,
787}
788
789#[derive(Clone, Debug, Serialize, Deserialize)]
792pub struct ExpressGatewayService {
793 pub service_name: String,
794 pub service_arn: String,
795 pub cluster_arn: String,
796 pub cluster_name: String,
797 pub status: String,
798 pub execution_role_arn: String,
799 pub infrastructure_role_arn: String,
800 pub task_role_arn: Option<String>,
801 pub primary_container: Value,
802 pub network_configuration: Option<Value>,
803 pub health_check_path: Option<String>,
804 pub cpu: Option<String>,
805 pub memory: Option<String>,
806 pub scaling_target: Option<Value>,
807 pub created_at: DateTime<Utc>,
808 pub updated_at: DateTime<Utc>,
809 #[serde(default)]
810 pub tags: Vec<TagEntry>,
811}
812
813impl EcsState {
814 pub fn daemon_key(cluster: &str, name: &str) -> String {
816 format!("{}/{}", cluster, name)
817 }
818
819 pub fn express_gateway_key(cluster: &str, name: &str) -> String {
821 format!("{}/{}", cluster, name)
822 }
823
824 pub fn allocate_daemon_revision(&mut self, family: &str) -> i32 {
826 let entry = self
827 .next_daemon_revision
828 .entry(family.to_string())
829 .or_insert(0);
830 *entry += 1;
831 *entry
832 }
833
834 pub fn daemon_arn(&self, cluster: &str, name: &str) -> String {
836 fakecloud_aws::arn::Arn::new(
837 "ecs",
838 &self.region,
839 &self.account_id,
840 &format!("daemon/{}/{}", cluster, name),
841 )
842 .to_string()
843 }
844
845 pub fn express_gateway_arn(&self, cluster: &str, name: &str) -> String {
847 fakecloud_aws::arn::Arn::new(
848 "ecs",
849 &self.region,
850 &self.account_id,
851 &format!("express-gateway-service/{}/{}", cluster, name),
852 )
853 .to_string()
854 }
855
856 pub fn daemon_task_definition_arn(&self, family: &str, revision: i32) -> String {
858 fakecloud_aws::arn::Arn::new(
859 "ecs",
860 &self.region,
861 &self.account_id,
862 &format!("daemon-task-definition/{}:{}", family, revision),
863 )
864 .to_string()
865 }
866
867 pub fn daemon_deployment_arn(&self, daemon_name: &str, deployment_id: &str) -> String {
869 fakecloud_aws::arn::Arn::new(
870 "ecs",
871 &self.region,
872 &self.account_id,
873 &format!("daemon-deployment/{}/{}", daemon_name, deployment_id),
874 )
875 .to_string()
876 }
877}
878
879#[cfg(test)]
880mod tests {
881 use super::*;
882
883 #[test]
884 fn resolve_cluster_name_defaults_to_default() {
885 assert_eq!(EcsState::resolve_cluster_name(None), "default");
886 assert_eq!(EcsState::resolve_cluster_name(Some("")), "default");
887 assert_eq!(EcsState::resolve_cluster_name(Some(" ")), "default");
888 }
889
890 #[test]
891 fn resolve_cluster_name_strips_arn_prefix() {
892 assert_eq!(
893 EcsState::resolve_cluster_name(Some("arn:aws:ecs:us-east-1:111122223333:cluster/prod")),
894 "prod"
895 );
896 }
897
898 #[test]
899 fn resolve_cluster_name_passes_through_name() {
900 assert_eq!(EcsState::resolve_cluster_name(Some("prod")), "prod");
901 }
902
903 #[test]
904 fn allocate_revision_monotonic() {
905 let mut s = EcsState::new("111122223333", "us-east-1");
906 assert_eq!(s.allocate_revision("web"), 1);
907 assert_eq!(s.allocate_revision("web"), 2);
908 assert_eq!(s.allocate_revision("worker"), 1);
909 assert_eq!(s.allocate_revision("web"), 3);
910 }
911
912 #[test]
913 fn cluster_arn_format() {
914 let s = EcsState::new("111122223333", "us-east-1");
915 assert_eq!(
916 s.cluster_arn("prod"),
917 "arn:aws:ecs:us-east-1:111122223333:cluster/prod"
918 );
919 }
920
921 #[test]
922 fn task_definition_arn_format() {
923 let s = EcsState::new("111122223333", "us-east-1");
924 assert_eq!(
925 s.task_definition_arn("web", 3),
926 "arn:aws:ecs:us-east-1:111122223333:task-definition/web:3"
927 );
928 }
929
930 #[test]
931 fn task_arn_long_format_default() {
932 let s = EcsState::new("111122223333", "us-east-1");
933 assert_eq!(
934 s.task_arn("prod", "abc123"),
935 "arn:aws:ecs:us-east-1:111122223333:task/prod/abc123"
936 );
937 }
938
939 #[test]
940 fn task_arn_short_when_disabled() {
941 let mut s = EcsState::new("111122223333", "us-east-1");
942 s.account_setting_defaults
943 .insert("taskLongArnFormat".into(), "disabled".into());
944 assert_eq!(
945 s.task_arn("prod", "abc123"),
946 "arn:aws:ecs:us-east-1:111122223333:task/abc123"
947 );
948 }
949
950 #[test]
951 fn service_arn_short_when_disabled() {
952 let mut s = EcsState::new("111122223333", "us-east-1");
953 s.account_setting_defaults
954 .insert("serviceLongArnFormat".into(), "disabled".into());
955 assert_eq!(
956 s.service_arn("prod", "web"),
957 "arn:aws:ecs:us-east-1:111122223333:service/web"
958 );
959 }
960
961 #[test]
962 fn container_instance_arn_short_when_disabled() {
963 let mut s = EcsState::new("111122223333", "us-east-1");
964 s.account_setting_defaults
965 .insert("containerInstanceLongArnFormat".into(), "disabled".into());
966 assert_eq!(
967 s.container_instance_arn("prod", "i-abc"),
968 "arn:aws:ecs:us-east-1:111122223333:container-instance/i-abc"
969 );
970 }
971
972 #[test]
973 fn principal_setting_overrides_default() {
974 let mut s = EcsState::new("111122223333", "us-east-1");
975 s.account_setting_defaults
976 .insert("taskLongArnFormat".into(), "disabled".into());
977 let principal = "arn:aws:iam::111122223333:user/alice".to_string();
978 let mut p = BTreeMap::new();
979 p.insert("taskLongArnFormat".into(), "enabled".into());
980 s.principal_account_settings.insert(principal.clone(), p);
981 assert_eq!(
982 s.effective_account_setting("taskLongArnFormat", Some(&principal))
983 .as_deref(),
984 Some("enabled")
985 );
986 assert_eq!(
988 s.effective_account_setting("taskLongArnFormat", None)
989 .as_deref(),
990 Some("disabled")
991 );
992 }
993
994 #[test]
995 fn reset_clears_all() {
996 let mut s = EcsState::new("111122223333", "us-east-1");
997 s.clusters.insert(
998 "prod".to_string(),
999 Cluster::new("prod", s.cluster_arn("prod")),
1000 );
1001 s.allocate_revision("web");
1002 s.account_setting_defaults
1003 .insert("serviceLongArnFormat".into(), "enabled".into());
1004 s.reset();
1005 assert!(s.clusters.is_empty());
1006 assert!(s.next_revision.is_empty());
1007 assert!(s.account_setting_defaults.is_empty());
1008 }
1009}