#![allow(clippy::too_many_arguments)]
use chrono::Utc;
use serde_json::{json, Value};
use fakecloud_core::service::{AwsRequest, AwsResponse, AwsServiceError};
use super::*;
impl EcsService {
pub fn run_task_external(
&self,
account_id: &str,
cluster: &str,
task_definition: &str,
launch_type: Option<&str>,
count: usize,
) -> Result<(), String> {
use bytes::Bytes;
use http::{HeaderMap, Method};
use std::collections::HashMap;
let body = serde_json::json!({
"cluster": cluster,
"taskDefinition": task_definition,
"launchType": launch_type.unwrap_or("FARGATE"),
"count": count.max(1) as i64,
});
let body_bytes =
Bytes::from(serde_json::to_vec(&body).map_err(|e| format!("encode body: {e}"))?);
let req = AwsRequest {
service: "ecs".into(),
action: "RunTask".into(),
region: "us-east-1".into(),
account_id: account_id.to_string(),
request_id: uuid::Uuid::new_v4().to_string(),
headers: HeaderMap::new(),
query_params: HashMap::new(),
body: body_bytes,
body_stream: parking_lot::Mutex::new(None),
path_segments: Vec::new(),
raw_path: "/".into(),
raw_query: String::new(),
method: Method::POST,
is_query_protocol: false,
access_key_id: None,
principal: None,
};
self.run_task(&req)
.map(|_| ())
.map_err(|e| format!("{e:?}"))
}
pub fn run_task(&self, request: &AwsRequest) -> Result<AwsResponse, AwsServiceError> {
let body = request.json_body();
let td_ref = req_str(&body, "taskDefinition")?;
let cluster_ref = opt_str(&body, "cluster");
let cluster_name = EcsState::resolve_cluster_name(cluster_ref);
let launch_type = opt_str(&body, "launchType")
.unwrap_or("FARGATE")
.to_string();
let placement_constraints: Vec<Value> = body
.get("placementConstraints")
.and_then(|v| v.as_array())
.cloned()
.unwrap_or_default();
let placement_strategy: Vec<Value> = body
.get("placementStrategy")
.and_then(|v| v.as_array())
.cloned()
.unwrap_or_default();
let count = body
.get("count")
.and_then(|v| v.as_i64())
.filter(|n| (1..=10).contains(n))
.unwrap_or(1) as usize;
let group = opt_str(&body, "group").map(String::from);
let started_by = opt_str(&body, "startedBy").map(String::from);
let enable_execute_command = body
.get("enableExecuteCommand")
.and_then(|v| v.as_bool())
.unwrap_or(false);
let propagate_tags = opt_str(&body, "propagateTags").map(String::from);
let _enable_ecs_managed_tags = body
.get("enableECSManagedTags")
.and_then(|v| v.as_bool())
.unwrap_or(false);
let _capacity_provider_strategy: Vec<Value> = body
.get("capacityProviderStrategy")
.and_then(|v| v.as_array())
.cloned()
.unwrap_or_default();
let volume_configurations: Vec<Value> = body
.get("volumeConfigurations")
.and_then(|v| v.as_array())
.cloned()
.unwrap_or_default();
let _availability_zone_rebalancing =
opt_str(&body, "availabilityZoneRebalancing").map(String::from);
let mut tags = parse_tags(&body);
if let Some(overrides) = body.get("overrides") {
if let Some(role_arn) = opt_str(overrides, "taskRoleArn") {
self.check_pass_role(&request.account_id, role_arn)?;
}
if let Some(role_arn) = opt_str(overrides, "executionRoleArn") {
self.check_pass_role(&request.account_id, role_arn)?;
}
}
let account = request.account_id.clone();
let runtime = self.runtime.clone();
let mut accounts = self.state.write();
let state = accounts.get_or_create(&account);
let cluster_arn = state
.clusters
.get(&cluster_name)
.map(|c| c.cluster_arn.clone())
.unwrap_or_else(|| state.cluster_arn(&cluster_name));
let (_, family, rev) = resolve_task_definition_ref(td_ref)?;
let revisions = state
.task_definitions
.get(&family)
.ok_or_else(|| task_definition_not_found(td_ref))?;
let td = match rev {
Some(n) => revisions
.get(&n)
.ok_or_else(|| task_definition_not_found(td_ref))?,
None => latest_active_revision(revisions)
.ok_or_else(|| task_definition_not_found(td_ref))?,
};
if td.status != "ACTIVE" {
return Err(client_exception(format!(
"Task definition {} is not ACTIVE",
td.task_definition_arn
)));
}
let td_arn = td.task_definition_arn.clone();
let td_family = td.family.clone();
let td_revision = td.revision;
let td_cpu = td.cpu.clone();
let td_memory = td.memory.clone();
let td_task_role = td.task_role_arn.clone();
let td_exec_role = td.execution_role_arn.clone();
let td_containers = td.container_definitions.clone();
if propagate_tags.as_deref() == Some("TASK_DEFINITION") {
let mut td_tags = td.tags.clone();
td_tags.retain(|t| !tags.iter().any(|x| x.key == t.key));
tags.extend(td_tags);
}
let mut spawned_tasks: Vec<String> = Vec::new();
let mut task_jsons: Vec<Value> = Vec::new();
for _ in 0..count {
let task_id = uuid::Uuid::new_v4().to_string().replace('-', "");
let task_arn = state.task_arn(&cluster_name, &task_id);
let containers: Vec<Container> = td_containers
.iter()
.map(|def| Container {
container_arn: format!(
"arn:aws:ecs:{}:{}:container/{}/{}/{}",
state.region,
state.account_id,
cluster_name,
task_id,
def.get("name").and_then(|v| v.as_str()).unwrap_or("app")
),
name: def
.get("name")
.and_then(|v| v.as_str())
.unwrap_or("app")
.to_string(),
image: def
.get("image")
.and_then(|v| v.as_str())
.unwrap_or("")
.to_string(),
task_arn: task_arn.clone(),
last_status: "PENDING".into(),
exit_code: None,
reason: None,
runtime_id: None,
essential: def
.get("essential")
.and_then(|v| v.as_bool())
.unwrap_or(true),
cpu: def
.get("cpu")
.and_then(|v| v.as_i64())
.map(|n| n.to_string()),
memory: def
.get("memory")
.and_then(|v| v.as_i64())
.map(|n| n.to_string()),
memory_reservation: def
.get("memoryReservation")
.and_then(|v| v.as_i64())
.map(|n| n.to_string()),
network_bindings: Vec::new(),
network_interfaces: Vec::new(),
health_status: Some("UNKNOWN".to_string()),
managed_agents: None,
image_digest: None,
})
.collect();
let awslogs = td_containers.iter().find_map(|def| {
let name = def.get("name").and_then(|v| v.as_str())?.to_string();
let log_cfg = def.get("logConfiguration")?;
if log_cfg.get("logDriver").and_then(|v| v.as_str()) != Some("awslogs") {
return None;
}
let opts = log_cfg.get("options").and_then(|v| v.as_object())?;
Some(AwsLogsConfig {
group: opts.get("awslogs-group").and_then(|v| v.as_str())?.into(),
stream_prefix: opts
.get("awslogs-stream-prefix")
.and_then(|v| v.as_str())
.map(String::from),
region: opts
.get("awslogs-region")
.and_then(|v| v.as_str())
.unwrap_or(&state.region)
.to_string(),
container_name: name,
})
});
let capacity_provider_name = body
.get("capacityProviderStrategy")
.and_then(|v| v.as_array())
.and_then(|arr| arr.first())
.and_then(|item| item.get("capacityProvider"))
.and_then(|v| v.as_str())
.map(String::from);
let mut task = Task {
task_arn: task_arn.clone(),
task_id: task_id.clone(),
cluster_arn: cluster_arn.clone(),
cluster_name: cluster_name.clone(),
task_definition_arn: td_arn.clone(),
family: td_family.clone(),
revision: td_revision,
container_instance_arn: None,
capacity_provider_name,
last_status: "PROVISIONING".into(),
desired_status: "RUNNING".into(),
launch_type: launch_type.clone(),
platform_version: Some("1.4.0".into()),
cpu: body
.get("overrides")
.and_then(|v| v.get("cpu"))
.and_then(|v| v.as_str())
.map(String::from)
.or_else(|| td_cpu.clone()),
memory: body
.get("overrides")
.and_then(|v| v.get("memory"))
.and_then(|v| v.as_str())
.map(String::from)
.or_else(|| td_memory.clone()),
containers,
overrides: body.get("overrides").cloned().unwrap_or_else(|| json!({})),
started_by: started_by.clone(),
group: group.clone(),
connectivity: "CONNECTING".into(),
stop_code: None,
stopped_reason: None,
created_at: Utc::now(),
started_at: None,
stopping_at: None,
stopped_at: None,
pull_started_at: None,
pull_stopped_at: None,
connectivity_at: None,
started_by_ref_id: None,
execution_role_arn: td_exec_role.clone(),
task_role_arn: td_task_role.clone(),
tags: tags.clone(),
awslogs,
captured_logs: String::new(),
protection: None,
enable_execute_command,
attachments: Vec::new(),
volume_configurations: volume_configurations.clone(),
task_set_arn: None,
};
if launch_type != "FARGATE" {
if let Some(arn) = crate::placement::select_container_instance(
state,
&cluster_name,
&placement_constraints,
&placement_strategy,
task.group.as_deref(),
&td_arn,
&launch_type,
) {
task.container_instance_arn = Some(arn.clone());
if let Some(ci) = state
.container_instances
.values_mut()
.find(|ci| ci.container_instance_arn == arn)
{
ci.pending_tasks_count += 1;
}
}
}
state.tasks.insert(task_id.clone(), task.clone());
if let Some(cluster) = state.clusters.get_mut(&cluster_name) {
cluster.pending_tasks_count += 1;
}
if let Some(t) = state.tasks.get_mut(&task_id) {
t.last_status = "PENDING".into();
}
task_jsons.push(task_to_json(&task));
spawned_tasks.push(task_id.clone());
}
drop(accounts);
if let Some(rt) = runtime {
for id in &spawned_tasks {
rt.clone()
.run_task(self.state.clone(), id.clone(), account.clone());
}
} else {
let mut accounts = self.state.write();
if let Some(state) = accounts.get_mut(&account) {
let mut cluster_drains: Vec<String> = Vec::new();
for id in &spawned_tasks {
if let Some(t) = state.tasks.get_mut(id) {
t.last_status = "STOPPED".into();
t.stop_code = Some("TaskFailedToStart".into());
t.stopped_reason = Some(
"No container runtime available (docker/podman not installed)".into(),
);
t.stopped_at = Some(Utc::now());
for c in t.containers.iter_mut() {
c.last_status = "STOPPED".into();
}
cluster_drains.push(t.cluster_name.clone());
}
}
for name in cluster_drains {
if let Some(cluster) = state.clusters.get_mut(&name) {
if cluster.pending_tasks_count > 0 {
cluster.pending_tasks_count -= 1;
}
}
}
}
}
Ok(AwsResponse::ok_json(json!({
"tasks": task_jsons,
"failures": [],
})))
}
pub(super) fn start_task(&self, request: &AwsRequest) -> Result<AwsResponse, AwsServiceError> {
self.run_task(request)
}
pub(super) async fn stop_task(
&self,
request: &AwsRequest,
) -> Result<AwsResponse, AwsServiceError> {
let body = request.json_body();
let task_ref = req_str(&body, "task")?;
let reason = opt_str(&body, "reason")
.unwrap_or("UserInitiated")
.to_string();
let cluster_ref = opt_str(&body, "cluster");
let _cluster_name = EcsState::resolve_cluster_name(cluster_ref);
let (task_id, account, task_snapshot) = {
let account = request.account_id.clone();
let mut accounts = self.state.write();
let state = accounts
.get_mut(&account)
.ok_or_else(|| task_not_found(task_ref))?;
let task_id = resolve_task_id(state, task_ref)?;
let task = state
.tasks
.get_mut(&task_id)
.ok_or_else(|| task_not_found(task_ref))?;
task.desired_status = "STOPPED".into();
task.stopping_at = Some(Utc::now());
task.stopped_reason = Some(reason.clone());
task.stop_code = Some("UserInitiated".into());
(task_id, account, task.clone())
};
if let Some(rt) = &self.runtime {
rt.stop_task(&task_id, &reason).await;
}
let _ = account;
Ok(AwsResponse::ok_json(json!({
"task": task_to_json(&task_snapshot),
})))
}
pub(super) fn describe_tasks(
&self,
request: &AwsRequest,
) -> Result<AwsResponse, AwsServiceError> {
let body = request.json_body();
let refs: Vec<String> = req_array(&body, "tasks")?
.iter()
.filter_map(|v| v.as_str().map(String::from))
.collect();
let include_tags = body
.get("include")
.and_then(|v| v.as_array())
.map(|arr| arr.iter().any(|v| v.as_str() == Some("TAGS")))
.unwrap_or(false);
let account = request.account_id.clone();
let accounts = self.state.read();
let Some(state) = accounts.get(&account) else {
return Ok(AwsResponse::ok_json(json!({
"tasks": [],
"failures": refs.iter().map(|r| json!({"arn": r, "reason": "MISSING"})).collect::<Vec<_>>(),
})));
};
let mut found = Vec::new();
let mut failures = Vec::new();
for input in &refs {
let task_id = task_id_from_ref(input);
match state.tasks.get(&task_id) {
Some(t) => {
let mut v = task_to_json(t);
if include_tags {
v.as_object_mut()
.unwrap()
.insert("tags".into(), tags_json(&t.tags));
}
found.push(v);
}
None => {
failures.push(json!({
"arn": input,
"reason": "MISSING",
}));
}
}
}
Ok(AwsResponse::ok_json(json!({
"tasks": found,
"failures": failures,
})))
}
pub(super) fn list_tasks(&self, request: &AwsRequest) -> Result<AwsResponse, AwsServiceError> {
let body = request.json_body();
validate_enum_opt(&body, "desiredStatus", &["RUNNING", "PENDING", "STOPPED"])?;
validate_enum_opt(
&body,
"launchType",
&["EC2", "FARGATE", "EXTERNAL", "MANAGED_INSTANCES"],
)?;
let cluster_ref = opt_str(&body, "cluster");
let cluster_name = EcsState::resolve_cluster_name(cluster_ref);
let family = opt_str(&body, "family");
let status_filter = opt_str(&body, "desiredStatus").or(Some("RUNNING"));
let started_by = opt_str(&body, "startedBy");
let max_results = body
.get("maxResults")
.and_then(|v| v.as_i64())
.filter(|n| (1..=100).contains(n))
.map(|n| n as usize)
.unwrap_or(100);
let next_token = opt_str(&body, "nextToken").unwrap_or("");
let account = request.account_id.clone();
let accounts = self.state.read();
let mut arns: Vec<String> = match accounts.get(&account) {
Some(state) => state
.tasks
.values()
.filter(|t| t.cluster_name == cluster_name)
.filter(|t| family.is_none_or(|f| t.family == f))
.filter(|t| status_filter.is_none_or(|s| t.desired_status == s))
.filter(|t| started_by.is_none_or(|s| t.started_by.as_deref() == Some(s)))
.map(|t| t.task_arn.clone())
.collect(),
None => Vec::new(),
};
arns.sort();
let start = next_token.parse::<usize>().unwrap_or(0).min(arns.len());
let end = (start + max_results).min(arns.len());
let page = arns[start..end].to_vec();
let mut out = json!({"taskArns": page});
if end < arns.len() {
out.as_object_mut()
.unwrap()
.insert("nextToken".into(), json!(end.to_string()));
}
Ok(AwsResponse::ok_json(out))
}
}
#[cfg(test)]
mod multi_container_tests {
use super::*;
use crate::EcsService;
use bytes::Bytes;
use fakecloud_core::multi_account::MultiAccountState;
use http::{HeaderMap, Method};
use parking_lot::RwLock;
use std::collections::HashMap;
use std::sync::Arc;
fn fresh_service() -> EcsService {
let accounts: MultiAccountState<EcsState> =
MultiAccountState::new("000000000000", "us-east-1", "http://localhost:4566");
let state = Arc::new(RwLock::new(accounts));
let svc = EcsService::new(state.clone());
let mut accounts = state.write();
let s = accounts.get_or_create("000000000000");
let arn = s.cluster_arn("default");
s.clusters
.insert("default".into(), Cluster::new("default", arn));
drop(accounts);
svc
}
fn make_request(action: &str, body: Value) -> AwsRequest {
let body_bytes = Bytes::from(serde_json::to_vec(&body).unwrap());
AwsRequest {
service: "ecs".into(),
action: action.into(),
region: "us-east-1".into(),
account_id: "000000000000".into(),
request_id: uuid::Uuid::new_v4().to_string(),
headers: HeaderMap::new(),
query_params: HashMap::new(),
body: body_bytes,
body_stream: parking_lot::Mutex::new(None),
path_segments: Vec::new(),
raw_path: "/".into(),
raw_query: String::new(),
method: Method::POST,
is_query_protocol: false,
access_key_id: None,
principal: None,
}
}
#[test]
fn register_task_def_with_two_containers_then_run_task_starts_both() {
let svc = fresh_service();
let reg = make_request(
"RegisterTaskDefinition",
json!({
"family": "multi",
"containerDefinitions": [
{"name": "app", "image": "alpine"},
{"name": "sidecar", "image": "alpine"}
]
}),
);
svc.register_task_definition(®)
.expect("register should succeed");
let run = make_request(
"RunTask",
json!({
"cluster": "default",
"taskDefinition": "multi",
}),
);
let resp = svc.run_task(&run).expect("run_task should succeed");
let body: Value =
serde_json::from_slice(resp.body.expect_bytes()).expect("body should be valid JSON");
let tasks = body
.get("tasks")
.and_then(|v| v.as_array())
.expect("tasks array");
assert_eq!(tasks.len(), 1);
let task = &tasks[0];
let containers = task
.get("containers")
.and_then(|v| v.as_array())
.expect("containers array on task");
assert_eq!(containers.len(), 2, "expected both containers in task");
let names: Vec<&str> = containers
.iter()
.filter_map(|c| c.get("name").and_then(|v| v.as_str()))
.collect();
assert!(names.contains(&"app"));
assert!(names.contains(&"sidecar"));
let arns: std::collections::HashSet<&str> = containers
.iter()
.filter_map(|c| c.get("containerArn").and_then(|v| v.as_str()))
.collect();
assert_eq!(arns.len(), 2);
}
#[test]
fn register_task_def_defaults_essential_true() {
let svc = fresh_service();
let reg = make_request(
"RegisterTaskDefinition",
json!({
"family": "default-essential",
"containerDefinitions": [
{"name": "main", "image": "alpine"},
{"name": "extra", "image": "alpine"}
]
}),
);
svc.register_task_definition(®).unwrap();
let run = make_request(
"RunTask",
json!({
"cluster": "default",
"taskDefinition": "default-essential",
}),
);
let resp = svc.run_task(&run).unwrap();
let body: Value = serde_json::from_slice(resp.body.expect_bytes()).unwrap();
let containers = body["tasks"][0]["containers"].as_array().unwrap();
for c in containers {
assert!(
c.get("essential").is_none(),
"container {:?} must not carry `essential` on the runtime shape",
c.get("name")
);
}
}
#[test]
fn task_to_json_emits_full_container_array() {
let mut task = Task {
task_arn: "arn:aws:ecs:us-east-1:000000000000:task/default/abc".into(),
task_id: "abc".into(),
cluster_arn: "arn:aws:ecs:us-east-1:000000000000:cluster/default".into(),
cluster_name: "default".into(),
task_definition_arn: "arn:aws:ecs:us-east-1:000000000000:task-definition/multi:1"
.into(),
family: "multi".into(),
revision: 1,
container_instance_arn: None,
capacity_provider_name: None,
last_status: "RUNNING".into(),
desired_status: "RUNNING".into(),
launch_type: "FARGATE".into(),
platform_version: None,
cpu: None,
memory: None,
containers: Vec::new(),
overrides: json!({}),
started_by: None,
group: None,
connectivity: "CONNECTED".into(),
stop_code: None,
stopped_reason: None,
created_at: chrono::Utc::now(),
started_at: None,
stopping_at: None,
stopped_at: None,
pull_started_at: None,
pull_stopped_at: None,
connectivity_at: None,
started_by_ref_id: None,
execution_role_arn: None,
task_role_arn: None,
tags: Vec::new(),
awslogs: None,
captured_logs: String::new(),
protection: None,
enable_execute_command: false,
attachments: Vec::new(),
volume_configurations: Vec::new(),
task_set_arn: None,
};
for name in ["app", "sidecar"] {
task.containers.push(Container {
container_arn: format!(
"arn:aws:ecs:us-east-1:000000000000:container/default/abc/{name}"
),
name: name.into(),
image: "alpine".into(),
task_arn: task.task_arn.clone(),
last_status: "RUNNING".into(),
exit_code: None,
reason: None,
runtime_id: Some(format!("docker-{name}")),
essential: true,
cpu: None,
memory: None,
memory_reservation: None,
network_bindings: Vec::new(),
network_interfaces: Vec::new(),
health_status: None,
managed_agents: None,
image_digest: None,
});
}
let v = task_to_json(&task);
let containers = v
.get("containers")
.and_then(|v| v.as_array())
.expect("containers array");
assert_eq!(containers.len(), 2);
let names: Vec<&str> = containers
.iter()
.filter_map(|c| c.get("name").and_then(|v| v.as_str()))
.collect();
assert_eq!(names, vec!["app", "sidecar"]);
for c in containers {
assert!(c.get("containerArn").is_some());
assert!(c.get("name").is_some());
assert!(c.get("lastStatus").is_some());
assert!(c.get("runtimeId").is_some());
assert!(c.get("essential").is_none());
}
}
}
#[cfg(test)]
mod port_mapping_tests {
use super::*;
use crate::runtime::{
build_run_argv, mark_running_multi, ContainerPlan, PortMapping, RunningContainer,
};
use crate::state::{Container, EcsState};
use crate::SharedEcsState;
use chrono::Utc;
use fakecloud_core::multi_account::MultiAccountState;
use parking_lot::RwLock;
use std::sync::Arc;
fn plan_with_ports(
port_mappings: Vec<PortMapping>,
network_mode: Option<&str>,
) -> ContainerPlan {
ContainerPlan {
container_name: "app".into(),
image: "alpine:latest".into(),
env: Vec::new(),
entry_point: Vec::new(),
command: Vec::new(),
secrets_refs: Vec::new(),
essential: true,
has_task_role: false,
port_mappings,
network_mode: network_mode.map(String::from),
depends_on: Vec::new(),
health_check: None,
volume_mounts: Vec::new(),
ulimits: Vec::new(),
linux_parameters: None,
stop_timeout: None,
user: None,
working_directory: None,
tty: false,
interactive: false,
readonly_rootfs: false,
}
}
fn argv_string(plan: &ContainerPlan) -> Vec<String> {
build_run_argv(
plan,
&[],
"task-1",
"host.docker.internal",
None,
"alpine:latest",
true,
)
}
fn argv_has_publish(argv: &[String], spec: &str) -> bool {
argv.windows(2).any(|w| w[0] == "--publish" && w[1] == spec)
}
#[test]
fn port_mappings_translate_to_publish_flags() {
let plan = plan_with_ports(
vec![PortMapping {
container_port: 80,
host_port: 8080,
protocol: "tcp".into(),
}],
None,
);
let argv = argv_string(&plan);
assert!(
argv_has_publish(&argv, "80:8080/tcp"),
"expected --publish 80:8080/tcp in argv: {argv:?}"
);
}
#[test]
fn port_mappings_default_host_port_to_container_port() {
let parsed =
crate::runtime::__test_parse_port_mapping(&serde_json::json!({"containerPort": 80}))
.expect("containerPort should parse");
assert_eq!(
parsed.host_port, 80,
"default hostPort should mirror containerPort"
);
let argv = argv_string(&plan_with_ports(vec![parsed], None));
assert!(
argv_has_publish(&argv, "80:80/tcp"),
"expected --publish 80:80/tcp when hostPort omitted: {argv:?}"
);
}
#[test]
fn port_mappings_default_protocol_tcp() {
let parsed = crate::runtime::__test_parse_port_mapping(
&serde_json::json!({"containerPort": 443, "hostPort": 443}),
)
.expect("containerPort should parse");
assert_eq!(parsed.protocol, "tcp");
let argv = argv_string(&plan_with_ports(vec![parsed], None));
assert!(
argv_has_publish(&argv, "443:443/tcp"),
"expected default protocol tcp: {argv:?}"
);
}
#[test]
fn awsvpc_network_mode_skips_publish() {
let plan = plan_with_ports(
vec![PortMapping {
container_port: 80,
host_port: 8080,
protocol: "tcp".into(),
}],
Some("awsvpc"),
);
let argv = argv_string(&plan);
assert!(
!argv.iter().any(|s| s == "--publish"),
"awsvpc must not emit --publish: {argv:?}"
);
}
#[test]
fn awsvpc_network_mode_includes_network_flag() {
let plan = plan_with_ports(Vec::new(), Some("awsvpc"));
let argv = argv_string(&plan);
let network_idx = argv.iter().position(|s| s == "--network");
assert!(
network_idx.is_some(),
"awsvpc must emit --network: {argv:?}"
);
let network_name = argv.get(network_idx.unwrap() + 1);
assert!(
network_name
.map(|n| n.starts_with("fakecloud-ecs-"))
.unwrap_or(false),
"awsvpc must reference fakecloud-ecs network: {argv:?}"
);
}
#[test]
fn network_bindings_populated_on_task() {
let mut accounts: MultiAccountState<EcsState> =
MultiAccountState::new("000000000000", "us-east-1", "http://localhost:4566");
let acct = accounts.get_or_create("000000000000");
let arn = acct.cluster_arn("default");
acct.clusters
.insert("default".into(), Cluster::new("default", arn));
let mut task = Task {
task_arn: "arn:aws:ecs:us-east-1:000000000000:task/default/abc".into(),
task_id: "abc".into(),
cluster_arn: "arn:aws:ecs:us-east-1:000000000000:cluster/default".into(),
cluster_name: "default".into(),
task_definition_arn: "arn:aws:ecs:us-east-1:000000000000:task-definition/web:1".into(),
family: "web".into(),
revision: 1,
container_instance_arn: None,
capacity_provider_name: None,
last_status: "PENDING".into(),
desired_status: "RUNNING".into(),
launch_type: "FARGATE".into(),
platform_version: None,
cpu: None,
memory: None,
containers: vec![Container {
container_arn: "arn:aws:ecs:us-east-1:000000000000:container/default/abc/web"
.into(),
name: "web".into(),
image: "alpine".into(),
task_arn: "arn:aws:ecs:us-east-1:000000000000:task/default/abc".into(),
last_status: "PENDING".into(),
exit_code: None,
reason: None,
runtime_id: None,
essential: true,
cpu: None,
memory: None,
memory_reservation: None,
network_bindings: Vec::new(),
network_interfaces: Vec::new(),
health_status: None,
managed_agents: None,
image_digest: None,
}],
overrides: serde_json::json!({}),
started_by: None,
group: None,
connectivity: "CONNECTING".into(),
stop_code: None,
stopped_reason: None,
created_at: Utc::now(),
started_at: None,
stopping_at: None,
stopped_at: None,
pull_started_at: None,
pull_stopped_at: None,
connectivity_at: None,
started_by_ref_id: None,
execution_role_arn: None,
task_role_arn: None,
tags: Vec::new(),
awslogs: None,
captured_logs: String::new(),
protection: None,
enable_execute_command: false,
attachments: Vec::new(),
volume_configurations: Vec::new(),
task_set_arn: None,
};
task.last_status = "PENDING".into();
acct.tasks.insert("abc".into(), task);
let state: SharedEcsState = Arc::new(RwLock::new(accounts));
let bindings = vec![serde_json::json!({
"bindIP": "0.0.0.0",
"containerPort": 80,
"hostPort": 8080,
"protocol": "tcp",
})];
let started = vec![RunningContainer {
name: "web".into(),
container_id: "docker-id".into(),
essential: true,
exit_code: None,
network_bindings: bindings.clone(),
image_digest: None,
}];
mark_running_multi(&state, "000000000000", "abc", &started);
let accounts = state.read();
let task = accounts
.get("000000000000")
.unwrap()
.tasks
.get("abc")
.unwrap();
let json = task_to_json(task);
let nb = &json["containers"][0]["networkBindings"];
assert_eq!(nb, &serde_json::Value::Array(bindings));
}
}