Skip to content

Commit 5c3a851

Browse files
authored
fix(ecs): apply the full mutable field set in UpdateService (#1799)
2 parents 2fb92b6 + dd4342f commit 5c3a851

2 files changed

Lines changed: 130 additions & 0 deletions

File tree

crates/fakecloud-ecs/src/service_services_resource.rs

Lines changed: 61 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -398,6 +398,67 @@ impl EcsService {
398398
service_cluster_arn = svc.cluster_arn.clone();
399399
launch_type_clone = svc.launch_type.clone();
400400

401+
// Apply the remaining mutable service fields UpdateService
402+
// accepts. AWS updates each only when present in the request;
403+
// omitting one leaves it unchanged. The old handler read only
404+
// desiredCount / taskDefinition / lifecycleHooks and silently
405+
// dropped everything else, so ECS Exec enablement, subnet/SG
406+
// changes, deployment tuning, load-balancer swaps, etc. all
407+
// no-op'd (bug-audit 2026-06-20, 1.15).
408+
if let Some(dc) = body.get("deploymentConfiguration") {
409+
if let Some(n) = dc.get("minimumHealthyPercent").and_then(|v| v.as_i64()) {
410+
svc.minimum_healthy_percent = Some(n as i32);
411+
}
412+
if let Some(n) = dc.get("maximumPercent").and_then(|v| v.as_i64()) {
413+
svc.maximum_percent = Some(n as i32);
414+
}
415+
if let Some(c) = dc.get("deploymentCircuitBreaker") {
416+
svc.circuit_breaker = Some(CircuitBreakerConfig {
417+
enable: c.get("enable").and_then(|v| v.as_bool()).unwrap_or(false),
418+
rollback: c.get("rollback").and_then(|v| v.as_bool()).unwrap_or(false),
419+
});
420+
}
421+
}
422+
if let Some(v) = body.get("networkConfiguration") {
423+
svc.network_configuration = Some(v.clone());
424+
}
425+
if let Some(s) = opt_str(&body, "platformVersion") {
426+
svc.platform_version = Some(s.to_string());
427+
}
428+
if let Some(n) = body
429+
.get("healthCheckGracePeriodSeconds")
430+
.and_then(|v| v.as_i64())
431+
{
432+
svc.health_check_grace_period_seconds = Some(n as i32);
433+
}
434+
if let Some(b) = body.get("enableExecuteCommand").and_then(|v| v.as_bool()) {
435+
svc.enable_execute_command = b;
436+
}
437+
if let Some(b) = body.get("enableECSManagedTags").and_then(|v| v.as_bool()) {
438+
svc.enable_ecs_managed_tags = b;
439+
}
440+
if let Some(s) = opt_str(&body, "propagateTags") {
441+
svc.propagate_tags = Some(s.to_string());
442+
}
443+
if let Some(a) = body
444+
.get("capacityProviderStrategy")
445+
.and_then(|v| v.as_array())
446+
{
447+
svc.capacity_provider_strategy = a.clone();
448+
}
449+
if let Some(a) = body.get("placementConstraints").and_then(|v| v.as_array()) {
450+
svc.placement_constraints = a.clone();
451+
}
452+
if let Some(a) = body.get("placementStrategy").and_then(|v| v.as_array()) {
453+
svc.placement_strategy = a.clone();
454+
}
455+
if let Some(a) = body.get("loadBalancers").and_then(|v| v.as_array()) {
456+
svc.load_balancers = a.clone();
457+
}
458+
if let Some(a) = body.get("serviceRegistries").and_then(|v| v.as_array()) {
459+
svc.service_registries = a.clone();
460+
}
461+
401462
if let Some(n) = new_desired {
402463
let n = n.max(0) as i32;
403464
svc.desired_count = n;

crates/fakecloud-ecs/src/service_tests.rs

Lines changed: 69 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -402,6 +402,75 @@ mod scheduler_reconcile {
402402
Arc::new(RwLock::new(accounts))
403403
}
404404

405+
fn ecs_request(action: &str, body: serde_json::Value) -> AwsRequest {
406+
AwsRequest {
407+
service: "ecs".into(),
408+
action: action.into(),
409+
region: "us-east-1".into(),
410+
account_id: ACCOUNT.into(),
411+
request_id: uuid::Uuid::new_v4().to_string(),
412+
headers: http::HeaderMap::new(),
413+
query_params: std::collections::HashMap::new(),
414+
body: bytes::Bytes::from(serde_json::to_vec(&body).unwrap()),
415+
body_stream: parking_lot::Mutex::new(None),
416+
path_segments: Vec::new(),
417+
raw_path: "/".into(),
418+
raw_query: String::new(),
419+
method: http::Method::POST,
420+
is_query_protocol: false,
421+
access_key_id: None,
422+
principal: None,
423+
}
424+
}
425+
426+
#[tokio::test]
427+
async fn update_service_applies_extended_fields() {
428+
// UpdateService previously read only desiredCount/taskDefinition/
429+
// lifecycleHooks and dropped everything else (bug-audit 2026-06-20, 1.15).
430+
let state = empty_state();
431+
{
432+
let mut g = state.write();
433+
let st = g.get_or_create(ACCOUNT);
434+
st.services
435+
.insert("default/api".to_string(), make_service(2));
436+
}
437+
let svc = EcsService::new(state.clone());
438+
439+
let body = serde_json::json!({
440+
"service": "api",
441+
"cluster": "default",
442+
"enableExecuteCommand": true,
443+
"enableECSManagedTags": true,
444+
"healthCheckGracePeriodSeconds": 120,
445+
"platformVersion": "1.4.0",
446+
"propagateTags": "SERVICE",
447+
"networkConfiguration": { "awsvpcConfiguration": { "subnets": ["subnet-aaa"] } },
448+
"deploymentConfiguration": {
449+
"minimumHealthyPercent": 50,
450+
"maximumPercent": 200,
451+
"deploymentCircuitBreaker": { "enable": true, "rollback": true }
452+
}
453+
});
454+
svc.update_service(&ecs_request("UpdateService", body))
455+
.expect("UpdateService");
456+
457+
let g = state.read();
458+
let s = &g.get(ACCOUNT).unwrap().services["default/api"];
459+
assert!(s.enable_execute_command, "enableExecuteCommand must apply");
460+
assert!(s.enable_ecs_managed_tags);
461+
assert_eq!(s.health_check_grace_period_seconds, Some(120));
462+
assert_eq!(s.platform_version.as_deref(), Some("1.4.0"));
463+
assert_eq!(s.propagate_tags.as_deref(), Some("SERVICE"));
464+
assert!(s.network_configuration.is_some());
465+
assert_eq!(s.minimum_healthy_percent, Some(50));
466+
assert_eq!(s.maximum_percent, Some(200));
467+
assert!(s
468+
.circuit_breaker
469+
.as_ref()
470+
.map(|c| c.enable)
471+
.unwrap_or(false));
472+
}
473+
405474
/// No snapshot store (memory mode) -> no persist hook for the CFN provisioner.
406475
#[test]
407476
fn snapshot_hook_is_none_without_store() {

0 commit comments

Comments
 (0)