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}
620
621#[derive(Clone, Debug, Serialize, Deserialize)]
622pub struct ContainerInstance {
623 pub container_instance_arn: String,
624 pub ec2_instance_id: Option<String>,
625 pub cluster_name: String,
626 pub cluster_arn: String,
627 pub status: String,
628 pub version: i64,
629 pub version_info: Option<Value>,
630 pub agent_connected: bool,
631 pub agent_update_status: Option<String>,
632 pub remaining_resources: Vec<Value>,
633 pub registered_resources: Vec<Value>,
634 pub running_tasks_count: i32,
635 pub pending_tasks_count: i32,
636 pub registered_at: DateTime<Utc>,
637 #[serde(default)]
638 pub attributes: Vec<AttributeRef>,
639 #[serde(default)]
640 pub tags: Vec<TagEntry>,
641 pub capacity_provider_name: Option<String>,
642 pub health_status: Option<Value>,
643}
644
645#[derive(Clone, Debug, Serialize, Deserialize)]
646pub struct AttributeRef {
647 pub name: String,
648 pub value: Option<String>,
649 pub target_type: Option<String>,
650 pub target_id: Option<String>,
651}
652
653#[derive(Clone, Debug, Serialize, Deserialize)]
654pub struct Attribute {
655 pub cluster_name: String,
656 pub target_type: String,
657 pub target_id: String,
658 pub name: String,
659 pub value: Option<String>,
660}
661
662#[derive(Clone, Debug, Serialize, Deserialize)]
663pub struct CapacityProvider {
664 pub name: String,
665 pub arn: String,
666 pub status: String,
667 pub auto_scaling_group_provider: Option<Value>,
668 pub update_status: Option<String>,
669 pub update_status_reason: Option<String>,
670 pub created_at: DateTime<Utc>,
671 #[serde(default)]
672 pub tags: Vec<TagEntry>,
673}
674
675#[derive(Clone, Debug, Serialize, Deserialize)]
676pub struct TaskSet {
677 pub task_set_id: String,
678 pub task_set_arn: String,
679 pub service_arn: String,
680 pub cluster_arn: String,
681 pub service_name: String,
682 pub cluster_name: String,
683 pub external_id: Option<String>,
684 pub status: String,
685 pub task_definition: String,
686 pub computed_desired_count: i32,
687 pub pending_count: i32,
688 pub running_count: i32,
689 pub launch_type: Option<String>,
690 pub platform_version: Option<String>,
691 pub scale: Option<Value>,
692 pub stability_status: String,
693 pub created_at: DateTime<Utc>,
694 pub updated_at: DateTime<Utc>,
695 #[serde(default)]
696 pub load_balancers: Vec<Value>,
697 #[serde(default)]
698 pub service_registries: Vec<Value>,
699 #[serde(default)]
700 pub capacity_provider_strategy: Vec<Value>,
701 #[serde(default)]
702 pub tags: Vec<TagEntry>,
703}
704
705#[derive(Clone, Debug, Serialize, Deserialize)]
709pub struct DaemonTaskDefinition {
710 pub family: String,
711 pub revision: i32,
712 pub task_definition_arn: String,
713 pub status: String,
714 pub container_definitions: Vec<Value>,
715 pub task_role_arn: Option<String>,
716 pub execution_role_arn: Option<String>,
717 pub cpu: Option<String>,
718 pub memory: Option<String>,
719 #[serde(default)]
720 pub volumes: Vec<Value>,
721 pub registered_at: DateTime<Utc>,
722 pub deregistered_at: Option<DateTime<Utc>>,
723 #[serde(default)]
724 pub tags: Vec<TagEntry>,
725}
726
727#[derive(Clone, Debug, Serialize, Deserialize)]
731pub struct Daemon {
732 pub daemon_name: String,
733 pub daemon_arn: String,
734 pub cluster_arn: String,
735 pub cluster_name: String,
736 pub daemon_task_definition_arn: String,
737 pub status: String,
738 pub deployment_arn: String,
739 pub created_at: DateTime<Utc>,
740 pub updated_at: DateTime<Utc>,
741 #[serde(default)]
742 pub capacity_provider_arns: Vec<String>,
743 pub deployment_configuration: Option<Value>,
744 pub propagate_tags: Option<String>,
745 pub enable_ecs_managed_tags: bool,
746 pub enable_execute_command: bool,
747 pub client_token: Option<String>,
748 #[serde(default)]
749 pub tags: Vec<TagEntry>,
750 #[serde(default)]
752 pub deployment_history: Vec<String>,
753 #[serde(default)]
755 pub task_arns: Vec<String>,
756}
757
758#[derive(Clone, Debug, Serialize, Deserialize)]
762pub struct DaemonDeployment {
763 pub deployment_arn: String,
764 pub daemon_arn: String,
765 pub daemon_name: String,
766 pub cluster_arn: String,
767 pub task_definition_arn: String,
768 pub status: String,
769 pub revision: i64,
770 pub created_at: DateTime<Utc>,
771 pub updated_at: DateTime<Utc>,
772}
773
774#[derive(Clone, Debug, Serialize, Deserialize)]
777pub struct ExpressGatewayService {
778 pub service_name: String,
779 pub service_arn: String,
780 pub cluster_arn: String,
781 pub cluster_name: String,
782 pub status: String,
783 pub execution_role_arn: String,
784 pub infrastructure_role_arn: String,
785 pub task_role_arn: Option<String>,
786 pub primary_container: Value,
787 pub network_configuration: Option<Value>,
788 pub health_check_path: Option<String>,
789 pub cpu: Option<String>,
790 pub memory: Option<String>,
791 pub scaling_target: Option<Value>,
792 pub created_at: DateTime<Utc>,
793 pub updated_at: DateTime<Utc>,
794 #[serde(default)]
795 pub tags: Vec<TagEntry>,
796}
797
798impl EcsState {
799 pub fn daemon_key(cluster: &str, name: &str) -> String {
801 format!("{}/{}", cluster, name)
802 }
803
804 pub fn express_gateway_key(cluster: &str, name: &str) -> String {
806 format!("{}/{}", cluster, name)
807 }
808
809 pub fn allocate_daemon_revision(&mut self, family: &str) -> i32 {
811 let entry = self
812 .next_daemon_revision
813 .entry(family.to_string())
814 .or_insert(0);
815 *entry += 1;
816 *entry
817 }
818
819 pub fn daemon_arn(&self, cluster: &str, name: &str) -> String {
821 fakecloud_aws::arn::Arn::new(
822 "ecs",
823 &self.region,
824 &self.account_id,
825 &format!("daemon/{}/{}", cluster, name),
826 )
827 .to_string()
828 }
829
830 pub fn express_gateway_arn(&self, cluster: &str, name: &str) -> String {
832 fakecloud_aws::arn::Arn::new(
833 "ecs",
834 &self.region,
835 &self.account_id,
836 &format!("express-gateway-service/{}/{}", cluster, name),
837 )
838 .to_string()
839 }
840
841 pub fn daemon_task_definition_arn(&self, family: &str, revision: i32) -> String {
843 fakecloud_aws::arn::Arn::new(
844 "ecs",
845 &self.region,
846 &self.account_id,
847 &format!("daemon-task-definition/{}:{}", family, revision),
848 )
849 .to_string()
850 }
851
852 pub fn daemon_deployment_arn(&self, daemon_name: &str, deployment_id: &str) -> String {
854 fakecloud_aws::arn::Arn::new(
855 "ecs",
856 &self.region,
857 &self.account_id,
858 &format!("daemon-deployment/{}/{}", daemon_name, deployment_id),
859 )
860 .to_string()
861 }
862}
863
864#[cfg(test)]
865mod tests {
866 use super::*;
867
868 #[test]
869 fn resolve_cluster_name_defaults_to_default() {
870 assert_eq!(EcsState::resolve_cluster_name(None), "default");
871 assert_eq!(EcsState::resolve_cluster_name(Some("")), "default");
872 assert_eq!(EcsState::resolve_cluster_name(Some(" ")), "default");
873 }
874
875 #[test]
876 fn resolve_cluster_name_strips_arn_prefix() {
877 assert_eq!(
878 EcsState::resolve_cluster_name(Some("arn:aws:ecs:us-east-1:111122223333:cluster/prod")),
879 "prod"
880 );
881 }
882
883 #[test]
884 fn resolve_cluster_name_passes_through_name() {
885 assert_eq!(EcsState::resolve_cluster_name(Some("prod")), "prod");
886 }
887
888 #[test]
889 fn allocate_revision_monotonic() {
890 let mut s = EcsState::new("111122223333", "us-east-1");
891 assert_eq!(s.allocate_revision("web"), 1);
892 assert_eq!(s.allocate_revision("web"), 2);
893 assert_eq!(s.allocate_revision("worker"), 1);
894 assert_eq!(s.allocate_revision("web"), 3);
895 }
896
897 #[test]
898 fn cluster_arn_format() {
899 let s = EcsState::new("111122223333", "us-east-1");
900 assert_eq!(
901 s.cluster_arn("prod"),
902 "arn:aws:ecs:us-east-1:111122223333:cluster/prod"
903 );
904 }
905
906 #[test]
907 fn task_definition_arn_format() {
908 let s = EcsState::new("111122223333", "us-east-1");
909 assert_eq!(
910 s.task_definition_arn("web", 3),
911 "arn:aws:ecs:us-east-1:111122223333:task-definition/web:3"
912 );
913 }
914
915 #[test]
916 fn task_arn_long_format_default() {
917 let s = EcsState::new("111122223333", "us-east-1");
918 assert_eq!(
919 s.task_arn("prod", "abc123"),
920 "arn:aws:ecs:us-east-1:111122223333:task/prod/abc123"
921 );
922 }
923
924 #[test]
925 fn task_arn_short_when_disabled() {
926 let mut s = EcsState::new("111122223333", "us-east-1");
927 s.account_setting_defaults
928 .insert("taskLongArnFormat".into(), "disabled".into());
929 assert_eq!(
930 s.task_arn("prod", "abc123"),
931 "arn:aws:ecs:us-east-1:111122223333:task/abc123"
932 );
933 }
934
935 #[test]
936 fn service_arn_short_when_disabled() {
937 let mut s = EcsState::new("111122223333", "us-east-1");
938 s.account_setting_defaults
939 .insert("serviceLongArnFormat".into(), "disabled".into());
940 assert_eq!(
941 s.service_arn("prod", "web"),
942 "arn:aws:ecs:us-east-1:111122223333:service/web"
943 );
944 }
945
946 #[test]
947 fn container_instance_arn_short_when_disabled() {
948 let mut s = EcsState::new("111122223333", "us-east-1");
949 s.account_setting_defaults
950 .insert("containerInstanceLongArnFormat".into(), "disabled".into());
951 assert_eq!(
952 s.container_instance_arn("prod", "i-abc"),
953 "arn:aws:ecs:us-east-1:111122223333:container-instance/i-abc"
954 );
955 }
956
957 #[test]
958 fn principal_setting_overrides_default() {
959 let mut s = EcsState::new("111122223333", "us-east-1");
960 s.account_setting_defaults
961 .insert("taskLongArnFormat".into(), "disabled".into());
962 let principal = "arn:aws:iam::111122223333:user/alice".to_string();
963 let mut p = BTreeMap::new();
964 p.insert("taskLongArnFormat".into(), "enabled".into());
965 s.principal_account_settings.insert(principal.clone(), p);
966 assert_eq!(
967 s.effective_account_setting("taskLongArnFormat", Some(&principal))
968 .as_deref(),
969 Some("enabled")
970 );
971 assert_eq!(
973 s.effective_account_setting("taskLongArnFormat", None)
974 .as_deref(),
975 Some("disabled")
976 );
977 }
978
979 #[test]
980 fn reset_clears_all() {
981 let mut s = EcsState::new("111122223333", "us-east-1");
982 s.clusters.insert(
983 "prod".to_string(),
984 Cluster::new("prod", s.cluster_arn("prod")),
985 );
986 s.allocate_revision("web");
987 s.account_setting_defaults
988 .insert("serviceLongArnFormat".into(), "enabled".into());
989 s.reset();
990 assert!(s.clusters.is_empty());
991 assert!(s.next_revision.is_empty());
992 assert!(s.account_setting_defaults.is_empty());
993 }
994}