From c5ef9d4ff083d6dee53c88725a0af57de7393505 Mon Sep 17 00:00:00 2001 From: Lucas Vieira Date: Sat, 17 Oct 2026 14:48:48 -0300 Subject: [PATCH 1/7] feat(conformance): measure AWS Batch; fix the gaps the probe found AWS Batch shipped in June but `aws-models/service-map.json` had no `batch` entry, so the conformance sweep never loaded its model: Batch was absent from the baseline, the parity matrix and every headline count, while the site claimed true 100% across every implemented service. - Probe: map the model, route it as restJson1, add the 45 `#[test_action]` annotations and the audit mapping. - Batch fixes surfaced by the 1,257 variants (1,230 passed before): - `CancelJob` / `TerminateJob` require `reason` (ClientException). - `RegisterJobDefinition` requires `type` and accepts only `container` / `multinode`. - Tag operations reject a `resourceArn` that is not a Batch ARN instead of tagging an arbitrary string. - `DescribeJobQueues` always returns the required `computeEnvironmentOrder` (empty for service-environment queues). - Baseline: batch 1,257/1,257; total 258,545/258,545. - Docs: Batch row in the parity matrix (45 ops) and service lists; headline counts cascade to 107 services, 7,598 operations. --- aws-models/service-map.json | 4 + conformance-baseline.json | 8 +- crates/fakecloud-batch/src/service.rs | 136 ++++++++++++++++-- crates/fakecloud-conformance/src/audit.rs | 1 + .../src/probe/service_detection.rs | 2 + crates/fakecloud-conformance/tests/batch.rs | 54 +++++++ website/content/docs/services/batch.md | 2 + 7 files changed, 193 insertions(+), 14 deletions(-) create mode 100644 crates/fakecloud-conformance/tests/batch.rs diff --git a/aws-models/service-map.json b/aws-models/service-map.json index cdc794cde..b03344ed8 100644 --- a/aws-models/service-map.json +++ b/aws-models/service-map.json @@ -243,6 +243,10 @@ "repo_dir": "backup", "service_name": "backup" }, + "batch": { + "repo_dir": "batch", + "service_name": "batch" + }, "ram": { "repo_dir": "ram", "service_name": "ram" diff --git a/conformance-baseline.json b/conformance-baseline.json index eb3661edf..c3f9b9919 100644 --- a/conformance-baseline.json +++ b/conformance-baseline.json @@ -1,6 +1,6 @@ { - "variants_passed": 257288, - "total_variants": 257288, + "variants_passed": 258545, + "total_variants": 258545, "per_service": { "ce": { "passed": 1781, @@ -429,6 +429,10 @@ "firehose": { "passed": 416, "total": 416 + }, + "batch": { + "passed": 1257, + "total": 1257 } } } diff --git a/crates/fakecloud-batch/src/service.rs b/crates/fakecloud-batch/src/service.rs index 8a3ac25ff..d4ad1262e 100644 --- a/crates/fakecloud-batch/src/service.rs +++ b/crates/fakecloud-batch/src/service.rs @@ -351,6 +351,30 @@ pub(crate) fn client_error(code: &str, msg: impl Into) -> AwsServiceErro AwsServiceError::aws_error(StatusCode::BAD_REQUEST, code, msg.into()) } +/// The `{resourceArn}` label of the tag operations, which AWS rejects unless it +/// is a Batch ARN (`arn::batch:::`). +fn tag_resource_arn(arn: &str) -> Result { + if arn.is_empty() { + return Err(client_error("ClientException", "resourceArn is required")); + } + let parts: Vec<&str> = arn.splitn(6, ':').collect(); + let valid = parts.len() == 6 + && parts[0] == "arn" + && !parts[1].is_empty() + && parts[2] == "batch" + && !parts[3].is_empty() + && parts[4].len() == 12 + && parts[4].bytes().all(|b| b.is_ascii_digit()) + && !parts[5].is_empty(); + if !valid { + return Err(client_error( + "ClientException", + format!("Invalid resourceArn: {arn}"), + )); + } + Ok(arn.to_string()) +} + pub(crate) type TagStore = std::collections::BTreeMap>; @@ -578,7 +602,20 @@ impl BatchService { st.job_queues .values() .filter(|q| match_named(q, &wanted, "jobQueueName", "jobQueueArn")) - .map(|q| merge_tag_overlay(q, "jobQueueArn", &st.tags)) + .map(|q| { + let mut q = merge_tag_overlay(q, "jobQueueArn", &st.tags); + // `computeEnvironmentOrder` is required on JobQueueDetail: a + // queue fed by service environments reports it empty. + if let Some(o) = q.as_object_mut() { + let order = o + .entry("computeEnvironmentOrder") + .or_insert_with(|| json!([])); + if order.is_null() { + *order = json!([]); + } + } + q + }) .collect() }) .unwrap_or_default(); @@ -605,6 +642,18 @@ impl BatchService { .and_then(Value::as_str) .ok_or_else(|| client_error("ClientException", "jobDefinitionName is required"))? .to_string(); + match body.get("type").and_then(Value::as_str) { + Some("container" | "multinode") => {} + Some(other) => { + return Err(client_error( + "ClientException", + format!( + "Invalid job definition type: {other}. Valid values: container, multinode" + ), + )) + } + None => return Err(client_error("ClientException", "type is required")), + } let mut accounts = self.state.write(); let st = accounts.get_or_create(&req.account_id); let revision = st.job_def_revisions.entry(name.clone()).or_insert(0); @@ -1372,7 +1421,7 @@ impl BatchService { let reason = body .get("reason") .and_then(Value::as_str) - .unwrap_or(op) + .ok_or_else(|| client_error("ClientException", format!("reason is required for {op}")))? .to_string(); let mut accounts = self.state.write(); if let Some(job) = accounts @@ -1397,8 +1446,9 @@ impl BatchService { let arn = req .path_segments .get(2) - .cloned() - .ok_or_else(|| client_error("ClientException", "resourceArn is required"))?; + .map(String::as_str) + .map(tag_resource_arn) + .unwrap_or_else(|| Err(client_error("ClientException", "resourceArn is required")))?; let body = req.json_body(); let tags = body .get("tags") @@ -1423,8 +1473,9 @@ impl BatchService { let arn = req .path_segments .get(2) - .cloned() - .ok_or_else(|| client_error("ClientException", "resourceArn is required"))?; + .map(String::as_str) + .map(tag_resource_arn) + .unwrap_or_else(|| Err(client_error("ClientException", "resourceArn is required")))?; // `tagKeys` is an `@httpQuery` list sent as repeated `tagKeys=a&tagKeys=b` // pairs; `query_params` collapses repeats to the last value, so parse the // raw query string for every occurrence, percent-decoding each. @@ -1447,8 +1498,9 @@ impl BatchService { let arn = req .path_segments .get(2) - .cloned() - .ok_or_else(|| client_error("ClientException", "resourceArn is required"))?; + .map(String::as_str) + .map(tag_resource_arn) + .unwrap_or_else(|| Err(client_error("ClientException", "resourceArn is required")))?; let accounts = self.state.read(); let tags = accounts .get(&req.account_id) @@ -2540,6 +2592,64 @@ mod tests { } } + #[tokio::test] + async fn required_inputs_are_rejected_when_missing() { + let s = svc(); + for (path, body) in [ + ( + "/v1/registerjobdefinition", + json!({"jobDefinitionName": "jd"}), + ), + ( + "/v1/registerjobdefinition", + json!({"jobDefinitionName": "jd", "type": "lambda"}), + ), + ("/v1/canceljob", json!({"jobId": "j"})), + ("/v1/terminatejob", json!({"jobId": "j"})), + ] { + match s.handle(req(path, body.clone())).await { + Err(e) => assert!(format!("{e:?}").contains("ClientException"), "{path}"), + Ok(_) => panic!("{path} {body} must be rejected"), + } + } + for path in [ + "/v1/tags/", + "/v1/tags/%7BresourceArn%7D", + "/v1/tags/not-an-arn", + ] { + for method in [Method::GET, Method::POST, Method::DELETE] { + let mut r = req(path, json!({"tags": {"k": "v"}})); + r.method = method.clone(); + assert!( + s.handle(r).await.is_err(), + "{method} {path} has no Batch ARN" + ); + } + } + // Nothing was registered by the rejected calls. + let d = body_of( + s.handle(req( + "/v1/describejobdefinitions", + json!({"jobDefinitionName": "jd"}), + )) + .await + .unwrap(), + ); + assert_eq!(d["jobDefinitions"].as_array().unwrap().len(), 0); + } + + #[tokio::test] + async fn every_described_queue_carries_compute_environment_order() { + let s = svc(); + mk_queue(&s, "plain").await; + let d = body_of( + s.handle(req("/v1/describejobqueues", json!({}))) + .await + .unwrap(), + ); + assert_eq!(d["jobQueues"][0]["computeEnvironmentOrder"], json!([])); + } + #[tokio::test] async fn tags_agree_across_describe_and_list_tags() { let s = svc(); @@ -3143,12 +3253,14 @@ mod tests { async fn list_tags_with_multibyte_arn_does_not_panic() { let s = svc(); // Raw multi-byte byte in the ARN path segment (what dispatch would pass - // through unescaped) must yield a normal response, not a dropped conn. + // through unescaped) must yield a normal ClientException (it is not a + // Batch ARN), not a dropped conn. let mut r = req("/v1/tags/%€", json!({})); r.method = Method::GET; - let resp = s.handle(r).await.unwrap(); - let v = body_of(resp); - assert!(v.get("tags").is_some()); + match s.handle(r).await { + Err(e) => assert!(format!("{e:?}").contains("ClientException")), + Ok(_) => panic!("a non-ARN resourceArn must be rejected"), + } } #[tokio::test] diff --git a/crates/fakecloud-conformance/src/audit.rs b/crates/fakecloud-conformance/src/audit.rs index 6ee4ed65b..7818be825 100644 --- a/crates/fakecloud-conformance/src/audit.rs +++ b/crates/fakecloud-conformance/src/audit.rs @@ -191,6 +191,7 @@ fn service_source_files(project_root: &Path) -> Vec { // together they cover the whole union, so either tag counts for both // mappings (same pattern as the bedrock control/data-plane crate). ("backup", "backup", &["service.rs"], &["backup"]), + ("batch", "batch", &["service.rs"], &["batch"]), ("ram", "ram", &["service.rs"], &["ram"]), ("s3tables", "s3tables", &["service.rs"], &["s3tables"]), ( diff --git a/crates/fakecloud-conformance/src/probe/service_detection.rs b/crates/fakecloud-conformance/src/probe/service_detection.rs index c16b7aeb3..1d9747e32 100644 --- a/crates/fakecloud-conformance/src/probe/service_detection.rs +++ b/crates/fakecloud-conformance/src/probe/service_detection.rs @@ -204,6 +204,8 @@ pub(super) fn service_protocol(service_name: &str) -> Protocol { "glacier" => Protocol::Rest, // AWS Backup: restJson1 control plane (@http traits + JSON bodies). "backup" => Protocol::Rest, + // AWS Batch: restJson1 control plane (@http POST URIs + JSON bodies). + "batch" => Protocol::Rest, // AWS Resource Access Manager: restJson1 control plane. "ram" => Protocol::Rest, // Amazon S3 Tables: restJson1 control plane (path-labelled URIs). diff --git a/crates/fakecloud-conformance/tests/batch.rs b/crates/fakecloud-conformance/tests/batch.rs new file mode 100644 index 000000000..387a2ba86 --- /dev/null +++ b/crates/fakecloud-conformance/tests/batch.rs @@ -0,0 +1,54 @@ +mod helpers; + +use fakecloud_conformance_macros::test_action; +use helpers::TestServer; + +#[test_action("batch", "CancelJob", checksum = "90317b1f")] +#[test_action("batch", "CreateComputeEnvironment", checksum = "17036471")] +#[test_action("batch", "CreateConsumableResource", checksum = "5b04885d")] +#[test_action("batch", "CreateJobQueue", checksum = "c011af7d")] +#[test_action("batch", "CreateQuotaShare", checksum = "6c246a6a")] +#[test_action("batch", "CreateSchedulingPolicy", checksum = "12721073")] +#[test_action("batch", "CreateServiceEnvironment", checksum = "04258095")] +#[test_action("batch", "DeleteComputeEnvironment", checksum = "8b5238e4")] +#[test_action("batch", "DeleteConsumableResource", checksum = "1e3cbcec")] +#[test_action("batch", "DeleteJobQueue", checksum = "ec1aef42")] +#[test_action("batch", "DeleteQuotaShare", checksum = "56724e81")] +#[test_action("batch", "DeleteSchedulingPolicy", checksum = "d3b59ce5")] +#[test_action("batch", "DeleteServiceEnvironment", checksum = "e4666c7f")] +#[test_action("batch", "DeregisterJobDefinition", checksum = "3e4cbd90")] +#[test_action("batch", "DescribeComputeEnvironments", checksum = "869b03be")] +#[test_action("batch", "DescribeConsumableResource", checksum = "3a2637f6")] +#[test_action("batch", "DescribeJobDefinitions", checksum = "bbb8837b")] +#[test_action("batch", "DescribeJobQueues", checksum = "e1b5f9f1")] +#[test_action("batch", "DescribeJobs", checksum = "3ae62030")] +#[test_action("batch", "DescribeQuotaShare", checksum = "23f482fb")] +#[test_action("batch", "DescribeSchedulingPolicies", checksum = "03205e4b")] +#[test_action("batch", "DescribeServiceEnvironments", checksum = "e024314e")] +#[test_action("batch", "DescribeServiceJob", checksum = "cdfbb3b2")] +#[test_action("batch", "GetJobQueueSnapshot", checksum = "4f1f56ce")] +#[test_action("batch", "ListConsumableResources", checksum = "d3b78fce")] +#[test_action("batch", "ListJobs", checksum = "06da1ccb")] +#[test_action("batch", "ListJobsByConsumableResource", checksum = "b8560f07")] +#[test_action("batch", "ListQuotaShares", checksum = "2ef7e367")] +#[test_action("batch", "ListSchedulingPolicies", checksum = "82d5be86")] +#[test_action("batch", "ListServiceJobs", checksum = "336eb324")] +#[test_action("batch", "ListTagsForResource", checksum = "bb4d12a2")] +#[test_action("batch", "RegisterJobDefinition", checksum = "bc804e45")] +#[test_action("batch", "SubmitJob", checksum = "63408c2c")] +#[test_action("batch", "SubmitServiceJob", checksum = "642aa8c3")] +#[test_action("batch", "TagResource", checksum = "35f49778")] +#[test_action("batch", "TerminateJob", checksum = "34801cd5")] +#[test_action("batch", "TerminateServiceJob", checksum = "d53ef2e8")] +#[test_action("batch", "UntagResource", checksum = "fbfb6fb7")] +#[test_action("batch", "UpdateComputeEnvironment", checksum = "d82a1bee")] +#[test_action("batch", "UpdateConsumableResource", checksum = "65195759")] +#[test_action("batch", "UpdateJobQueue", checksum = "98b3181b")] +#[test_action("batch", "UpdateQuotaShare", checksum = "01eae780")] +#[test_action("batch", "UpdateSchedulingPolicy", checksum = "64eb17d8")] +#[test_action("batch", "UpdateServiceEnvironment", checksum = "2f9a8af4")] +#[test_action("batch", "UpdateServiceJob", checksum = "ff7e7d3f")] +#[tokio::test] +async fn batch_probe() { + let _server = TestServer::start().await; +} diff --git a/website/content/docs/services/batch.md b/website/content/docs/services/batch.md index c117a504b..71e0f97df 100644 --- a/website/content/docs/services/batch.md +++ b/website/content/docs/services/batch.md @@ -14,6 +14,8 @@ compute does no real work. MiniStack's Batch jumps a job straight to Batch behind its Ultimate tier. fakecloud already runs ECS tasks as real containers, and Batch is built to run real jobs on that same engine. +fakecloud implements **45 of 45** AWS Batch operations at 100% Smithy conformance. + ## Supported today - **Compute environments** — `CreateComputeEnvironment`, `DescribeComputeEnvironments`, `UpdateComputeEnvironment`, `DeleteComputeEnvironment`. Created `VALID` / `ENABLED`. From 42bd9e9bad9a57baecb690594e2b1556eaa9b4d1 Mon Sep 17 00:00:00 2001 From: Lucas Vieira Date: Sat, 17 Oct 2026 15:04:24 -0300 Subject: [PATCH 2/7] fix(batch): stop the real task on TerminateJob; tag only existing resources Review follow-ups: - TerminateJob / CancelJob now stop the job's ECS task (and an array job's children), and the status poller leaves a job the user stopped terminal instead of flipping it back to RUNNING and then SUCCEEDED when the container exits 0. - Tag operations parse the label with the shared ARN helper (known partitions, any account format) and reject an ARN naming no Batch resource, so tags never attach to resources that do not exist; TagResource requires `tags`. - Required inputs share `required_str`; `computeEnvironmentOrder` is stored at create (API and CloudFormation) and on update. --- crates/fakecloud-batch/src/extended.rs | 23 +- crates/fakecloud-batch/src/lib.rs | 2 +- crates/fakecloud-batch/src/service.rs | 346 ++++++++++++------ .../src/resource_provisioner/batch.rs | 1 + .../tests/batch_real_execution.rs | 62 ++++ 5 files changed, 319 insertions(+), 115 deletions(-) diff --git a/crates/fakecloud-batch/src/extended.rs b/crates/fakecloud-batch/src/extended.rs index 2858d27ec..4cbb294fd 100644 --- a/crates/fakecloud-batch/src/extended.rs +++ b/crates/fakecloud-batch/src/extended.rs @@ -38,7 +38,7 @@ fn now_ms() -> i64 { chrono::Utc::now().timestamp_millis() } -fn required_str<'a>(body: &'a Value, key: &str) -> Result<&'a str, AwsServiceError> { +pub(crate) fn required_str<'a>(body: &'a Value, key: &str) -> Result<&'a str, AwsServiceError> { body.get(key) .and_then(Value::as_str) .filter(|s| !s.is_empty()) @@ -2100,6 +2100,23 @@ mod tests { } } + /// A deleted resource's tags are purged, and listing them is a + /// ClientException since the resource no longer exists. + async fn assert_deleted_tags_gone(s: &BatchService, arn: &str) { + assert!(!s + .state + .read() + .get("123456789012") + .unwrap() + .tags + .contains_key(arn)); + let path = format!("/v1/tags/{}", arn.replace('/', "%2F")); + match s.handle(req_with(Method::GET, &path, "", json!({}))).await { + Err(e) => assert_eq!(e.code(), "ClientException"), + Ok(_) => panic!("ListTagsForResource on deleted {arn} must be rejected"), + } + } + async fn list_tags(s: &BatchService, arn: &str) -> Value { // The SDK percent-encodes the ARN label (its `/` becomes %2F). let path = format!("/v1/tags/{}", arn.replace('/', "%2F")); @@ -2226,7 +2243,7 @@ mod tests { json!({"consumableResource": "licenses"}), ) .await; - assert_eq!(list_tags(&s, &arn).await, json!({})); + assert_deleted_tags_gone(&s, &arn).await; } #[tokio::test] @@ -2677,7 +2694,7 @@ mod tests { assert_eq!(d["capacityLimits"][0]["maxCapacity"], 8); ok(&s, "deletequotashare", json!({"quotaShareArn": arn})).await; client_err(&s, "describequotashare", json!({"quotaShareArn": arn})).await; - assert_eq!(list_tags(&s, &arn).await, json!({})); + assert_deleted_tags_gone(&s, &arn).await; } const PAYLOAD: &str = r#"{"TrainingJobName":"t","ResourceConfig":{"InstanceType":"ml.m5.large","InstanceCount":2,"VolumeSizeInGB":10}}"#; diff --git a/crates/fakecloud-batch/src/lib.rs b/crates/fakecloud-batch/src/lib.rs index 1ac3c7ade..d8ed10201 100644 --- a/crates/fakecloud-batch/src/lib.rs +++ b/crates/fakecloud-batch/src/lib.rs @@ -11,7 +11,7 @@ mod extended; pub mod service; pub mod state; -pub use service::{batch_arn, BatchService}; +pub use service::{batch_arn, normalize_compute_environment_order, BatchService}; pub use state::{ BatchAccounts, BatchSnapshot, BatchState, SharedBatchState, BATCH_SNAPSHOT_SCHEMA_VERSION, }; diff --git a/crates/fakecloud-batch/src/service.rs b/crates/fakecloud-batch/src/service.rs index d4ad1262e..d053e6e73 100644 --- a/crates/fakecloud-batch/src/service.rs +++ b/crates/fakecloud-batch/src/service.rs @@ -12,7 +12,7 @@ use fakecloud_aws::arn::Arn; use fakecloud_core::service::{AwsRequest, AwsResponse, AwsService, AwsServiceError}; use fakecloud_persistence::{SnapshotHook, SnapshotStore}; -use crate::state::{BatchSnapshot, SharedBatchState, BATCH_SNAPSHOT_SCHEMA_VERSION}; +use crate::state::{BatchSnapshot, BatchState, SharedBatchState, BATCH_SNAPSHOT_SCHEMA_VERSION}; const SUPPORTED_ACTIONS: &[&str] = &[ "CancelJob", @@ -312,8 +312,6 @@ impl BatchService { "DeleteSchedulingPolicy" => self.delete_scheduling_policy(req), "DescribeJobs" => self.describe_jobs(req), "ListJobs" => self.list_jobs(req), - "CancelJob" => self.cancel_job(req), - "TerminateJob" => self.terminate_job(req), "TagResource" => self.tag_resource(req), "UntagResource" => self.untag_resource(req), "ListTagsForResource" => self.list_tags_for_resource(req), @@ -351,28 +349,60 @@ pub(crate) fn client_error(code: &str, msg: impl Into) -> AwsServiceErro AwsServiceError::aws_error(StatusCode::BAD_REQUEST, code, msg.into()) } -/// The `{resourceArn}` label of the tag operations, which AWS rejects unless it -/// is a Batch ARN (`arn::batch:::`). -fn tag_resource_arn(arn: &str) -> Result { - if arn.is_empty() { - return Err(client_error("ClientException", "resourceArn is required")); - } - let parts: Vec<&str> = arn.splitn(6, ':').collect(); - let valid = parts.len() == 6 - && parts[0] == "arn" - && !parts[1].is_empty() - && parts[2] == "batch" - && !parts[3].is_empty() - && parts[4].len() == 12 - && parts[4].bytes().all(|b| b.is_ascii_digit()) - && !parts[5].is_empty(); - if !valid { +/// Every taggable Batch resource store, with the field holding its ARN. +fn taggable_stores(st: &BatchState) -> [(&crate::state::JsonStore, &str); 9] { + [ + (&st.compute_environments, "computeEnvironmentArn"), + (&st.job_queues, "jobQueueArn"), + (&st.job_definitions, "jobDefinitionArn"), + (&st.jobs, "jobArn"), + (&st.scheduling_policies, "arn"), + (&st.consumable_resources, "consumableResourceArn"), + (&st.service_environments, "serviceEnvironmentArn"), + (&st.quota_shares, "quotaShareArn"), + (&st.service_jobs, "jobArn"), + ] +} + +/// Resolve the `{resourceArn}` label of the tag operations. AWS rejects one +/// that is not a Batch ARN, or names no resource in the account, with +/// `ClientException`, so tags never attach to a resource that does not exist. +fn tagged_resource( + st: Option<&BatchState>, + label: Option<&String>, +) -> Result { + let arn = label.ok_or_else(|| client_error("ClientException", "resourceArn is required"))?; + if fakecloud_aws::arn::arn_resource(arn, "batch").is_none() { return Err(client_error( "ClientException", format!("Invalid resourceArn: {arn}"), )); } - Ok(arn.to_string()) + let exists = st.is_some_and(|st| { + taggable_stores(st).iter().any(|(store, key)| { + store + .values() + .any(|r| r.get(*key).and_then(Value::as_str) == Some(arn.as_str())) + }) + }); + if !exists { + return Err(client_error( + "ClientException", + format!("Resource {arn} does not exist"), + )); + } + Ok(arn.clone()) +} + +/// `computeEnvironmentOrder` is required on JobQueueDetail; a queue fed by +/// service environments carries it empty rather than absent or null. +pub fn normalize_compute_environment_order(queue: &mut Map) { + let order = queue + .entry("computeEnvironmentOrder") + .or_insert_with(|| json!([])); + if order.is_null() { + *order = json!([]); + } } pub(crate) type TagStore = @@ -555,6 +585,8 @@ impl BatchService { stored.insert("jobQueueArn".into(), json!(arn)); stored.insert("status".into(), json!("VALID")); stored.insert("statusReason".into(), json!("JobQueue Healthy")); + // Required on JobQueueDetail: empty for a service-environment queue. + normalize_compute_environment_order(&mut stored); stored .entry("state".to_string()) .or_insert_with(|| json!("ENABLED")); @@ -604,15 +636,9 @@ impl BatchService { .filter(|q| match_named(q, &wanted, "jobQueueName", "jobQueueArn")) .map(|q| { let mut q = merge_tag_overlay(q, "jobQueueArn", &st.tags); - // `computeEnvironmentOrder` is required on JobQueueDetail: a - // queue fed by service environments reports it empty. + // Queues stored before creates normalized the field. if let Some(o) = q.as_object_mut() { - let order = o - .entry("computeEnvironmentOrder") - .or_insert_with(|| json!([])); - if order.is_null() { - *order = json!([]); - } + normalize_compute_environment_order(o); } q }) @@ -642,17 +668,14 @@ impl BatchService { .and_then(Value::as_str) .ok_or_else(|| client_error("ClientException", "jobDefinitionName is required"))? .to_string(); - match body.get("type").and_then(Value::as_str) { - Some("container" | "multinode") => {} - Some(other) => { - return Err(client_error( - "ClientException", - format!( - "Invalid job definition type: {other}. Valid values: container, multinode" - ), - )) - } - None => return Err(client_error("ClientException", "type is required")), + let job_type = crate::extended::required_str(&body, "type")?; + if !matches!(job_type, "container" | "multinode") { + return Err(client_error( + "ClientException", + format!( + "Invalid job definition type: {job_type}. Valid values: container, multinode" + ), + )); } let mut accounts = self.state.write(); let st = accounts.get_or_create(&req.account_id); @@ -865,6 +888,9 @@ impl BatchService { ], "jobQueueArn", ); + if let Some(o) = q.as_object_mut() { + normalize_compute_environment_order(o); + } Ok(AwsResponse::ok_json(json!({ "jobQueueName": name, "jobQueueArn": arn, @@ -1394,73 +1420,78 @@ impl BatchService { Ok(AwsResponse::ok_json(Value::Object(resp))) } - fn cancel_job(&self, req: &AwsRequest) -> Result { - self.stop_job(req, &["SUBMITTED", "PENDING", "RUNNABLE"], "CancelJob") - } - - fn terminate_job(&self, req: &AwsRequest) -> Result { - self.stop_job( - req, - &["SUBMITTED", "PENDING", "RUNNABLE", "STARTING", "RUNNING"], - "TerminateJob", - ) - } - - fn stop_job( - &self, - req: &AwsRequest, - cancelable: &[&str], - op: &str, - ) -> Result { + /// `CancelJob` stops jobs that have not started; `TerminateJob` also stops + /// `STARTING` / `RUNNING` ones. Either applies to an array job's children. + async fn stop_job(&self, req: &AwsRequest, op: &str) -> Result { + let stoppable: &[&str] = if op == "TerminateJob" { + &["SUBMITTED", "PENDING", "RUNNABLE", "STARTING", "RUNNING"] + } else { + &["SUBMITTED", "PENDING", "RUNNABLE"] + }; let body = req.json_body(); - let job_id = body - .get("jobId") - .and_then(Value::as_str) - .ok_or_else(|| client_error("ClientException", "jobId is required"))? - .to_string(); - let reason = body - .get("reason") - .and_then(Value::as_str) - .ok_or_else(|| client_error("ClientException", format!("reason is required for {op}")))? - .to_string(); - let mut accounts = self.state.write(); - if let Some(job) = accounts - .get_or_create(&req.account_id) - .jobs - .get_mut(&job_id) + let job_id = crate::extended::required_str(&body, "jobId")?.to_string(); + let reason = crate::extended::required_str(&body, "reason")?.to_string(); + let child_prefix = format!("{job_id}:"); + // The real ECS tasks of the jobs being stopped: (cluster, task ARN). + let mut tasks = Vec::new(); { - if let Some(o) = job.as_object_mut() { + let mut accounts = self.state.write(); + let jobs = &mut accounts.get_or_create(&req.account_id).jobs; + for (id, job) in jobs.iter_mut() { + if *id != job_id && !id.starts_with(&child_prefix) { + continue; + } + let Some(o) = job.as_object_mut() else { + continue; + }; let cur = o.get("status").and_then(Value::as_str).unwrap_or(""); - if cancelable.contains(&cur) { - o.insert("status".into(), json!("FAILED")); - o.insert("statusReason".into(), json!(reason)); + if !stoppable.contains(&cur) { + continue; + } + o.insert("status".into(), json!("FAILED")); + o.insert("statusReason".into(), json!(reason)); + o.insert( + "stoppedAt".into(), + json!(chrono::Utc::now().timestamp_millis()), + ); + if let (Some(cluster), Some(task)) = ( + o.get("ecsCluster").and_then(Value::as_str), + o.get("ecsTaskArn").and_then(Value::as_str), + ) { + tasks.push((cluster.to_string(), task.to_string())); } } } + // Stop the containers too, so a terminated job does not keep running. + // The status poller sees the job already terminal and leaves it FAILED. + if let (Some(ecs), false) = (self.ecs.as_ref(), tasks.is_empty()) { + for (cluster, task) in tasks { + let _ = ecs + .handle(ecs_request( + "StopTask", + json!({ "cluster": cluster, "task": task, "reason": reason }), + req, + )) + .await; + } + ecs.persist().await; + } Ok(AwsResponse::ok_json(json!({}))) } // ---- Tags ---- fn tag_resource(&self, req: &AwsRequest) -> Result { - let arn = req - .path_segments - .get(2) - .map(String::as_str) - .map(tag_resource_arn) - .unwrap_or_else(|| Err(client_error("ClientException", "resourceArn is required")))?; let body = req.json_body(); let tags = body .get("tags") .and_then(Value::as_object) .cloned() - .unwrap_or_default(); + .ok_or_else(|| client_error("ClientException", "tags is required"))?; let mut accounts = self.state.write(); - let entry = accounts - .get_or_create(&req.account_id) - .tags - .entry(arn) - .or_default(); + let st = accounts.get_or_create(&req.account_id); + let arn = tagged_resource(Some(st), req.path_segments.get(2))?; + let entry = st.tags.entry(arn).or_default(); for (k, v) in tags { if let Some(s) = v.as_str() { entry.insert(k, s.to_string()); @@ -1470,12 +1501,6 @@ impl BatchService { } fn untag_resource(&self, req: &AwsRequest) -> Result { - let arn = req - .path_segments - .get(2) - .map(String::as_str) - .map(tag_resource_arn) - .unwrap_or_else(|| Err(client_error("ClientException", "resourceArn is required")))?; // `tagKeys` is an `@httpQuery` list sent as repeated `tagKeys=a&tagKeys=b` // pairs; `query_params` collapses repeats to the last value, so parse the // raw query string for every occurrence, percent-decoding each. @@ -1486,7 +1511,9 @@ impl BatchService { .map(fakecloud_core::path::percent_decode_segment) .collect(); let mut accounts = self.state.write(); - if let Some(entry) = accounts.get_or_create(&req.account_id).tags.get_mut(&arn) { + let st = accounts.get_or_create(&req.account_id); + let arn = tagged_resource(Some(st), req.path_segments.get(2))?; + if let Some(entry) = st.tags.get_mut(&arn) { for k in keys { entry.remove(&k); } @@ -1495,15 +1522,10 @@ impl BatchService { } fn list_tags_for_resource(&self, req: &AwsRequest) -> Result { - let arn = req - .path_segments - .get(2) - .map(String::as_str) - .map(tag_resource_arn) - .unwrap_or_else(|| Err(client_error("ClientException", "resourceArn is required")))?; let accounts = self.state.read(); - let tags = accounts - .get(&req.account_id) + let st = accounts.get(&req.account_id); + let arn = tagged_resource(st, req.path_segments.get(2))?; + let tags = st .and_then(|st| st.tags.get(&arn)) .map(|m| { m.iter() @@ -2021,6 +2043,9 @@ fn spawn_status_sync( let mut succeeded = false; for _ in 0..max_polls { tokio::time::sleep(std::time::Duration::from_secs(1)).await; + if job_is_terminal(&batch_state, &account_id, &job_id) { + return; + } let resp = match ecs .handle(ecs_request( "DescribeTasks", @@ -2045,7 +2070,10 @@ fn spawn_status_sync( .get_mut(&job_id) .and_then(|j| j.as_object_mut()) { - if j.get("status").and_then(Value::as_str) != Some("RUNNING") { + if matches!( + j.get("status").and_then(Value::as_str), + Some("STARTING" | "RUNNABLE") + ) { j.insert("status".into(), json!("RUNNING")); } } @@ -2068,6 +2096,10 @@ fn spawn_status_sync( } } + // Cancelled or terminated while this attempt ran: keep that outcome. + if job_is_terminal(&batch_state, &account_id, &job_id) { + return; + } if succeeded { set_job_terminal( &batch_state, @@ -2109,7 +2141,9 @@ fn spawn_status_sync( // released when the attempt failed, so it must be admitted // again (atomically with the other waiters) before relaunch, // and waits at RUNNABLE while capacity is held by others. - if !wait_for_retry_admission(&batch_state, &account_id, &job_id).await { + if !wait_for_retry_admission(&batch_state, &account_id, &job_id).await + || job_is_terminal(&batch_state, &account_id, &job_id) + { break; } let relaunched = launch_ecs_task(&ecs, &src, &job_id, &job_name, &container).await; @@ -2160,6 +2194,18 @@ fn spawn_status_sync( } /// Write a job's terminal status + `container.exitCode`/reason + `stoppedAt`. +/// True once the job reached `SUCCEEDED` / `FAILED`, including through +/// `CancelJob` / `TerminateJob` while its task was still being polled. +fn job_is_terminal(batch_state: &SharedBatchState, account_id: &str, job_id: &str) -> bool { + batch_state + .read() + .get(account_id) + .and_then(|st| st.jobs.get(job_id)) + .and_then(|j| j.get("status")) + .and_then(Value::as_str) + .is_some_and(|s| matches!(s, "SUCCEEDED" | "FAILED")) +} + fn set_job_terminal( batch_state: &SharedBatchState, account_id: &str, @@ -2411,12 +2457,12 @@ impl AwsService for BatchService { format!("Unknown operation: {} {}", req.method, req.raw_path), )); }; - // SubmitJob is async (it launches a real ECS task); everything else is - // a synchronous metadata op. - let result = if action == "SubmitJob" { - self.submit_job(&req).await - } else { - self.dispatch(action, &req) + // SubmitJob launches a real ECS task and Cancel/TerminateJob stop them, + // so they are async; everything else is a synchronous metadata op. + let result = match action { + "SubmitJob" => self.submit_job(&req).await, + "CancelJob" | "TerminateJob" => self.stop_job(&req, action).await, + _ => self.dispatch(action, &req), }; if MUTATING_ACTIONS.contains(&action) && matches!(result.as_ref(), Ok(resp) if resp.status.is_success()) @@ -2638,6 +2684,84 @@ mod tests { assert_eq!(d["jobDefinitions"].as_array().unwrap().len(), 0); } + #[tokio::test] + async fn tags_attach_only_to_existing_batch_resources() { + let s = svc(); + let q = body_of( + s.handle(req( + "/v1/createjobqueue", + json!({"jobQueueName": "q", "priority": 1}), + )) + .await + .unwrap(), + ); + let arn = q["jobQueueArn"].as_str().unwrap().to_string(); + let path = format!("/v1/tags/{}", arn.replace('/', "%2F")); + let missing = "arn:aws:batch:us-east-1:123456789012:job-queue/nope"; + let missing_path = format!("/v1/tags/{}", missing.replace('/', "%2F")); + let bad_partition = "/v1/tags/arn:bogus:batch:us-east-1:123456789012:job-queue%2Fq"; + + // `tags` is required; a well-formed ARN of nothing, or a non-Batch ARN, + // is rejected; nothing is recorded for any of them. + for (p, body) in [ + (path.as_str(), json!({})), + (missing_path.as_str(), json!({"tags": {"k": "v"}})), + (bad_partition, json!({"tags": {"k": "v"}})), + ] { + match s.handle(req(p, body)).await { + Err(e) => assert!(format!("{e:?}").contains("ClientException"), "{p}"), + Ok(_) => panic!("TagResource {p} must be rejected"), + } + } + assert!(!s + .state + .read() + .get("123456789012") + .unwrap() + .tags + .contains_key(missing)); + + s.handle(req(&path, json!({"tags": {"k": "v"}}))) + .await + .unwrap(); + let mut lt = req(&path, json!({})); + lt.method = Method::GET; + assert_eq!(body_of(s.handle(lt).await.unwrap())["tags"]["k"], "v"); + } + + #[tokio::test] + async fn terminating_an_array_job_stops_every_child() { + let s = svc(); + mk_queue(&s, "q").await; + let sub = body_of( + s.handle(req( + "/v1/submitjob", + json!({"jobName": "arr", "jobQueue": "q", "jobDefinition": "jd:1", + "arrayProperties": {"size": 2}}), + )) + .await + .unwrap(), + ); + let id = sub["jobId"].as_str().unwrap().to_string(); + s.handle(req( + "/v1/terminatejob", + json!({"jobId": id, "reason": "user stop"}), + )) + .await + .unwrap(); + let accounts = s.state.read(); + let jobs = &accounts.get("123456789012").unwrap().jobs; + for job_id in [id.clone(), format!("{id}:0"), format!("{id}:1")] { + let j = &jobs[&job_id]; + assert_eq!(j["status"], "FAILED", "{job_id}"); + assert_eq!(j["statusReason"], "user stop", "{job_id}"); + assert!(j["stoppedAt"].is_i64(), "{job_id}"); + } + drop(accounts); + // The status poller treats the job as finished and never revives it. + assert!(job_is_terminal(&s.state, "123456789012", &id)); + } + #[tokio::test] async fn every_described_queue_carries_compute_environment_order() { let s = svc(); diff --git a/crates/fakecloud-cloudformation/src/resource_provisioner/batch.rs b/crates/fakecloud-cloudformation/src/resource_provisioner/batch.rs index 8bfae86be..df95df6a3 100644 --- a/crates/fakecloud-cloudformation/src/resource_provisioner/batch.rs +++ b/crates/fakecloud-cloudformation/src/resource_provisioner/batch.rs @@ -132,6 +132,7 @@ impl ResourceProvisioner { ] { copy_prop(&mut stored, props, cfn, api); } + fakecloud_batch::normalize_compute_environment_order(&mut stored); self.batch_state .write() .get_or_create(&self.account_id) diff --git a/crates/fakecloud-e2e/tests/batch_real_execution.rs b/crates/fakecloud-e2e/tests/batch_real_execution.rs index 186989593..2971b6ebb 100644 --- a/crates/fakecloud-e2e/tests/batch_real_execution.rs +++ b/crates/fakecloud-e2e/tests/batch_real_execution.rs @@ -405,3 +405,65 @@ async fn restart_fails_in_flight_job_instead_of_zombie() { Some("Job interrupted by a fakecloud restart") ); } + +#[tokio::test] +async fn terminate_job_stops_the_running_container() { + if !require_docker_or_skip("terminate_job_stops_the_running_container") { + return; + } + let s = TestServer::start().await; + let config = s.aws_config().await; + let batch = aws_sdk_batch::Client::new(&config); + let ecs = aws_sdk_ecs::Client::new(&config); + // Long enough to be terminated mid-run, short enough that a container left + // running would exit 0 (and flip the job to SUCCEEDED) within the test. + let job_id = run_job(&batch, "term", vec!["sh", "-c", "sleep 15"]).await; + + let mut running = false; + for _ in 0..120 { + let d = batch.describe_jobs().jobs(&job_id).send().await.unwrap(); + if d.jobs()[0].status().map(|s| s.as_str()) == Some("RUNNING") { + running = true; + break; + } + tokio::time::sleep(Duration::from_secs(1)).await; + } + assert!( + running, + "job never reached RUNNING\n{}", + describe_for_diagnostics(&batch, &job_id).await + ); + + batch + .terminate_job() + .job_id(&job_id) + .reason("user stop") + .send() + .await + .expect("terminate job"); + + // The task is stopped, not left running behind a FAILED job. + let live = ecs + .list_tasks() + .cluster("fakecloud-batch") + .desired_status(aws_sdk_ecs::types::DesiredStatus::Running) + .send() + .await + .unwrap(); + assert!(live.task_arns().is_empty(), "{:?}", live.task_arns()); + + // Past the point the container would have exited 0, the job is still the + // FAILED the user asked for: the status poller does not revive it. + for _ in 0..20 { + let d = batch.describe_jobs().jobs(&job_id).send().await.unwrap(); + let job = &d.jobs()[0]; + assert_eq!( + job.status().map(|s| s.as_str()), + Some("FAILED"), + "{}", + job_failure_detail(job) + ); + assert_eq!(job.status_reason(), Some("user stop")); + tokio::time::sleep(Duration::from_secs(1)).await; + } +} From 5762641b85956f97348c6a75532ac16ded389040 Mon Sep 17 00:00:00 2001 From: Lucas Vieira Date: Sat, 17 Oct 2026 15:08:30 -0300 Subject: [PATCH 3/7] fix(batch): never revive a stopped job from a background status writer Every async status writer (launch, retry relaunch, poll, dependency and consumable admission, terminal write) now goes through live_job under the write lock, so a CancelJob/TerminateJob that races a launch or a container exit is never undone; a task launched for a job stopped meanwhile is stopped, and the poller stops its own task when the job was stopped (a retry for a failed StopTask). TerminateJob stops tasks concurrently and accepts an empty reason. Deleting a compute environment, job queue or scheduling policy drops its tags. --- Cargo.lock | 1 + crates/fakecloud-batch/Cargo.toml | 1 + crates/fakecloud-batch/src/extended.rs | 8 + crates/fakecloud-batch/src/service.rs | 256 ++++++++++++++++++------- 4 files changed, 193 insertions(+), 73 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 78bd4ffba..6e7ae5731 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -4840,6 +4840,7 @@ dependencies = [ "serde", "serde_json", "tokio", + "tracing", "uuid", ] diff --git a/crates/fakecloud-batch/Cargo.toml b/crates/fakecloud-batch/Cargo.toml index 02036c1a5..6644b1e6a 100644 --- a/crates/fakecloud-batch/Cargo.toml +++ b/crates/fakecloud-batch/Cargo.toml @@ -20,6 +20,7 @@ parking_lot = { workspace = true } serde = { workspace = true } serde_json = { workspace = true } tokio = { workspace = true } +tracing = { workspace = true } uuid = { workspace = true } [dev-dependencies] diff --git a/crates/fakecloud-batch/src/extended.rs b/crates/fakecloud-batch/src/extended.rs index 4cbb294fd..7fab13a78 100644 --- a/crates/fakecloud-batch/src/extended.rs +++ b/crates/fakecloud-batch/src/extended.rs @@ -238,6 +238,8 @@ pub(crate) enum Admission { Admitted, /// At least one requirement does not fit right now: wait at RUNNABLE. Wait, + /// The job was cancelled or terminated (or finished): never start it. + Stopped, } /// Atomically admit a job against its consumable resources. On success with @@ -248,6 +250,12 @@ pub(crate) fn admit_job(st: &mut BatchState, job_id: &str) -> Admission { let Some(job) = st.jobs.get(job_id) else { return Admission::Admitted; }; + if matches!( + job.get("status").and_then(Value::as_str), + Some("SUCCEEDED" | "FAILED") + ) { + return Admission::Stopped; + } let reqs = job_consumable_requirements(job); if reqs.is_empty() { return Admission::Admitted; diff --git a/crates/fakecloud-batch/src/service.rs b/crates/fakecloud-batch/src/service.rs index d053e6e73..2735cd935 100644 --- a/crates/fakecloud-batch/src/service.rs +++ b/crates/fakecloud-batch/src/service.rs @@ -564,10 +564,15 @@ impl BatchService { let body = req.json_body(); let name = arn_or_name(&body, "computeEnvironment")?; let mut accounts = self.state.write(); - accounts - .get_or_create(&req.account_id) - .compute_environments - .remove(&name); + let st = accounts.get_or_create(&req.account_id); + // Its tags go with it: a later resource of the same name starts untagged. + if let Some(arn) = st.compute_environments.remove(&name).and_then(|r| { + r.get("computeEnvironmentArn") + .and_then(Value::as_str) + .map(String::from) + }) { + st.tags.remove(&arn); + } Ok(AwsResponse::ok_json(json!({}))) } @@ -652,10 +657,15 @@ impl BatchService { let body = req.json_body(); let name = arn_or_name(&body, "jobQueue")?; let mut accounts = self.state.write(); - accounts - .get_or_create(&req.account_id) - .job_queues - .remove(&name); + let st = accounts.get_or_create(&req.account_id); + // Its tags go with it: a later resource of the same name starts untagged. + if let Some(arn) = st.job_queues.remove(&name).and_then(|r| { + r.get("jobQueueArn") + .and_then(Value::as_str) + .map(String::from) + }) { + st.tags.remove(&arn); + } Ok(AwsResponse::ok_json(json!({}))) } @@ -984,10 +994,15 @@ impl BatchService { let body = req.json_body(); let name = arn_or_name(&body, "arn")?; let mut accounts = self.state.write(); - accounts - .get_or_create(&req.account_id) + let st = accounts.get_or_create(&req.account_id); + // Its tags go with it: a later resource of the same name starts untagged. + if let Some(arn) = st .scheduling_policies - .remove(&name); + .remove(&name) + .and_then(|r| r.get("arn").and_then(Value::as_str).map(String::from)) + { + st.tags.remove(&arn); + } Ok(AwsResponse::ok_json(json!({}))) } @@ -1430,7 +1445,11 @@ impl BatchService { }; let body = req.json_body(); let job_id = crate::extended::required_str(&body, "jobId")?.to_string(); - let reason = crate::extended::required_str(&body, "reason")?.to_string(); + let reason = body + .get("reason") + .and_then(Value::as_str) + .ok_or_else(|| client_error("ClientException", format!("reason is required for {op}")))? + .to_string(); let child_prefix = format!("{job_id}:"); // The real ECS tasks of the jobs being stopped: (cluster, task ARN). let mut tasks = Vec::new(); @@ -1465,15 +1484,13 @@ impl BatchService { // Stop the containers too, so a terminated job does not keep running. // The status poller sees the job already terminal and leaves it FAILED. if let (Some(ecs), false) = (self.ecs.as_ref(), tasks.is_empty()) { + let mut stops = tokio::task::JoinSet::new(); for (cluster, task) in tasks { - let _ = ecs - .handle(ecs_request( - "StopTask", - json!({ "cluster": cluster, "task": task, "reason": reason }), - req, - )) - .await; + let ecs = ecs.clone(); + let src = bare_request(&req.account_id, &req.region, &req.request_id); + stops.spawn(async move { stop_ecs_task(&ecs, &src, &cluster, &task).await }); } + while stops.join_next().await.is_some() {} ecs.persist().await; } Ok(AwsResponse::ok_json(json!({}))) @@ -1592,6 +1609,9 @@ async fn launch( let mut accounts = ctx.batch_state.write(); crate::extended::admit_job(accounts.get_or_create(account), job_id) }; + if let crate::extended::Admission::Stopped = admission { + return; + } if let crate::extended::Admission::Wait = admission { spawn_consumable_waiter( ctx.clone(), @@ -1712,19 +1732,23 @@ async fn launch_now( let src = bare_request(account, region, request_id); match launch_ecs_task(&ecs, &src, job_id, job_name, &container).await { Ok((cluster, task_arn)) => { - { + let recorded = { let mut accounts = ctx.batch_state.write(); - if let Some(j) = accounts - .get_or_create(account) - .jobs - .get_mut(job_id) - .and_then(|j| j.as_object_mut()) - { - j.insert("status".into(), json!("STARTING")); - j.insert("ecsCluster".into(), json!(cluster)); - j.insert("ecsTaskArn".into(), json!(task_arn)); - j.insert("startedAt".into(), json!(now)); + match live_job(accounts.get_or_create(account), job_id) { + Some(j) => { + j.insert("status".into(), json!("STARTING")); + j.insert("ecsCluster".into(), json!(cluster)); + j.insert("ecsTaskArn".into(), json!(task_arn)); + j.insert("startedAt".into(), json!(now)); + true + } + None => false, } + }; + if !recorded { + // Cancelled or terminated while the task launched: it must not run. + stop_ecs_task(&ecs, &src, &cluster, &task_arn).await; + return; } spawn_status_sync( ctx, @@ -1741,12 +1765,7 @@ async fn launch_now( } Err(err) => { let mut accounts = ctx.batch_state.write(); - if let Some(j) = accounts - .get_or_create(account) - .jobs - .get_mut(job_id) - .and_then(|j| j.as_object_mut()) - { + if let Some(j) = live_job(accounts.get_or_create(account), job_id) { j.insert("status".into(), json!("FAILED")); j.insert("statusReason".into(), json!(err.message().to_string())); } @@ -1801,12 +1820,7 @@ fn spawn_dependency_waiter( if statuses.iter().any(|s| s == "FAILED") { { let mut accounts = ctx.batch_state.write(); - if let Some(j) = accounts - .get_or_create(&account) - .jobs - .get_mut(&job_id) - .and_then(|j| j.as_object_mut()) - { + if let Some(j) = live_job(accounts.get_or_create(&account), &job_id) { j.insert("status".into(), json!("FAILED")); j.insert("statusReason".into(), json!("Dependent job failed")); } @@ -2044,6 +2058,9 @@ fn spawn_status_sync( for _ in 0..max_polls { tokio::time::sleep(std::time::Duration::from_secs(1)).await; if job_is_terminal(&batch_state, &account_id, &job_id) { + // Stopped by CancelJob / TerminateJob. Stop this task too in + // case the handler's StopTask failed or missed it. + stop_ecs_task(&ecs, &src, &cluster, &task).await; return; } let resp = match ecs @@ -2064,12 +2081,7 @@ fn spawn_status_sync( match t.get("lastStatus").and_then(Value::as_str).unwrap_or("") { "RUNNING" => { let mut accounts = batch_state.write(); - if let Some(j) = accounts - .get_or_create(&account_id) - .jobs - .get_mut(&job_id) - .and_then(|j| j.as_object_mut()) - { + if let Some(j) = live_job(accounts.get_or_create(&account_id), &job_id) { if matches!( j.get("status").and_then(Value::as_str), Some("STARTING" | "RUNNABLE") @@ -2098,6 +2110,7 @@ fn spawn_status_sync( // Cancelled or terminated while this attempt ran: keep that outcome. if job_is_terminal(&batch_state, &account_id, &job_id) { + stop_ecs_task(&ecs, &src, &cluster, &task).await; return; } if succeeded { @@ -2120,12 +2133,7 @@ fn spawn_status_sync( // Record the failed attempt and re-launch a fresh task. { let mut accounts = batch_state.write(); - if let Some(j) = accounts - .get_or_create(&account_id) - .jobs - .get_mut(&job_id) - .and_then(|j| j.as_object_mut()) - { + if let Some(j) = live_job(accounts.get_or_create(&account_id), &job_id) { let attempts = j.entry("attempts".to_string()).or_insert_with(|| json!([])); if let Some(a) = attempts.as_array_mut() { a.push(json!({ @@ -2141,9 +2149,7 @@ fn spawn_status_sync( // released when the attempt failed, so it must be admitted // again (atomically with the other waiters) before relaunch, // and waits at RUNNABLE while capacity is held by others. - if !wait_for_retry_admission(&batch_state, &account_id, &job_id).await - || job_is_terminal(&batch_state, &account_id, &job_id) - { + if !wait_for_retry_admission(&batch_state, &account_id, &job_id).await { break; } let relaunched = launch_ecs_task(&ecs, &src, &job_id, &job_name, &container).await; @@ -2152,15 +2158,21 @@ fn spawn_status_sync( Ok((_, new_task)) => { task = new_task; attempt += 1; - let mut accounts = batch_state.write(); - if let Some(j) = accounts - .get_or_create(&account_id) - .jobs - .get_mut(&job_id) - .and_then(|j| j.as_object_mut()) - { - j.insert("status".into(), json!("STARTING")); - j.insert("ecsTaskArn".into(), json!(task)); + let recorded = { + let mut accounts = batch_state.write(); + match live_job(accounts.get_or_create(&account_id), &job_id) { + Some(j) => { + j.insert("status".into(), json!("STARTING")); + j.insert("ecsTaskArn".into(), json!(task)); + true + } + None => false, + } + }; + if !recorded { + // Stopped while this attempt launched: it must not run. + stop_ecs_task(&ecs, &src, &cluster, &task).await; + return; } continue; } @@ -2193,7 +2205,6 @@ fn spawn_status_sync( }); } -/// Write a job's terminal status + `container.exitCode`/reason + `stoppedAt`. /// True once the job reached `SUCCEEDED` / `FAILED`, including through /// `CancelJob` / `TerminateJob` while its task was still being polled. fn job_is_terminal(batch_state: &SharedBatchState, account_id: &str, job_id: &str) -> bool { @@ -2206,6 +2217,47 @@ fn job_is_terminal(batch_state: &SharedBatchState, account_id: &str, job_id: &st .is_some_and(|s| matches!(s, "SUCCEEDED" | "FAILED")) } +/// The job's record while it can still change state, or `None` once it is +/// `SUCCEEDED` / `FAILED`. Every background status writer goes through this +/// under the write lock, so a `CancelJob` / `TerminateJob` (or a finished +/// attempt) is never overwritten by a launch, poll or retry still in flight. +pub(crate) fn live_job<'a>( + st: &'a mut BatchState, + job_id: &str, +) -> Option<&'a mut Map> { + st.jobs + .get_mut(job_id) + .and_then(Value::as_object_mut) + .filter(|j| { + !matches!( + j.get("status").and_then(Value::as_str), + Some("SUCCEEDED" | "FAILED") + ) + }) +} + +/// Stop an ECS task Batch launched. Best-effort: a failure is logged, and the +/// status poller retries it for a task whose job was stopped. +async fn stop_ecs_task( + ecs: &fakecloud_ecs::EcsService, + src: &AwsRequest, + cluster: &str, + task: &str, +) { + if let Err(e) = ecs + .handle(ecs_request( + "StopTask", + json!({ "cluster": cluster, "task": task, "reason": "Batch job stopped" }), + src, + )) + .await + { + tracing::warn!(task, error = %e.message(), "failed to stop the ECS task of a stopped Batch job"); + } +} + +/// Write a job's terminal status + `container.exitCode`/reason + `stoppedAt`, +/// unless it is already terminal (a user stop that raced the task's exit wins). fn set_job_terminal( batch_state: &SharedBatchState, account_id: &str, @@ -2215,12 +2267,7 @@ fn set_job_terminal( reason: Option, ) { let mut accounts = batch_state.write(); - if let Some(j) = accounts - .get_or_create(account_id) - .jobs - .get_mut(job_id) - .and_then(|j| j.as_object_mut()) - { + if let Some(j) = live_job(accounts.get_or_create(account_id), job_id) { j.insert("status".into(), json!(status)); if let Some(c) = exit_code { let container = j @@ -2762,6 +2809,69 @@ mod tests { assert!(job_is_terminal(&s.state, "123456789012", &id)); } + #[tokio::test] + async fn a_stopped_job_is_never_revived_by_background_writers() { + let s = svc(); + mk_queue(&s, "q").await; + let sub = body_of( + s.handle(req( + "/v1/submitjob", + json!({"jobName": "j", "jobQueue": "q", "jobDefinition": "jd:1"}), + )) + .await + .unwrap(), + ); + let id = sub["jobId"].as_str().unwrap().to_string(); + s.handle(req("/v1/canceljob", json!({"jobId": id, "reason": ""}))) + .await + .unwrap(); + // A container exit racing the cancel does not overwrite it, and a + // launch waiting on admission (dependsOn, consumables) never starts it. + set_job_terminal(&s.state, "123456789012", &id, "SUCCEEDED", Some(0), None); + let mut accounts = s.state.write(); + let st = accounts.get_or_create("123456789012"); + assert!(matches!( + crate::extended::admit_job(st, &id), + crate::extended::Admission::Stopped + )); + assert!(live_job(st, &id).is_none()); + assert_eq!(st.jobs[&id]["status"], "FAILED"); + assert_eq!(st.jobs[&id]["statusReason"], ""); + } + + #[tokio::test] + async fn deleting_a_resource_drops_its_tags() { + let s = svc(); + let make = |name: &str| { + req( + "/v1/createjobqueue", + json!({"jobQueueName": name, "priority": 1, "tags": {"env": "prod"}}), + ) + }; + let q = body_of(s.handle(make("q")).await.unwrap()); + let arn = q["jobQueueArn"].as_str().unwrap().to_string(); + s.handle(req("/v1/deletejobqueue", json!({"jobQueue": "q"}))) + .await + .unwrap(); + assert!(!s + .state + .read() + .get("123456789012") + .unwrap() + .tags + .contains_key(&arn)); + // Re-created under the same name (same ARN), it starts untagged. + s.handle(req( + "/v1/createjobqueue", + json!({"jobQueueName": "q", "priority": 1}), + )) + .await + .unwrap(); + let mut lt = req(&format!("/v1/tags/{}", arn.replace('/', "%2F")), json!({})); + lt.method = Method::GET; + assert_eq!(body_of(s.handle(lt).await.unwrap())["tags"], json!({})); + } + #[tokio::test] async fn every_described_queue_carries_compute_environment_order() { let s = svc(); From bbfe800261cbd6132a53a07d77764ec2092c85e8 Mon Sep 17 00:00:00 2001 From: Lucas Vieira Date: Sat, 17 Oct 2026 15:15:41 -0300 Subject: [PATCH 4/7] docs: add AWS Batch to the parity matrix and service lists (107 services, 7,657 ops) Batch row (45 ops) and entries in supported-services, the services index and llms*.txt; headline counts cascade to 107 services, 7,657 operations and 258,545/258,545 variants. PENDING_PARITY_ROW is now empty, and its loop expands safely when empty under set -u on bash < 4.4. --- AGENTS.md | 2 +- README.md | 6 +++--- conformance-baseline-notes.md | 2 +- crates/fakecloud-conformance/README.md | 2 +- scripts/check-doc-counts.sh | 9 ++------- website/content/docs/about/conformance.md | 4 ++-- website/content/docs/about/what-it-is.md | 6 +++--- .../content/docs/migration-from-localstack.md | 2 +- website/content/docs/parity.md | 5 +++-- website/content/docs/services/_index.md | 3 ++- website/content/dynamodb-emulator.md | 2 +- website/content/fake-aws-server.md | 2 +- website/content/fake-bedrock.md | 2 +- website/content/faq.md | 16 ++++++++-------- website/content/glossary.md | 2 +- website/content/localstack-alternative.md | 14 +++++++------- website/content/supported-services.md | 11 ++++++----- website/content/vs/dynamodb-local.md | 4 ++-- website/content/vs/elasticmq.md | 6 +++--- website/content/vs/floci.md | 6 +++--- website/content/vs/localstack.md | 4 ++-- website/content/vs/minio.md | 4 ++-- website/content/vs/ministack.md | 6 +++--- website/content/vs/moto.md | 4 ++-- website/content/vs/s3mock.md | 6 +++--- website/content/vs/sam-local.md | 4 ++-- website/static/llms-full.txt | 16 ++++++++++++---- website/static/llms.txt | 5 +++-- website/templates/index.html | 10 +++++----- 29 files changed, 86 insertions(+), 79 deletions(-) diff --git a/AGENTS.md b/AGENTS.md index 34989bdb9..f391992e6 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -5,7 +5,7 @@ Local AWS cloud emulator. Part of the faisca project family. ## Product Context - FakeCloud is a local AWS emulator focused on high-fidelity behavior and AWS-compatible responses. -- Current project state: 106 AWS services, 7,612 operations, 257,288/257,288 Smithy conformance variants pass — true 100% across every implemented service, no flake margin. See [the parity matrix](website/content/docs/parity.md) for the full service-by-service breakdown of control-plane vs data-plane coverage and known limitations. +- Current project state: 107 AWS services, 7,657 operations, 258,545/258,545 Smithy conformance variants pass — true 100% across every implemented service, no flake margin. See [the parity matrix](website/content/docs/parity.md) for the full service-by-service breakdown of control-plane vs data-plane coverage and known limitations. - The broader roadmap prioritizes services that LocalStack keeps behind paid tiers, especially ECS, ELB/ALB, CloudFront, CloudWatch Metrics, and EC2. - Introspection SDKs (Rust, Python, TypeScript, Go, PHP, Java, .NET) are already built and maintained for the `/_fakecloud/*` endpoints. diff --git a/README.md b/README.md index aa4fc9e98..dd9514eef 100644 --- a/README.md +++ b/README.md @@ -48,7 +48,7 @@ Works as a drop-in for LocalStack in CI, with Terraform (`endpoints` block), CDK ## Why fakecloud - **Free, forever.** AGPL-3.0, no paid tier, no account, no token. -- **True 100% conformance.** 257,288 Smithy-model-generated test variants pass on every commit, validated against AWS's own Smithy models. CI also runs upstream `terraform-provider-aws` `TestAcc*` suites to catch waiter/field/drift bugs that SDK tests miss. +- **True 100% conformance.** 258,545 Smithy-model-generated test variants pass on every commit, validated against AWS's own Smithy models. CI also runs upstream `terraform-provider-aws` `TestAcc*` suites to catch waiter/field/drift bugs that SDK tests miss. - **Real cross-service wiring.** EventBridge -> Step Functions, S3 -> Lambda, SES inbound -> S3/SNS/Lambda, and 15+ more integrations execute end-to-end. - **Real infrastructure for stateful services.** Lambda (31 runtimes), RDS (Postgres/MySQL/MariaDB/Oracle/SQL Server/Db2), ElastiCache (Redis/Valkey/Memcached), ECS, and EC2 run as real containers. Use Docker (default) or native Kubernetes Pods via `FAKECLOUD_CONTAINER_BACKEND=k8s`. See the [Kubernetes backend guide](https://fakecloud.dev/docs/guides/kubernetes-backend/). - **Single binary.** ~19 MB, ~10 MiB idle, ~300ms startup. No Docker needed to run fakecloud itself. @@ -60,7 +60,7 @@ Works as a drop-in for LocalStack in CI, with Terraform (`endpoints` block), CDK ## Supported services -106 services, 7,612 operations, and true 100% conformance across every implemented service. +107 services, 7,657 operations, and true 100% conformance across every implemented service. Highlights: S3, DynamoDB, SQS, SNS, EventBridge, Lambda, IAM, STS, KMS, Secrets Manager, CloudFormation, SES, Cognito, Kinesis, RDS (6 real engines), ElastiCache, ECS/ECR, EC2, Step Functions, API Gateway v1/v2, Bedrock, and 80+ more. @@ -78,7 +78,7 @@ Since March 2026, LocalStack's Community image requires an account and token, an | License | AGPL-3.0, free for commercial use | Proprietary, paid plans | | Auth | None | Account + token required | | Footprint | ~19 MB binary, ~10 MiB idle, ~300ms start, no Docker | ~1 GB image, ~150 MiB idle, ~3s start, Docker required | -| Conformance | true 100% (257,288 Smithy variants) | partial | +| Conformance | true 100% (258,545 Smithy variants) | partial | | Paywalled services | None (RDS, Cognito, SES, ElastiCache, ECS/ECR, EKS, Redshift, and more are all free) | Many core services paid-only | | Test-assertion SDKs | TypeScript, Python, Go, PHP, Java, Rust, .NET | Python, Java | diff --git a/conformance-baseline-notes.md b/conformance-baseline-notes.md index 9610fee2c..168c5ded0 100644 --- a/conformance-baseline-notes.md +++ b/conformance-baseline-notes.md @@ -2,7 +2,7 @@ `conformance-baseline.json` is the floor a PR must clear in CI. Every service is pinned at its full observed pass count — currently -257,288 / 257,288 variants across all 106 services. CI fails any PR with a +258,545 / 258,545 variants across all 107 services. CI fails any PR with a failing variant, and any PR that drops a service below its baseline pass count (no flake margin). diff --git a/crates/fakecloud-conformance/README.md b/crates/fakecloud-conformance/README.md index 0d3882ef9..fdfef0314 100644 --- a/crates/fakecloud-conformance/README.md +++ b/crates/fakecloud-conformance/README.md @@ -32,7 +32,7 @@ For every operation, [`generators/`](./src/generators/) produces test variants u | **Round-trip echo** (`round_trip.rs`) | Walks the Smithy graph to pair every `Create*`/`Put*`/`Update*` with its matching `Get*`/`Describe*`. Sends the writer with one optional input field set, then chases the reader and asserts that field echoed | Silent input drops on Create/Put/Update — the field is accepted at the wire layer but lost before storage (e.g. Lambda `Layers` dropped on `CreateFunction`, [#853](https://github.com/faiscadev/fakecloud/issues/853)) | | **Identifier-form fanout** (`id_forms.rs`) | For URL-bound `@httpLabel` members whose `@pattern` admits an ARN, sends the operation with each accepted identifier form: bare name, `name:qualifier`, partial ARN, full ARN, URL-encoded full ARN | REST endpoints that 404 on alternate identifier forms (e.g. Lambda `GetFunction` rejecting full ARNs, [#817](https://github.com/faiscadev/fakecloud/issues/817)) | -A single operation typically produces anywhere from a dozen to several hundred variants. Across all 106 services the baseline is **257,288 / 257,288 variants passing (100%)** — see [`conformance-baseline.json`](../../conformance-baseline.json) for the per-service breakdown. +A single operation typically produces anywhere from a dozen to several hundred variants. Across all 107 services the baseline is **258,545 / 258,545 variants passing (100%)** — see [`conformance-baseline.json`](../../conformance-baseline.json) for the per-service breakdown. ### 3. Send and capture diff --git a/scripts/check-doc-counts.sh b/scripts/check-doc-counts.sh index 1bd4f70af..951baf215 100755 --- a/scripts/check-doc-counts.sh +++ b/scripts/check-doc-counts.sh @@ -660,12 +660,7 @@ done < <( # the service count, the operation total and the conformance claim at once. # PENDING_PARITY_ROW names such slugs while the row is being added; it must be # empty on main once that work lands. -PENDING_PARITY_ROW=( - # AWS Batch: shipped (crates/fakecloud-batch, 45 ops) but never wired into - # the conformance probe (no aws-models/service-map.json entry), so it has - # no baseline to back a "true 100%" row yet. - "batch" -) +PENDING_PARITY_ROW=() OPS_INDEX_GEN="scripts/generate-operations-index.sh" if [ ! -f "$OPS_INDEX_GEN" ]; then echo "missing $OPS_INDEX_GEN (model -> slug map moved? update OPS_INDEX_GEN)" >&2 @@ -704,7 +699,7 @@ parity_slug_ops=$(awk -F'|' '/^\| \[/ { while IFS=$'\t' read -r slug want got; do [ -z "$slug" ] && continue pending=0 - for _p in "${PENDING_PARITY_ROW[@]}"; do [ "$_p" = "$slug" ] && pending=1; done + for _p in ${PENDING_PARITY_ROW[@]+"${PENDING_PARITY_ROW[@]}"}; do [ "$_p" = "$slug" ] && pending=1; done if [ "$got" = "-" ]; then if [ "$pending" -eq 0 ]; then problems+=("$PARITY: no row for docs/services/$slug.md ($want modeled operations); the service is missing from every headline count") diff --git a/website/content/docs/about/conformance.md b/website/content/docs/about/conformance.md index a43de230e..8cf3b7787 100644 --- a/website/content/docs/about/conformance.md +++ b/website/content/docs/about/conformance.md @@ -35,7 +35,7 @@ Every response is validated against the operation's Smithy output shape. Missing ## Current coverage -257,288/257,288 generated test variants across all 106 services (7,612 operations) pass on every commit — true 100% conformance with no flake margin and no skipped services. The exact pass/total per service is checked into [`conformance-baseline.json`](https://github.com/faiscadev/fakecloud/blob/main/conformance-baseline.json). +258,545/258,545 generated test variants across all 107 services (7,657 operations) pass on every commit — true 100% conformance with no flake margin and no skipped services. The exact pass/total per service is checked into [`conformance-baseline.json`](https://github.com/faiscadev/fakecloud/blob/main/conformance-baseline.json). See the harness and methodology at [`crates/fakecloud-conformance/`](https://github.com/faiscadev/fakecloud/tree/main/crates/fakecloud-conformance). @@ -94,7 +94,7 @@ The real-AWS job is locked down hard: ### Why E2E isn't parity -The E2E suite is much bigger (2,300+ tests, 106 services) but it doesn't run against real AWS — ever. Two reasons: +The E2E suite is much bigger (2,300+ tests, 107 services) but it doesn't run against real AWS — ever. Two reasons: 1. **A lot of E2E is testing fakecloud itself.** Introspection endpoints, persistence mode, `/_fakecloud/*/tick` processors, warm Lambda container introspection, forced SQS DLQ moves, auth event logs in Cognito — none of that exists on real AWS. Running those tests against AWS would be meaningless; they'd just fail at the first `/_fakecloud/*` request. diff --git a/website/content/docs/about/what-it-is.md b/website/content/docs/about/what-it-is.md index 38902041a..626936eca 100644 --- a/website/content/docs/about/what-it-is.md +++ b/website/content/docs/about/what-it-is.md @@ -6,7 +6,7 @@ weight = 3 ## What fakecloud is -A free, open-source local AWS emulator for integration testing and local development. For every service it implements, the goal is 100% behavioral parity with real AWS — measured by a schema-driven [conformance harness](/docs/about/conformance/) that runs 257,288 generated test variants against official AWS Smithy models on every commit, all of which pass. +A free, open-source local AWS emulator for integration testing and local development. For every service it implements, the goal is 100% behavioral parity with real AWS — measured by a schema-driven [conformance harness](/docs/about/conformance/) that runs 258,545 generated test variants against official AWS Smithy models on every commit, all of which pass. The point is to let you run your application code against something that behaves like AWS, without burning an AWS account or hitting rate limits. Your tests exercise real SDK code paths end-to-end. Your CI pipeline runs fast and free. @@ -14,7 +14,7 @@ The point is to let you run your application code against something that behaves **Not a production cloud.** It's not designed for scale, durability, multi-tenancy, or production workloads. State is in-memory by default. Persistence is limited to a subset of services. It's single-binary and single-process. Don't put it in front of real users. -**Not a drop-in for all of AWS.** It implements 106 services — the ones most teams actually test against. If you need QuickSight, Polly, or Rekognition, fakecloud isn't the right tool (yet). See the [parity matrix](@/docs/parity.md) for what ships today. +**Not a drop-in for all of AWS.** It implements 107 services — the ones most teams actually test against. If you need QuickSight, Polly, or Rekognition, fakecloud isn't the right tool (yet). See the [parity matrix](@/docs/parity.md) for what ships today. **Not a mock.** Mocks return predefined values regardless of whether you call them correctly. fakecloud speaks real AWS wire protocols, validates real SigV4 headers (without signature checking), and returns AWS-shaped responses that the real SDK parses and deserializes. If your code assembles the request wrong, fakecloud fails the same way real AWS would. @@ -37,4 +37,4 @@ The point is to let you run your application code against something that behaves fakecloud exists because testing AWS code well is harder than it should be. Mocks lie. Staging accounts cost money and leak state between tests. Real AWS is slow, rate-limited, and unsuitable for fast CI feedback. The bet is that a correctness-first local emulator — one that matches AWS's actual wire protocol on every documented operation — is a better tradeoff for most testing workflows than any of the alternatives. -That's why the project prioritizes conformance and cross-service wiring over breadth. 106 services at true 100% (257,288/257,288 Smithy variants pass) is more useful than 106 services at 50%. +That's why the project prioritizes conformance and cross-service wiring over breadth. 107 services at true 100% (258,545/258,545 Smithy variants pass) is more useful than 107 services at 50%. diff --git a/website/content/docs/migration-from-localstack.md b/website/content/docs/migration-from-localstack.md index 2283b02da..7d51781e2 100644 --- a/website/content/docs/migration-from-localstack.md +++ b/website/content/docs/migration-from-localstack.md @@ -9,7 +9,7 @@ If your local development workflow is blocked by LocalStack's account requiremen ## Key Differences - **No API Key**: fakecloud is fully functional offline. No `ACTIVATE_PRO` or account login required. - **Single Binary**: Replace heavy Docker-in-Docker setups with a ~19MB binary that starts in <300ms. -- **Parity**: 100% API conformance across 7,612 operations, including features LocalStack gates behind Pro (like ECR and Bedrock). +- **Parity**: 100% API conformance across 7,657 operations, including features LocalStack gates behind Pro (like ECR and Bedrock). ## Service Mapping | Feature | LocalStack | fakecloud | diff --git a/website/content/docs/parity.md b/website/content/docs/parity.md index 0b4a4bdd9..3b1175d56 100644 --- a/website/content/docs/parity.md +++ b/website/content/docs/parity.md @@ -4,7 +4,7 @@ description = "Service-by-service behavior parity: what is real, what is synthes weight = 1 +++ -fakecloud implements **106 AWS services** with **7,612 operations**. **257,288/257,288 generated Smithy conformance variants pass** on every commit — true 100% across every implemented service, no flake margin and no skipped services. Conformance checks request/response shapes, field names, and error codes against [AWS's own Smithy models](https://github.com/faiscadev/fakecloud/blob/main/conformance-baseline.json). Behavior parity varies by service — some run real infrastructure (Postgres, Redis, Docker containers), some run a real control plane but return synthesized data for complex queries, and a few have control-plane-only coverage with no data-plane enforcement. +fakecloud implements **107 AWS services** with **7,657 operations**. **258,545/258,545 generated Smithy conformance variants pass** on every commit — true 100% across every implemented service, no flake margin and no skipped services. Conformance checks request/response shapes, field names, and error codes against [AWS's own Smithy models](https://github.com/faiscadev/fakecloud/blob/main/conformance-baseline.json). Behavior parity varies by service — some run real infrastructure (Postgres, Redis, Docker containers), some run a real control plane but return synthesized data for complex queries, and a few have control-plane-only coverage with no data-plane enforcement. | Service | Ops | Protocol | Control plane | Data plane | Known limitations | | --- | --- | --- | --- | --- | --- | @@ -90,6 +90,7 @@ fakecloud implements **106 AWS services** with **7,612 operations**. **257,288/2 | [Bedrock Agent Runtime](@/docs/services/bedrock-agent-runtime.md) | 31 | JSON 1.1 | Full | Partial | `InvokeAgent`, `Retrieve`, `RetrieveAndGenerate`, `InvokeFlow`, and the streaming variants are wired end-to-end with shape-correct synthetic chunks. No real foundation-model inference — see Bedrock runtime caveat above. | | [ECR](@/docs/services/ecr.md) | 58 | JSON 1.1 | Full | Full | OCI v2 push/pull is real. Lifecycle policy evaluation, image scanning, pull-through cache, registry templates, and cosign signature verification are all implemented. | | [ECS](@/docs/services/ecs.md) | 77 | JSON 1.1 (Query) | Full | Full | Real Fargate-style task execution via Docker, services with rolling deployments + blue/green lifecycle-hook pauses (`ContinueServiceDeployment`), task sets, container instances, capacity providers, and ECS Exec. Multi-container tasks, volume mounts, health checks, and `dependsOn` ordering are all implemented. | +| [Batch](@/docs/services/batch.md) | 45 | REST-JSON | Full | Full | Complete 45-op AWS Batch surface: compute environments, job queues (compute- or service-environment ordered), revisioned job definitions, fair-share scheduling policies, consumable resources, service environments, quota shares, tags and the job control plane. `SubmitJob` runs the job as a real container on fakecloud's ECS task engine and drives its status off the container's lifecycle (`SUCCEEDED` on exit 0, `FAILED` with the real exit code otherwise), including array jobs, `dependsOn`, retry strategies, attempt timeouts and consumable-resource gating; `CancelJob` / `TerminateJob` stop the real container. Service jobs (`SubmitServiceJob`) are accepted and queued but wait at `RUNNABLE`, since fakecloud's SageMaker has no training executor. With no container runtime, jobs stay `SUBMITTED` rather than reporting a fabricated success. | | [ELBv2](@/docs/services/elbv2.md) | 51 | JSON 1.1 (Query) | Full | Partial | Control plane (ALB/NLB/GWLB CRUD, target groups, listeners, rules, mTLS trust stores) is fully implemented. An in-process HTTP data plane for ALBs handles rule matching, forwarding, fixed-response, redirect, and sticky sessions. WAFv2 inspection is wired into the ALB data plane. NLB and GWLB data planes are not implemented. | | [CloudFront](@/docs/services/cloudfront.md) | 167 | REST-XML | Full | Partial | Control plane is fully implemented (distributions, policies, functions, key value stores, etc.). An in-process HTTP data plane serves enabled distributions from their origins on the main listener, routed by `Host` (discovered via `/_fakecloud/cloudfront/distributions`): path-pattern cache-behavior routing, S3 (REST and website) and custom origins, private S3 origins through an origin access control or legacy origin access identity (authorized by the bucket policy under `--iam`), and CustomErrorResponses (the SPA `404 -> /index.html` fallback). Not implemented: a global/geo edge network, in-path CloudFront Functions / Lambda@Edge (CloudFront Functions can still be exercised out-of-band via `TestFunction`; Lambda@Edge cannot), and TTL caching / invalidation. | | [CloudTrail](@/docs/services/cloudtrail.md) | 60 | JSON 1.1 | Full | None | Complete 60-op CloudTrail control plane: trails (create/get/update/delete, `DescribeTrails`, `ListTrails`), per-trail logging that `GetTrailStatus` reflects (`StartLogging`/`StopLogging` toggle `IsLogging`, which starts `false`), event selectors and insight selectors that persist and round-trip, CloudTrail Lake event data stores (full CRUD + `RestoreEventDataStore` PENDING_DELETION -> ENABLED + ingestion start/stop + federation enable/disable), channels, imports, Lake queries (`StartQuery`/`DescribeQuery`/`GetQueryResults` settle to `FINISHED` with empty rows, `CancelQuery`), dashboards, resource policies, organization delegated admins, event configuration, and tagging. `LookupEvents`, `ListPublicKeys`, and `ListInsightsMetricData` return real empty result sets. `@length`/`@range`/enum constraints enforced. Account-partitioned and persisted. No event-recording engine — a fake needn't record its own API activity, matching how LocalStack Community mocks CloudTrail. | @@ -115,7 +116,7 @@ fakecloud implements **106 AWS services** with **7,612 operations**. **257,288/2 ## What "100% conformance" means -fakecloud validates every implemented operation against AWS's own Smithy models using a generated test suite with **257,288 variants**, **all of which pass** on every commit. This guarantees that field names, types, required/optional flags, error codes, and HTTP signatures are identical to AWS. It does *not* guarantee that every operation behaves exactly like AWS in all edge cases — that is what the **Data plane** and **Known limitations** columns describe. +fakecloud validates every implemented operation against AWS's own Smithy models using a generated test suite with **258,545 variants**, **all of which pass** on every commit. This guarantees that field names, types, required/optional flags, error codes, and HTTP signatures are identical to AWS. It does *not* guarantee that every operation behaves exactly like AWS in all edge cases — that is what the **Data plane** and **Known limitations** columns describe. If you need a service that is not listed above, the issue tracker and [roadmap](https://github.com/faiscadev/fakecloud#roadmap) are the best places to request it. diff --git a/website/content/docs/services/_index.md b/website/content/docs/services/_index.md index 9f71d63ef..253f5d978 100644 --- a/website/content/docs/services/_index.md +++ b/website/content/docs/services/_index.md @@ -7,7 +7,7 @@ template = "docs.html" page_template = "docs-page.html" +++ -fakecloud implements 106 AWS services with 7,612 total operations. 257,288/257,288 generated Smithy conformance variants pass on every commit — true 100% across the board. Per-service feature matrices and gotchas live on individual service pages — use the sidebar to navigate. +fakecloud implements 107 AWS services with 7,657 total operations. 258,545/258,545 generated Smithy conformance variants pass on every commit — true 100% across the board. Per-service feature matrices and gotchas live on individual service pages — use the sidebar to navigate. | Service | Ops | Notes | | ---------------------- | --- | ---------------------------------------------------------------------- | @@ -48,6 +48,7 @@ fakecloud implements 106 AWS services with 7,612 total operations. 257,288/257,2 | Amazon MSK | 64 | Complete Amazon MSK (Managed Streaming for Apache Kafka) control plane, signing as `kafka`: provisioned + serverless clusters (async `CREATING` -> `ACTIVE` on describe, monotonic `.../cluster//-` ARNs, name/type filters + pagination, region-scoped, `DescribeClusterV2` union), the eleven `Update*`/`RebootBroker` ops recording settling `ClusterOperation`s, broker-node synthesis (`ListNodes` + `GetBootstrapBrokers`), configurations with base64 `ServerProperties` revisions, SCRAM secrets, cluster policies, client VPC connections, replicators, topics as real control-plane state, the Kafka version catalogs, and ARN-keyed tags; persisted (in-flight lifecycle transitions reconcile on restart). Real Kafka broker data plane + CloudFormation are later batches (all 64 ops) | | Amazon S3 Glacier | 33 | Complete Glacier surface with real data plane: vaults, archive upload/delete storing the real bytes + a computed SHA-256 tree hash, multipart uploads assembled into a stored archive, retrieval + inventory jobs that settle to `Succeeded` on read so `GetJobOutput` returns the exact uploaded bytes (or a JSON inventory), vault notifications, access policy, the vault-lock state machine, tags, data-retrieval policy, provisioned capacity; persisted (archives survive restart). All 33 ops | | AWS Backup | 115 | Complete AWS Backup control plane: backup plans (+ versions) + selections, vaults (standard / logically-air-gapped / restore-access) with notifications / policies / lock, recovery points, backup / copy / restore / scan jobs (progressed to a terminal state; `StartBackupJob` records a synthetic recovery point `DescribeRecoveryPoint` resolves), frameworks, report plans + jobs, legal holds, restore testing, tiering, protected resources, tags, global / region settings; persisted. No real backup engine (all 115 ops) | +| AWS Batch | 45 | Compute environments, job queues (compute- or service-environment ordered), revisioned job definitions, fair-share scheduling policies, consumable resources, service environments, quota shares and tags. `SubmitJob` runs a real container on fakecloud's ECS task engine and drives the job status off its lifecycle (`SUCCEEDED` on exit 0, `FAILED` with the real exit code), with array jobs, `dependsOn`, retries, attempt timeouts and consumable-resource gating; `CancelJob` / `TerminateJob` stop the real container; service jobs queue at `RUNNABLE` (no SageMaker training executor). | | OpenSearch Service | 96 | Complete Amazon OpenSearch Service control plane sharing one domain store with Elasticsearch Service (both sign as `es`): domains (create/describe/delete/config persist; `Processing=false`/`Created=true` settle on describe with a synthetic search endpoint), packages, VPC endpoints, cross-cluster connections, applications + capabilities, per-domain data sources + indices, direct-query data sources, reserved instances, tags, instance-type/version/upgrade catalogues; persisted. No real cluster spawned (all 96 ops) | | Elasticsearch Service | 51 | Complete legacy Amazon Elasticsearch Service (`es`, 2015-01-01) over the SAME shared domain store as OpenSearch Service — a domain created through either API is one entity, surfaced here via `ElasticsearchDomainStatus`. Domains, packages, VPC endpoints, cross-cluster search connections, reserved instances, tags, instance-type/version/upgrade catalogues; persisted (all 51 ops) | | AWS AppConfig | 58 | Complete AppConfig across `appconfig` (56 ops) + `appconfigdata` (2 ops): applications, environments, configuration profiles, hosted configuration versions (raw bytes + content type stored/returned verbatim, auto-incrementing version), custom + AWS-predefined deployment strategies, deployments (settle to `COMPLETE`), extensions + associations, experiments, account settings, `ValidateConfiguration`, tags; persisted. Real data plane: `StartConfigurationSession` -> `GetLatestConfiguration` serves the deployed hosted-config bytes | diff --git a/website/content/dynamodb-emulator.md b/website/content/dynamodb-emulator.md index 6ff68450e..09dae7d58 100644 --- a/website/content/dynamodb-emulator.md +++ b/website/content/dynamodb-emulator.md @@ -16,7 +16,7 @@ Point your AWS SDK at `http://localhost:4566`. That's the whole setup. ## Why fakecloud for DynamoDB - **58 DynamoDB operations** at 100% conformance — tables, items, transactions (`TransactWriteItems`, `TransactGetItems`), PartiQL (`ExecuteStatement`, `BatchExecuteStatement`), backups, global tables, streams, secondary indexes. -- **Validated against AWS's own Smithy models** on every commit — 257,288/257,288 generated test variants pass, true 100% conformance. +- **Validated against AWS's own Smithy models** on every commit — 258,545/258,545 generated test variants pass, true 100% conformance. - **Any AWS SDK in any language.** Real HTTP server on port 4566 — Python boto3, Node aws-sdk, Go aws-sdk-go-v2, Java, Kotlin, Rust, PHP all work identically. - **No account, no auth token, no paid tier.** AGPL-3.0. - **No Docker required** for DynamoDB (binary runs the storage engine in-process). Fastest local DynamoDB you can run. diff --git a/website/content/fake-aws-server.md b/website/content/fake-aws-server.md index f50deef69..9a0d6c894 100644 --- a/website/content/fake-aws-server.md +++ b/website/content/fake-aws-server.md @@ -16,7 +16,7 @@ Listens on `http://localhost:4566`. Any AWS SDK in any language points at it and ## What "fake AWS server" means here - **Real HTTP server**, not an in-process mock. Your Go / Java / Kotlin / Node / Rust / PHP / Python code uses the regular AWS SDK with `endpoint_url` set to `http://localhost:4566`. -- **Speaks the AWS wire protocol** at true 100% conformance across every implemented service. 106 services, 7,612 operations, 257,288/257,288 Smithy-model-generated test variants pass on every commit. +- **Speaks the AWS wire protocol** at true 100% conformance across every implemented service. 107 services, 7,657 operations, 258,545/258,545 Smithy-model-generated test variants pass on every commit. - **Real execution** for stateful services: Lambda runs your function code in Docker containers across 31 runtimes, RDS runs real PostgreSQL/MySQL/MariaDB/Oracle/SQL Server/Db2, ElastiCache runs real Redis/Valkey/Memcached. - **Real cross-service wiring**: S3 -> Lambda, SQS -> Lambda, SNS fan-out, EventBridge -> Step Functions, and 15+ more integrations execute end-to-end, not as stubs. - **Free, open-source, AGPL-3.0.** No account, no auth token, no paid tier. diff --git a/website/content/fake-bedrock.md b/website/content/fake-bedrock.md index 20b90a11d..3a038686d 100644 --- a/website/content/fake-bedrock.md +++ b/website/content/fake-bedrock.md @@ -20,7 +20,7 @@ Point any AWS SDK at `http://localhost:4566`. - A mock fails to catch bugs in how your code assembles HTTP requests. It never sent a request. - A fake catches those bugs. Your code made a real HTTP call. The request body was parsed by a real JSON parser. The response traveled back through the real SDK response parser. -fakecloud is a fake AWS — a complete server on port 4566 that speaks 106 AWS services' wire protocols at true 100% Smithy conformance (257,288/257,288 generated variants pass). Your code thinks it's talking to real AWS. +fakecloud is a fake AWS — a complete server on port 4566 that speaks 107 AWS services' wire protocols at true 100% Smithy conformance (258,545/258,545 generated variants pass). Your code thinks it's talking to real AWS. And it is, as far as the protocol is concerned. The part that's fake is the response content — which is exactly the part you want to control in tests. diff --git a/website/content/faq.md b/website/content/faq.md index 66a3513e2..0a5bd6cec 100644 --- a/website/content/faq.md +++ b/website/content/faq.md @@ -20,7 +20,7 @@ Yes. LocalStack replaced its open-source Community Edition with a proprietary im ### How many AWS services does fakecloud support? -106 services and 7,612 API operations. 257,288/257,288 generated Smithy conformance variants pass on every commit — true 100% across every implemented service, with more services on the roadmap. The explicit goal is 100% of AWS services, each at 100% behavioral conformance, with 100% of cross-service integrations. Services land depth-first — a service is added when it passes the full Smithy-model test variants and cross-service wire-ups. +107 services and 7,657 API operations. 258,545/258,545 generated Smithy conformance variants pass on every commit — true 100% across every implemented service, with more services on the roadmap. The explicit goal is 100% of AWS services, each at 100% behavioral conformance, with 100% of cross-service integrations. Services land depth-first — a service is added when it passes the full Smithy-model test variants and cross-service wire-ups. ### Which AWS services are supported? @@ -75,7 +75,7 @@ Yes. Written in Rust, no LocalStack code was used. LocalStack is written in Pyth ### How does fakecloud validate correctness? -Every commit runs 257,288 conformance test variants generated from AWS's own Smithy models, plus end-to-end tests using the official AWS SDKs, plus the upstream `hashicorp/terraform-provider-aws` `TestAcc*` suites. If fakecloud behaves differently from real AWS, that's a bug — [open an issue](https://github.com/faiscadev/fakecloud/issues). +Every commit runs 258,545 conformance test variants generated from AWS's own Smithy models, plus end-to-end tests using the official AWS SDKs, plus the upstream `hashicorp/terraform-provider-aws` `TestAcc*` suites. If fakecloud behaves differently from real AWS, that's a bug — [open an issue](https://github.com/faiscadev/fakecloud/issues). ### Can AI coding agents (Claude Code, Cursor, Copilot) use fakecloud? @@ -91,11 +91,11 @@ Moto is a Python library that patches boto3 inside a test process. fakecloud is ### How does fakecloud compare to MinIO? -MinIO is production-grade S3-compatible storage. fakecloud is an AWS testing emulator that happens to do S3 among 105 other services. Different tools for different jobs. See [fakecloud vs MinIO](/vs/minio/). +MinIO is production-grade S3-compatible storage. fakecloud is an AWS testing emulator that happens to do S3 among 106 other services. Different tools for different jobs. See [fakecloud vs MinIO](/vs/minio/). ### How does fakecloud compare to DynamoDB Local? -DynamoDB Local is AWS's official DynamoDB emulator (DynamoDB only). fakecloud emulates DynamoDB plus 105 other AWS services, with Streams fired through real Lambda execution. See [fakecloud vs DynamoDB Local](/vs/dynamodb-local/). +DynamoDB Local is AWS's official DynamoDB emulator (DynamoDB only). fakecloud emulates DynamoDB plus 106 other AWS services, with Streams fired through real Lambda execution. See [fakecloud vs DynamoDB Local](/vs/dynamodb-local/). ### Where can I ask questions or report bugs? @@ -109,7 +109,7 @@ GitHub issues: [github.com/faiscadev/fakecloud/issues](https://github.com/faisca {"@type": "Question", "name": "What is fakecloud?", "acceptedAnswer": {"@type": "Answer", "text": "fakecloud is a free, open-source local AWS cloud emulator for integration testing and local development. It runs on a single port (4566), requires no account or auth token, and aims for 100% behavioral conformance with real AWS on every service it implements. AGPL-3.0 licensed."}}, {"@type": "Question", "name": "Is fakecloud free?", "acceptedAnswer": {"@type": "Answer", "text": "Yes. AGPL-3.0, free for commercial use. Using fakecloud as a dev/test dependency has zero AGPL implications for your application."}}, {"@type": "Question", "name": "Is fakecloud a LocalStack alternative?", "acceptedAnswer": {"@type": "Answer", "text": "Yes. LocalStack replaced its open-source Community Edition with a proprietary image in March 2026 that requires an account and auth token. fakecloud is a free, open-source replacement."}}, - {"@type": "Question", "name": "How many AWS services does fakecloud support?", "acceptedAnswer": {"@type": "Answer", "text": "106 services and 7,612 API operations. 257,288/257,288 generated Smithy conformance variants pass on every commit, true 100% across every implemented service, with more on the roadmap. The goal is 100% of AWS services, each at 100% behavioral conformance, with 100% of cross-service integrations."}}, + {"@type": "Question", "name": "How many AWS services does fakecloud support?", "acceptedAnswer": {"@type": "Answer", "text": "107 services and 7,657 API operations. 258,545/258,545 generated Smithy conformance variants pass on every commit, true 100% across every implemented service, with more on the roadmap. The goal is 100% of AWS services, each at 100% behavioral conformance, with 100% of cross-service integrations."}}, {"@type": "Question", "name": "Which AWS services are supported?", "acceptedAnswer": {"@type": "Answer", "text": "S3, SQS, SNS, EventBridge, EventBridge Pipes, EventBridge Scheduler, Lambda, EC2, DynamoDB, IAM, STS, Organizations, SSM, Secrets Manager, CloudWatch Logs, CloudWatch (Metrics & Alarms), KMS, CloudFormation, Cloud Control API, SES (v2 + v1 inbound), Cognito User Pools, Cognito Identity, Kinesis, Firehose, RDS, RDS Data API, Aurora DSQL, Resource Groups, Resource Groups Tagging API, ElastiCache, Step Functions, API Gateway v1 (REST), API Gateway v2 (HTTP), Bedrock, Bedrock Agent, Bedrock Agent Runtime, Bedrock Runtime, ECR, ECS, Elastic Load Balancing v2, CloudFront, Route 53, WAF v2, Application Auto Scaling, Athena, ACM, Glue, and more — see the parity matrix at https://fakecloud.dev/docs/parity/."}}, {"@type": "Question", "name": "Does fakecloud execute Lambda code for real?", "acceptedAnswer": {"@type": "Answer", "text": "Yes. fakecloud pulls real AWS Lambda runtime containers and executes your handler against them. 31 official runtimes including Node.js 16/18/20/22/24/26, Python 3.8 through 3.15, Java 8/11/17 (Amazon Linux 2 and 2023 variants)/21/25, .NET 8/10, Ruby 3.2/3.3/3.4/4.0, Go (go1.x), and custom provided.al2/provided.al2023."}}, {"@type": "Question", "name": "Does fakecloud run real databases for RDS?", "acceptedAnswer": {"@type": "Answer", "text": "Yes. RDS emulation pulls real PostgreSQL, MySQL, MariaDB, Oracle, SQL Server, and Db2 Docker images and runs them as the DB instance."}}, @@ -121,12 +121,12 @@ GitHub issues: [github.com/faiscadev/fakecloud/issues](https://github.com/faisca {"@type": "Question", "name": "Can I use fakecloud in CI?", "acceptedAnswer": {"@type": "Answer", "text": "Yes. Fits as a GitHub Actions service container, GitLab CI service, CircleCI service, or install-and-run background step. ~300ms startup means negligible CI overhead."}}, {"@type": "Question", "name": "Does fakecloud require Docker?", "acceptedAnswer": {"@type": "Answer", "text": "To run fakecloud itself, no. Single binary, ~19 MB. Docker is required only for services that run real containers such as Lambda runtimes, RDS engines, and ElastiCache engines."}}, {"@type": "Question", "name": "Is fakecloud written from scratch?", "acceptedAnswer": {"@type": "Answer", "text": "Yes. Written in Rust, no LocalStack code was used. LocalStack is written in Python; fakecloud is written in Rust and ships as a single static binary."}}, - {"@type": "Question", "name": "How does fakecloud validate correctness?", "acceptedAnswer": {"@type": "Answer", "text": "Every commit runs 257,288 conformance test variants generated from AWS's own Smithy models, plus end-to-end tests using the official AWS SDKs, plus the upstream hashicorp/terraform-provider-aws TestAcc suites."}}, + {"@type": "Question", "name": "How does fakecloud validate correctness?", "acceptedAnswer": {"@type": "Answer", "text": "Every commit runs 258,545 conformance test variants generated from AWS's own Smithy models, plus end-to-end tests using the official AWS SDKs, plus the upstream hashicorp/terraform-provider-aws TestAcc suites."}}, {"@type": "Question", "name": "Can AI coding agents like Claude Code, Cursor, and GitHub Copilot use fakecloud?", "acceptedAnswer": {"@type": "Answer", "text": "Yes. Paste-ready snippets for CLAUDE.md, .cursor/rules, and .github/copilot-instructions.md are in the README. Agents can also fetch https://fakecloud.dev/llms.txt for a structured API surface."}}, {"@type": "Question", "name": "Is fakecloud production-ready cloud infrastructure?", "acceptedAnswer": {"@type": "Answer", "text": "No. fakecloud is a testing and local-development tool. It is not production AWS and is not intended to be."}}, {"@type": "Question", "name": "How does fakecloud compare to Moto?", "acceptedAnswer": {"@type": "Answer", "text": "Moto is a Python library that patches boto3 inside a test process. fakecloud is a real HTTP server on port 4566. Moto is fast and Python-only; fakecloud is language-agnostic and runs real Lambda, RDS, and Redis."}}, - {"@type": "Question", "name": "How does fakecloud compare to MinIO?", "acceptedAnswer": {"@type": "Answer", "text": "MinIO is production-grade S3-compatible storage. fakecloud is an AWS testing emulator that happens to do S3 among 105 other services."}}, - {"@type": "Question", "name": "How does fakecloud compare to DynamoDB Local?", "acceptedAnswer": {"@type": "Answer", "text": "DynamoDB Local is AWS's official DynamoDB emulator, DynamoDB only. fakecloud emulates DynamoDB plus 105 other AWS services, with Streams fired through real Lambda execution."}}, + {"@type": "Question", "name": "How does fakecloud compare to MinIO?", "acceptedAnswer": {"@type": "Answer", "text": "MinIO is production-grade S3-compatible storage. fakecloud is an AWS testing emulator that happens to do S3 among 106 other services."}}, + {"@type": "Question", "name": "How does fakecloud compare to DynamoDB Local?", "acceptedAnswer": {"@type": "Answer", "text": "DynamoDB Local is AWS's official DynamoDB emulator, DynamoDB only. fakecloud emulates DynamoDB plus 106 other AWS services, with Streams fired through real Lambda execution."}}, {"@type": "Question", "name": "Where can I ask questions or report bugs?", "acceptedAnswer": {"@type": "Answer", "text": "GitHub issues: github.com/faiscadev/fakecloud/issues. The roadmap is demand-driven."}} ] } diff --git a/website/content/glossary.md b/website/content/glossary.md index 7a0a59ac5..467e88b6f 100644 --- a/website/content/glossary.md +++ b/website/content/glossary.md @@ -10,7 +10,7 @@ A short reference for the terms that show up across the fakecloud docs, [conform The measure of whether an emulator reproduces real AWS behavior — request and response shapes, field names, error codes, and observable side effects — not just whether a given API call returns a 200. fakecloud aims at 100% behavioral parity on every service it implements. See [What fakecloud is (and isn't)](/docs/about/what-it-is/). ### Conformance -The automated check that fakecloud's responses match AWS's [Smithy](#smithy) models exactly. fakecloud's conformance harness runs **257,288 generated test variants** on every commit, all of which pass. See [How conformance works](/docs/about/conformance/). +The automated check that fakecloud's responses match AWS's [Smithy](#smithy) models exactly. fakecloud's conformance harness runs **258,545 generated test variants** on every commit, all of which pass. See [How conformance works](/docs/about/conformance/). ### Configurable response A test-only mechanism for setting the exact response (or error) fakecloud will return for a given operation, without modifying application code. Used for testing failure paths — throttles, timeouts, specific error codes — that are hard to trigger against real AWS. diff --git a/website/content/localstack-alternative.md b/website/content/localstack-alternative.md index 7b6ccb14c..5a28cd56a 100644 --- a/website/content/localstack-alternative.md +++ b/website/content/localstack-alternative.md @@ -1,6 +1,6 @@ +++ title = "Free, open-source LocalStack alternative" -description = "fakecloud is a free, open-source local AWS emulator: 106 services, 7,612 operations, 257,288/257,288 Smithy variants pass (true 100% conformance), 7 test-assertion SDKs. No account, no token, no paid tier. Drop-in replacement for LocalStack Community." +description = "fakecloud is a free, open-source local AWS emulator: 107 services, 7,657 operations, 258,545/258,545 Smithy variants pass (true 100% conformance), 7 test-assertion SDKs. No account, no token, no paid tier. Drop-in replacement for LocalStack Community." template = "page.html" aliases = [ "/alternative/localstack/", @@ -24,14 +24,14 @@ Point any AWS SDK or CLI at `http://localhost:4566` with dummy credentials. That ## Goal: 100% AWS, 100% conformance, 100% integrations -fakecloud aims at every AWS service, each at 100% behavioral conformance, including every cross-service integration. Services land depth-first: a service is supported when it matches real AWS across every documented operation and cross-service wire-up — not when the API surface looks filled in. 106 services are there today (see below), all at true 100% — 257,288/257,288 Smithy variants pass; the rest are on the roadmap, prioritized by real-project demand. +fakecloud aims at every AWS service, each at 100% behavioral conformance, including every cross-service integration. Services land depth-first: a service is supported when it matches real AWS across every documented operation and cross-service wire-up — not when the API surface looks filled in. 107 services are there today (see below), all at true 100% — 258,545/258,545 Smithy variants pass; the rest are on the roadmap, prioritized by real-project demand. This is why fakecloud runs real Lambda code in real runtime containers, runs real PostgreSQL/MySQL/MariaDB/Oracle/SQL Server/Db2 for RDS, runs real Redis/Valkey/Memcached for ElastiCache, fires real S3 -> Lambda and SES inbound -> S3/SNS/Lambda flows, and validates every operation against AWS's own Smithy models on every commit. ## What fakecloud gives you -- **106 AWS services.** S3, SQS, SNS, EventBridge, EventBridge Pipes, EventBridge Scheduler, Lambda, EC2, DynamoDB, IAM, STS, Organizations, SSM, Secrets Manager, CloudWatch Logs, CloudWatch (Metrics & Alarms), KMS, CloudFormation, Cloud Control API, SES (v2 + v1 inbound), Cognito User Pools, Cognito Identity, Kinesis, Firehose, RDS, RDS Data API, Aurora DSQL, Resource Groups, Resource Groups Tagging API, ElastiCache, Step Functions, API Gateway v1 (REST), API Gateway v2 (HTTP), Bedrock, Bedrock Agent, Bedrock Agent Runtime, Bedrock Runtime, ECR, ECS, Elastic Load Balancing v2, CloudFront, Route 53, WAF v2, Application Auto Scaling, Athena, ACM, Glue, and more — see the [parity matrix](@/docs/parity.md). -- **7,612 API operations. True 100% conformance** across every implemented service — 257,288/257,288 Smithy-model-generated test variants pass on every commit. +- **107 AWS services.** S3, SQS, SNS, EventBridge, EventBridge Pipes, EventBridge Scheduler, Lambda, EC2, DynamoDB, IAM, STS, Organizations, SSM, Secrets Manager, CloudWatch Logs, CloudWatch (Metrics & Alarms), KMS, CloudFormation, Cloud Control API, SES (v2 + v1 inbound), Cognito User Pools, Cognito Identity, Kinesis, Firehose, RDS, RDS Data API, Aurora DSQL, Resource Groups, Resource Groups Tagging API, ElastiCache, Step Functions, API Gateway v1 (REST), API Gateway v2 (HTTP), Bedrock, Bedrock Agent, Bedrock Agent Runtime, Bedrock Runtime, ECR, ECS, Elastic Load Balancing v2, CloudFront, Route 53, WAF v2, Application Auto Scaling, Athena, ACM, Glue, and more — see the [parity matrix](@/docs/parity.md). +- **7,657 API operations. True 100% conformance** across every implemented service — 258,545/258,545 Smithy-model-generated test variants pass on every commit. - **Tested against upstream Terraform acceptance tests.** CI runs `hashicorp/terraform-provider-aws` `TestAcc*` suites against fakecloud, catching waiter and field-presence drift that pure SDK tests miss. - **Real Lambda execution.** 31 runtimes in Docker containers. Not a mock, not a stub. Node, Python, Java, Go, .NET, Ruby, custom runtimes. - **Real stateful services.** RDS runs real PostgreSQL/MySQL/MariaDB/Oracle/SQL Server/Db2. ElastiCache runs real Redis/Valkey/Memcached. Your Lambda talking to RDS is talking to a real Postgres (or Oracle, or SQL Server). @@ -52,7 +52,7 @@ This is why fakecloud runs real Lambda code in real runtime containers, runs rea | Startup | ~300ms | ~3s | | Idle memory | ~10 MiB | ~150 MiB | | Install size | ~19 MB binary | ~1 GB image | -| Conformance methodology | Smithy-model-validated, 257,288/257,288 variants pass | Not published | +| Conformance methodology | Smithy-model-validated, 258,545/258,545 variants pass | Not published | | Test-assertion SDKs | TypeScript, Python, Go, PHP, Java, Rust, .NET | Python, Java | | Cognito User Pools | 132 operations | [Paid only](https://docs.localstack.cloud/references/licensing/) | | SES v2 | 116 operations, full send + templates + DKIM | [Paid only](https://docs.localstack.cloud/references/licensing/) | @@ -113,7 +113,7 @@ Moto is a Python library that patches boto3 inside a test process. fakecloud is **How is fakecloud different from SAM Local / serverless-offline?** -SAM Local and serverless-offline only run Lambda (and a limited HTTP/API Gateway surface in front of it). fakecloud runs Lambda plus 105 other services, with real cross-service integrations. If your function calls SQS, fans out over SNS, or reads from DynamoDB, fakecloud has those services wired up. +SAM Local and serverless-offline only run Lambda (and a limited HTTP/API Gateway surface in front of it). fakecloud runs Lambda plus 106 other services, with real cross-service integrations. If your function calls SQS, fans out over SNS, or reads from DynamoDB, fakecloud has those services wired up. **Does fakecloud run on CI?** @@ -121,7 +121,7 @@ Yes. Single binary, ~19 MB, ~300ms startup. Common patterns: install-and-run as **What does "100% conformance" mean?** -For every operation exposed by AWS's Smithy model, fakecloud accepts every documented input shape and returns the documented output shape, with every field AWS returns. Validated on every commit against 257,288 generated test variants — every single one passes, no flake margin and no skipped services — plus the upstream `hashicorp/terraform-provider-aws` `TestAcc*` suites. This applies to every service listed above. +For every operation exposed by AWS's Smithy model, fakecloud accepts every documented input shape and returns the documented output shape, with every field AWS returns. Validated on every commit against 258,545 generated test variants — every single one passes, no flake margin and no skipped services — plus the upstream `hashicorp/terraform-provider-aws` `TestAcc*` suites. This applies to every service listed above. **What's fakecloud's coverage goal?** diff --git a/website/content/supported-services.md b/website/content/supported-services.md index 131869e30..1a2962f6b 100644 --- a/website/content/supported-services.md +++ b/website/content/supported-services.md @@ -1,15 +1,15 @@ +++ title = "AWS Service Coverage & API Conformance" -description = "fakecloud provides 100% API conformance across 7,612 operations. Explore our supported AWS services for local development." +description = "fakecloud provides 100% API conformance across 7,657 operations. Explore our supported AWS services for local development." template = "page.html" +++ -fakecloud provides 100% API conformance across 7,612 operations. Unlike mocks, fakecloud is built against official AWS Smithy models to ensure wire-protocol compatibility and deterministic behavior for local development. +fakecloud provides 100% API conformance across 7,657 operations. Unlike mocks, fakecloud is built against official AWS Smithy models to ensure wire-protocol compatibility and deterministic behavior for local development. ## Coverage Summary -- **Total Services**: 106 -- **Total Operations**: 7,612 -- **Conformance Engine**: 257,288 Smithy-based test variants +- **Total Services**: 107 +- **Total Operations**: 7,657 +- **Conformance Engine**: 258,545 Smithy-based test variants - **Startup Time**: ~300ms ## Supported Services @@ -40,6 +40,7 @@ fakecloud provides 100% API conformance across 7,612 operations. Unlike mocks, f - **Amazon S3 Glacier**: 33 operations (complete). Full Glacier surface with a real data plane: vaults (create/describe/delete/list), archive upload/delete storing the real bytes with a computed SHA-256 tree hash, multipart uploads assembled into a stored archive, retrieval and inventory jobs that settle to `Succeeded` on read so `GetJobOutput` returns the exact uploaded bytes (or a JSON inventory), vault notifications, vault access policy, the vault-lock state machine (`InProgress` -> `Locked` with a 24h lock-id expiry), per-vault tags, the account data-retrieval policy, and provisioned capacity, with persistence (archives survive restart). Account-scoped paths accept the literal `-`; archive/upload/job ids and `x-amz-sha256-tree-hash` / `Location` are mirrored in response headers. - **AWS AppConfig**: 58 operations (complete). Full AppConfig across both `appconfig` (56 ops) and `appconfigdata` (2 ops): applications, environments, configuration profiles, hosted configuration versions (the raw request bytes and content type are stored and returned verbatim, with an auto-incrementing version number), custom and AWS-predefined deployment strategies, deployments (settled to `COMPLETE` synchronously with an event log), extensions and extension associations, experiment definitions and runs, account settings, `ValidateConfiguration`, and tagging, with persistence. Real data plane: `StartConfigurationSession` -> `GetLatestConfiguration` resolves a session token to the latest deployed hosted-config bytes. - **AWS Backup**: 115 operations (complete). Full AWS Backup control plane: backup plans (with versions) + selections, backup vaults (standard / logically-air-gapped / restore-access) with notifications, access policies, and lock configuration, recovery points, backup / copy / restore / scan jobs (progressed synthetically to a terminal state so Describe/List show completed work; `StartBackupJob` records a synthetic recovery point that `DescribeRecoveryPoint` resolves), frameworks, report plans + jobs, legal holds, restore-testing plans + selections, tiering configurations, protected resources, tags, and account-scoped global / region settings, with persistence. No real backup engine runs (control-plane emulation, matching LocalStack Community). +- **AWS Batch**: 45 operations (complete). Compute environments, job queues (compute- or service-environment ordered), revisioned job definitions, fair-share scheduling policies, consumable resources, service environments, quota shares and tags. `SubmitJob` runs a real container on fakecloud's ECS task engine and drives the job status off its lifecycle (`SUCCEEDED` on exit 0, `FAILED` with the real exit code), with array jobs, `dependsOn`, retries, attempt timeouts and consumable-resource gating; `CancelJob` / `TerminateJob` stop the real container; service jobs queue at `RUNNABLE` (no SageMaker training executor). ### AI & Machine Learning - **Bedrock**: 222 operations across 4 APIs (Bedrock 108, Bedrock Runtime 11, Bedrock Agent 72, Bedrock Agent Runtime 31). Guardrails, Model Customization, Provisioned Throughput, Agents, Knowledge Bases. diff --git a/website/content/vs/dynamodb-local.md b/website/content/vs/dynamodb-local.md index 4ca0399e7..8c7f096dc 100644 --- a/website/content/vs/dynamodb-local.md +++ b/website/content/vs/dynamodb-local.md @@ -1,6 +1,6 @@ +++ title = "fakecloud vs DynamoDB Local" -description = "How fakecloud compares to DynamoDB Local. Same DynamoDB behavior, plus cross-service triggers, Lambda execution, and the 105 other AWS services around it." +description = "How fakecloud compares to DynamoDB Local. Same DynamoDB behavior, plus cross-service triggers, Lambda execution, and the 106 other AWS services around it." template = "page.html" +++ @@ -34,7 +34,7 @@ DynamoDB Local is focused and battle-tested. For pure DynamoDB tests, nothing wr | Global tables | Yes | Yes (limited) | | Lambda consumes Streams (real) | **Yes** | **No** (no Lambda service) | | S3 writes trigger DynamoDB updates via Lambda | **Yes** | **No** | -| Other AWS services available | 105 more | None | +| Other AWS services available | 106 more | None | | Runtime | Single Rust binary (~19 MB) | Java JAR or Docker image | | Startup | ~300ms | ~2s | | Install size | ~19 MB | ~60 MB JAR + JVM | diff --git a/website/content/vs/elasticmq.md b/website/content/vs/elasticmq.md index e1759ee05..e4284c348 100644 --- a/website/content/vs/elasticmq.md +++ b/website/content/vs/elasticmq.md @@ -1,12 +1,12 @@ +++ title = "fakecloud vs ElasticMQ" -description = "How fakecloud compares to ElasticMQ. Both provide local SQS; fakecloud adds SNS fan-out, Lambda event source mappings, and 105 other AWS services." +description = "How fakecloud compares to ElasticMQ. Both provide local SQS; fakecloud adds SNS fan-out, Lambda event source mappings, and 106 other AWS services." template = "page.html" +++ [ElasticMQ](https://github.com/softwaremill/elasticmq) is a message queue server with an Amazon SQS-compatible interface. Scala-based, focused, battle-tested. Good at what it does. -fakecloud's SQS is one of 106 services and ties into the rest (SNS fan-out, Lambda event source mappings, DLQ to other services, IAM policy enforcement). +fakecloud's SQS is one of 107 services and ties into the rest (SNS fan-out, Lambda event source mappings, DLQ to other services, IAM policy enforcement). ## When to pick ElasticMQ @@ -33,7 +33,7 @@ fakecloud's SQS is one of 106 services and ties into the rest (SNS fan-out, Lamb | SNS -> SQS fan-out | **Yes** | **No** (no SNS service) | | SQS -> Lambda event source mapping | **Yes** (Lambda runs for real) | **No** (no Lambda service) | | IAM policy enforcement on SQS | Yes (opt-in `--iam strict`) | No | -| Other AWS services | 105 more | None | +| Other AWS services | 106 more | None | | Runtime | Rust binary (~19 MB) | Scala/JVM | | Startup | ~300ms | ~2-3s (JVM) | diff --git a/website/content/vs/floci.md b/website/content/vs/floci.md index 7cd445cac..ad808f850 100644 --- a/website/content/vs/floci.md +++ b/website/content/vs/floci.md @@ -16,7 +16,7 @@ Both are free, open-source, local AWS emulators. Real HTTP server speaking the A **floci's approach** (check their site for the current details — the project publishes performance claims like startup time, memory, and SDK-test pass rate): a free LocalStack replacement. Verify their numbers against the version you'd actually run. -**fakecloud's approach:** depth-first, explicit goal. 100% of AWS services, each at 100% behavioral conformance, with 100% of cross-service integrations. A service is added when it passes the full Smithy-model test variants and cross-service wire-ups, not when the API surface looks filled in. 106 services shipped today (including full ECR with OCI v2 `docker push`/`pull`, full ECS, full ELBv2 ALB/NLB/GWLB control plane). Built around real Lambda execution (31 runtimes in Docker), real stateful backends (Postgres/MySQL/MariaDB/Redis/Valkey/Memcached via Docker), real cross-service wiring, and validation on every commit against AWS's own Smithy models — 257,288/257,288 generated test variants pass, true 100% conformance — plus the upstream `hashicorp/terraform-provider-aws` `TestAcc*` suites. +**fakecloud's approach:** depth-first, explicit goal. 100% of AWS services, each at 100% behavioral conformance, with 100% of cross-service integrations. A service is added when it passes the full Smithy-model test variants and cross-service wire-ups, not when the API surface looks filled in. 107 services shipped today (including full ECR with OCI v2 `docker push`/`pull`, full ECS, full ELBv2 ALB/NLB/GWLB control plane). Built around real Lambda execution (31 runtimes in Docker), real stateful backends (Postgres/MySQL/MariaDB/Redis/Valkey/Memcached via Docker), real cross-service wiring, and validation on every commit against AWS's own Smithy models — 258,545/258,545 generated test variants pass, true 100% conformance — plus the upstream `hashicorp/terraform-provider-aws` `TestAcc*` suites. Breadth-first and depth-first are both valid tradeoffs. Pick by whether your tests need real downstream execution and cross-service flows, or surface-level plausibility across more services. @@ -32,12 +32,12 @@ Run your actual test suite against both. Numbers published on landing pages are | Distribution | Single static binary (~19 MB) + Docker image | | Startup | ~300ms | | Idle memory | ~10 MiB | -| Services covered today | 106 (7,612 ops) at true 100% conformance (257,288/257,288 variants), incl. ECR + ECS + ELBv2 | +| Services covered today | 107 (7,657 ops) at true 100% conformance (258,545/258,545 variants), incl. ECR + ECS + ELBv2 | | Lambda execution | Real code in 31 Docker runtime containers | | RDS | Real PostgreSQL/MySQL/MariaDB via Docker | | ElastiCache | Real Redis/Valkey/Memcached via Docker | | Cross-service wiring | S3 -> Lambda, SNS fan-out, EventBridge -> Step Functions, SES inbound -> S3/SNS/Lambda, 15+ more fire end-to-end | -| Conformance methodology | Smithy-validated, 257,288/257,288 test variants pass on every commit | +| Conformance methodology | Smithy-validated, 258,545/258,545 test variants pass on every commit | | Terraform TestAcc CI | Yes (upstream suites run against fakecloud) | | Test-assertion SDKs | TypeScript, Python, Go, PHP, Java, Rust, .NET | | Multi-account, SCPs, ABAC | Yes | diff --git a/website/content/vs/localstack.md b/website/content/vs/localstack.md index dc14d20e9..dab6c54c3 100644 --- a/website/content/vs/localstack.md +++ b/website/content/vs/localstack.md @@ -27,7 +27,7 @@ Since LocalStack replaced its open-source Community Edition with a proprietary i | Startup | ~300ms | ~3s | ~3s | | Idle memory | ~10 MiB | ~150 MiB | ~150 MiB | | Install size | ~19 MB | ~1 GB Docker image | ~1 GB Docker image | -| Conformance methodology | Smithy-validated, 257,288/257,288 test variants pass on every commit | Not published | Not published | +| Conformance methodology | Smithy-validated, 258,545/258,545 test variants pass on every commit | Not published | Not published | | Terraform TestAcc CI | Yes (upstream suites run against fakecloud) | Not published | Not published | | Test-assertion SDKs | TypeScript, Python, Go, PHP, Java, Rust, .NET | Python, Java | Python, Java | | Cognito User Pools | 132 ops, full auth flows | [Paid only](https://docs.localstack.cloud/references/licensing/) | Yes | @@ -46,7 +46,7 @@ Since LocalStack replaced its open-source Community Edition with a proprietary i **LocalStack's approach** is breadth-first — a very large catalog of AWS services at varying depth. Good for "tests need the call to resolve plausibly." -**fakecloud's approach** is depth-first — 106 services today, each at 100% behavioral conformance with 100% of cross-service integrations. Good for "tests need the downstream actually to happen." +**fakecloud's approach** is depth-first — 107 services today, each at 100% behavioral conformance with 100% of cross-service integrations. Good for "tests need the downstream actually to happen." Both are valid. Pick by whether your tests need real cross-service wiring, real Lambda execution, and real stateful backends, or whether you need surface-level plausibility across more services. diff --git a/website/content/vs/minio.md b/website/content/vs/minio.md index 1762cb9a2..27ac7809a 100644 --- a/website/content/vs/minio.md +++ b/website/content/vs/minio.md @@ -1,6 +1,6 @@ +++ title = "fakecloud vs MinIO" -description = "How fakecloud compares to MinIO. MinIO is an S3-compatible object store; fakecloud is an AWS emulator with S3 plus 105 other services." +description = "How fakecloud compares to MinIO. MinIO is an S3-compatible object store; fakecloud is an AWS emulator with S3 plus 106 other services." template = "page.html" +++ @@ -35,7 +35,7 @@ MinIO is excellent for these. It's not primarily a testing tool; it's real infra | S3 notifications | Yes | Yes | | Notifications fire Lambda | **Yes** (real Lambda runs) | **No** (no Lambda service) | | IAM + STS API | Yes | MinIO-specific IAM (not AWS IAM API) | -| Other AWS services (Lambda, DynamoDB, SQS, etc.) | **105 more** | **None** | +| Other AWS services (Lambda, DynamoDB, SQS, etc.) | **106 more** | **None** | | Encryption via KMS | Yes (real AWS KMS emulation) | MinIO-specific KMS gateway | | Startup | ~300ms | ~1-2s | | Use case | Local integration testing | Real object storage | diff --git a/website/content/vs/ministack.md b/website/content/vs/ministack.md index 04fdeea04..66e2cc5c6 100644 --- a/website/content/vs/ministack.md +++ b/website/content/vs/ministack.md @@ -16,7 +16,7 @@ Free, open-source, local AWS emulation. Real HTTP server speaking the AWS wire p **MiniStack's approach** (check their repo for current details — it's moving fast): positions as a free LocalStack replacement. Evaluate their service coverage and architecture directly against your test suite. -**fakecloud's approach:** depth-first, explicit goal. 100% of AWS services, each at 100% behavioral conformance, with 100% of cross-service integrations. Services land one at a time; a service is added when it passes the full Smithy-model test variants and cross-service wire-ups, not when the API surface looks filled in. 106 services shipped today (including full ECR with OCI v2 `docker push`/`pull`, full ECS, full ELBv2 ALB/NLB/GWLB control plane); more progressively. Built around real Lambda execution, real stateful backends (Postgres/MySQL/MariaDB/Redis/Valkey/Memcached via Docker), and real cross-service wiring, validated on every commit against AWS's own Smithy models — 257,288/257,288 generated test variants pass, true 100% conformance — plus the upstream `hashicorp/terraform-provider-aws` `TestAcc*` suites. +**fakecloud's approach:** depth-first, explicit goal. 100% of AWS services, each at 100% behavioral conformance, with 100% of cross-service integrations. Services land one at a time; a service is added when it passes the full Smithy-model test variants and cross-service wire-ups, not when the API surface looks filled in. 107 services shipped today (including full ECR with OCI v2 `docker push`/`pull`, full ECS, full ELBv2 ALB/NLB/GWLB control plane); more progressively. Built around real Lambda execution, real stateful backends (Postgres/MySQL/MariaDB/Redis/Valkey/Memcached via Docker), and real cross-service wiring, validated on every commit against AWS's own Smithy models — 258,545/258,545 generated test variants pass, true 100% conformance — plus the upstream `hashicorp/terraform-provider-aws` `TestAcc*` suites. These are philosophies, not rankings. Breadth-first and depth-first are different tradeoffs. A team whose tests lean lightly on many services will prefer breadth. A team whose tests exercise real cross-service flows or need real code execution will prefer depth. @@ -35,11 +35,11 @@ These are philosophies, not rankings. Breadth-first and depth-first are differen | Distribution | Single static binary (~19 MB) + Docker image | | Startup | ~300ms | | Idle memory | ~10 MiB | -| Services covered today | 106 (7,612 ops) at true 100% conformance (257,288/257,288 variants), incl. ECR + ECS + ELBv2 | +| Services covered today | 107 (7,657 ops) at true 100% conformance (258,545/258,545 variants), incl. ECR + ECS + ELBv2 | | Lambda execution | Real, 31 runtimes in Docker | | RDS | Real PostgreSQL/MySQL/MariaDB via Docker | | ElastiCache | Real Redis/Valkey/Memcached via Docker | -| Conformance methodology | Smithy-validated, 257,288/257,288 test variants pass on every commit | +| Conformance methodology | Smithy-validated, 258,545/258,545 test variants pass on every commit | | Terraform TestAcc CI | Yes (upstream suites run against fakecloud) | | Test-assertion SDKs | TypeScript, Python, Go, PHP, Java, Rust, .NET | | License | AGPL-3.0 | diff --git a/website/content/vs/moto.md b/website/content/vs/moto.md index e2de330a6..9e6e56907 100644 --- a/website/content/vs/moto.md +++ b/website/content/vs/moto.md @@ -28,8 +28,8 @@ fakecloud is a different tool for a different problem. | Real `docker push`/`pull` to ECR | Yes (OCI v2 Distribution) | **No** (Moto ships the JSON control plane only) | | Real cross-service wiring | Yes (S3 -> Lambda, SNS fan-out, etc fire end-to-end) | Partial (Moto simulates some events, not real execution) | | Setup for Python unit tests | HTTP endpoint override + fakecloud running | `@mock_aws` decorator | -| Service count | 106 at true 100% conformance (depth-first) | 100+ at varying depth (breadth-first) | -| Conformance methodology | Smithy-validated, 257,288/257,288 test variants pass on every commit | Not published | +| Service count | 107 at true 100% conformance (depth-first) | 100+ at varying depth (breadth-first) | +| Conformance methodology | Smithy-validated, 258,545/258,545 test variants pass on every commit | Not published | | License | AGPL-3.0 | Apache-2.0 | ## When Moto is the right pick diff --git a/website/content/vs/s3mock.md b/website/content/vs/s3mock.md index 354a60442..02e178ab1 100644 --- a/website/content/vs/s3mock.md +++ b/website/content/vs/s3mock.md @@ -1,12 +1,12 @@ +++ title = "fakecloud vs S3Mock" -description = "How fakecloud compares to adobe/S3Mock. Both local S3 emulators; fakecloud adds cross-service wiring and 105 other AWS services." +description = "How fakecloud compares to adobe/S3Mock. Both local S3 emulators; fakecloud adds cross-service wiring and 106 other AWS services." template = "page.html" +++ [adobe/S3Mock](https://github.com/adobe/S3Mock) is a lightweight S3-only mock for integration tests, written in Java and distributed as a JAR or Docker image. Simple, focused, well-maintained. -fakecloud does S3 (112 operations) plus 105 other AWS services end-to-end. +fakecloud does S3 (112 operations) plus 106 other AWS services end-to-end. ## When to pick S3Mock @@ -33,7 +33,7 @@ fakecloud does S3 (112 operations) plus 105 other AWS services end-to-end. | S3 notifications fire subscribers (real) | **Yes** (SNS/SQS/Lambda) | **No** (no other services) | | Bucket policy enforcement | Yes (opt-in `--iam strict`) | No | | Non-JVM SDKs | Any | Any (S3 SDK only) | -| Other AWS services | 105 more | None | +| Other AWS services | 106 more | None | | Startup | ~300ms | ~2s (JVM) | | Runtime | Rust binary (~19 MB) | JAR / Docker | diff --git a/website/content/vs/sam-local.md b/website/content/vs/sam-local.md index cb028d5bc..91941edc5 100644 --- a/website/content/vs/sam-local.md +++ b/website/content/vs/sam-local.md @@ -1,12 +1,12 @@ +++ title = "fakecloud vs SAM Local" -description = "How fakecloud compares to AWS SAM Local. Scope difference: SAM Local runs Lambda + limited API Gateway; fakecloud runs 106 services with real cross-service wiring." +description = "How fakecloud compares to AWS SAM Local. Scope difference: SAM Local runs Lambda + limited API Gateway; fakecloud runs 107 services with real cross-service wiring." template = "page.html" +++ AWS SAM Local is AWS's official tool for running Lambda functions locally. It invokes Lambda inside a Docker container with the real AWS runtime image, and provides a limited HTTP / API Gateway surface in front. -fakecloud runs Lambda the same way — real runtime containers, 31 runtimes supported — and also runs 105 other AWS services end-to-end at true 100% Smithy conformance (257,288/257,288 variants pass). +fakecloud runs Lambda the same way — real runtime containers, 31 runtimes supported — and also runs 106 other AWS services end-to-end at true 100% Smithy conformance (258,545/258,545 variants pass). ## Scope difference diff --git a/website/static/llms-full.txt b/website/static/llms-full.txt index dce4a23ae..6d9a377cf 100644 --- a/website/static/llms-full.txt +++ b/website/static/llms-full.txt @@ -6,10 +6,10 @@ fakecloud emulates AWS locally for integration testing and development. It is a single Rust binary (~19 MB, ~300ms startup, ~10 MiB idle memory) — no Docker required to run fakecloud itself, no signup. Point any AWS SDK or the AWS CLI at `http://localhost:4566` with dummy credentials. -**Coverage goal:** 100% of AWS services, each at 100% behavioral conformance, with 100% of cross-service integrations. Approach is depth-first — a service lands when it passes the full Smithy-model test variants and the cross-service wire-ups that matter for it, not when the API surface looks filled in. 106 services (7,612 operations) are shipped today, all at true 100% conformance — 257,288/257,288 generated Smithy variants pass on every commit; more land progressively as they hit the bar. +**Coverage goal:** 100% of AWS services, each at 100% behavioral conformance, with 100% of cross-service integrations. Approach is depth-first — a service lands when it passes the full Smithy-model test variants and the cross-service wire-ups that matter for it, not when the API surface looks filled in. 107 services (7,657 operations) are shipped today, all at true 100% conformance — 258,545/258,545 generated Smithy variants pass on every commit; more land progressively as they hit the bar. Key design principles: -- **Depth-first coverage**: every implemented service targets 100% conformance with real AWS, validated on every commit against AWS's own Smithy models — 257,288/257,288 generated test variants pass on every commit, true 100% across every implemented service. CI also runs upstream `hashicorp/terraform-provider-aws` `TestAcc*` suites against fakecloud. +- **Depth-first coverage**: every implemented service targets 100% conformance with real AWS, validated on every commit against AWS's own Smithy models — 258,545/258,545 generated test variants pass on every commit, true 100% across every implemented service. CI also runs upstream `hashicorp/terraform-provider-aws` `TestAcc*` suites against fakecloud. - **Real cross-service integrations**: 30+ service-to-service integrations wire up end-to-end — S3 notifications, SNS fan-out, EventBridge rules, DynamoDB Streams, CloudWatch Logs subscriptions, Cognito triggers, API Gateway -> Lambda, Step Functions task integrations, SES inbound -> S3/SNS/Lambda, and more. Not stubs — the downstream service actually executes. - **Real execution for stateful services**: Lambda runs function code in real Lambda runtime containers across 31 runtimes. RDS runs real PostgreSQL/MySQL/MariaDB. ElastiCache runs real Redis/Valkey. - **Zero friction**: no auth tokens, no accounts, no config files needed @@ -87,10 +87,10 @@ Default is `memory` mode — all state lives in RAM, startup is instant, shutdown is a no-op. Pass `--storage-mode=persistent --data-path=` to mirror all service state to disk and reload on the next launch. -All 106 services persist their durable state: S3, SQS, SNS, EventBridge, +All 107 services persist their durable state: S3, SQS, SNS, EventBridge, EventBridge Pipes, EventBridge Scheduler, Lambda, DynamoDB, IAM, STS, SSM, Secrets Manager, CloudWatch Logs, CloudWatch (Metrics & Alarms), KMS, CloudFormation, Cloud Control API, SES, -Cognito User Pools, Cognito Identity, Kinesis, Firehose, RDS, RDS Data API, Aurora DSQL, Resource Groups, Resource Groups Tagging API, ElastiCache, MemoryDB, EKS, AWS Backup, AWS AppConfig, +Cognito User Pools, Cognito Identity, Kinesis, Firehose, RDS, RDS Data API, Aurora DSQL, Resource Groups, Resource Groups Tagging API, ElastiCache, MemoryDB, EKS, AWS Backup, AWS Batch, AWS AppConfig, Step Functions, API Gateway v1, API Gateway v2, Bedrock, Bedrock Agent, Bedrock Agent Runtime, Bedrock Runtime, ECR, ECS, Elastic Load Balancing v2, CloudFront, CloudTrail, Route 53, WAF v2, Application Auto Scaling, Athena, ACM, Glue, @@ -387,6 +387,14 @@ Features: AWS Backup control plane (no real backup engine; models the AWS manage Protocol: REST (restJson1, path-based routing, JSON responses) +### AWS Batch (45 actions, complete) + +CancelJob, CreateComputeEnvironment, CreateConsumableResource, CreateJobQueue, CreateQuotaShare, CreateSchedulingPolicy, CreateServiceEnvironment, DeleteComputeEnvironment, DeleteConsumableResource, DeleteJobQueue, DeleteQuotaShare, DeleteSchedulingPolicy, DeleteServiceEnvironment, DeregisterJobDefinition, DescribeComputeEnvironments, DescribeConsumableResource, DescribeJobDefinitions, DescribeJobQueues, DescribeJobs, DescribeQuotaShare, DescribeSchedulingPolicies, DescribeServiceEnvironments, DescribeServiceJob, GetJobQueueSnapshot, ListConsumableResources, ListJobs, ListJobsByConsumableResource, ListQuotaShares, ListSchedulingPolicies, ListServiceJobs, ListTagsForResource, RegisterJobDefinition, SubmitJob, SubmitServiceJob, TagResource, TerminateJob, TerminateServiceJob, UntagResource, UpdateComputeEnvironment, UpdateConsumableResource, UpdateJobQueue, UpdateQuotaShare, UpdateSchedulingPolicy, UpdateServiceEnvironment, UpdateServiceJob + +Features: Compute environments, job queues (compute- or service-environment ordered), revisioned job definitions, fair-share scheduling policies, consumable resources, service environments, quota shares and tags. `SubmitJob` runs a real container on fakecloud's ECS task engine and drives the job status off its lifecycle (`SUCCEEDED` on exit 0, `FAILED` with the real exit code), with array jobs, `dependsOn`, retries, attempt timeouts and consumable-resource gating; `CancelJob` / `TerminateJob` stop the real container; service jobs queue at `RUNNABLE` (no SageMaker training executor). + +Protocol: REST (restJson1, path-based routing, JSON responses) + ### AWS AppConfig (58 actions, complete) CreateApplication, GetApplication, UpdateApplication, DeleteApplication, ListApplications, CreateEnvironment, GetEnvironment, UpdateEnvironment, DeleteEnvironment, ListEnvironments, CreateConfigurationProfile, GetConfigurationProfile, UpdateConfigurationProfile, DeleteConfigurationProfile, ListConfigurationProfiles, ValidateConfiguration, CreateHostedConfigurationVersion, GetHostedConfigurationVersion, DeleteHostedConfigurationVersion, ListHostedConfigurationVersions, CreateDeploymentStrategy, GetDeploymentStrategy, UpdateDeploymentStrategy, DeleteDeploymentStrategy, ListDeploymentStrategies, StartDeployment, GetDeployment, StopDeployment, ListDeployments, GetConfiguration, CreateExtension, GetExtension, UpdateExtension, DeleteExtension, ListExtensions, CreateExtensionAssociation, GetExtensionAssociation, UpdateExtensionAssociation, DeleteExtensionAssociation, ListExtensionAssociations, CreateExperimentDefinition, GetExperimentDefinition, UpdateExperimentDefinition, DeleteExperimentDefinition, ListExperimentDefinitions, StartExperimentRun, GetExperimentRun, UpdateExperimentRun, StopExperimentRun, ListExperimentRuns, ListExperimentRunEvents, GetAccountSettings, UpdateAccountSettings, TagResource, UntagResource, ListTagsForResource (appconfig); StartConfigurationSession, GetLatestConfiguration (appconfigdata) diff --git a/website/static/llms.txt b/website/static/llms.txt index c3f046486..5295d8d43 100644 --- a/website/static/llms.txt +++ b/website/static/llms.txt @@ -4,8 +4,8 @@ - **Goal:** 100% of AWS services, each at 100% conformance, with 100% of cross-service integrations. Approach is depth-first — a service is added when it passes the full Smithy-model test variants and cross-service wire-ups, not when the API surface looks filled in. - Single static binary (~19 MB), ~300ms startup, ~10 MiB idle memory, no Docker required to run fakecloud itself -- **106 AWS services shipped today, 7,612 operations, true 100% Smithy conformance — 257,288/257,288 generated test variants pass on every commit**, no flake margin and no skipped services. More services land as they hit the conformance bar; roadmap is driven by real-project demand. -- Services: S3, SQS, SNS, EventBridge, EventBridge Pipes, EventBridge Scheduler, Lambda, EC2, DynamoDB, IAM, STS, Organizations, SSM, Secrets Manager, CloudWatch Logs, CloudWatch (Metrics & Alarms), KMS, CloudFormation, Cloud Control API, SES (v2 + v1 inbound), Cognito User Pools, Cognito Identity, Kinesis, Firehose, RDS, RDS Data API, Aurora DSQL, Resource Groups, Resource Groups Tagging API, ElastiCache, MemoryDB, EKS, AWS Backup, AWS AppConfig, Cloud Map, Step Functions, API Gateway v1 (REST), API Gateway v2 (HTTP), Bedrock, Bedrock Agent, Bedrock Agent Runtime, Bedrock Runtime, ECR, ECS, Elastic Load Balancing v2, CloudFront, CloudTrail, Route 53, WAF v2, Application Auto Scaling, Athena, ACM, Glue +- **107 AWS services shipped today, 7,657 operations, true 100% Smithy conformance — 258,545/258,545 generated test variants pass on every commit**, no flake margin and no skipped services. More services land as they hit the conformance bar; roadmap is driven by real-project demand. +- Services: S3, SQS, SNS, EventBridge, EventBridge Pipes, EventBridge Scheduler, Lambda, EC2, DynamoDB, IAM, STS, Organizations, SSM, Secrets Manager, CloudWatch Logs, CloudWatch (Metrics & Alarms), KMS, CloudFormation, Cloud Control API, SES (v2 + v1 inbound), Cognito User Pools, Cognito Identity, Kinesis, Firehose, RDS, RDS Data API, Aurora DSQL, Resource Groups, Resource Groups Tagging API, ElastiCache, MemoryDB, EKS, AWS Backup, AWS Batch, AWS AppConfig, Cloud Map, Step Functions, API Gateway v1 (REST), API Gateway v2 (HTTP), Bedrock, Bedrock Agent, Bedrock Agent Runtime, Bedrock Runtime, ECR, ECS, Elastic Load Balancing v2, CloudFront, CloudTrail, Route 53, WAF v2, Application Auto Scaling, Athena, ACM, Glue - 30+ cross-service integrations: S3 notifications, SNS fan-out, EventBridge rules, DynamoDB Streams, CloudWatch Logs subscriptions, Cognito triggers, API Gateway -> Lambda, Step Functions task integrations, SES inbound -> S3/SNS/Lambda, and more - Real Lambda execution via Docker across 31 runtimes (Node.js 16/18/20/22/24/26, Python 3.8 through 3.15, Java 8/11/17 (Amazon Linux 2 and 2023 variants)/21/25, Go 1.x, Ruby 3.2/3.3/3.4/4.0, .NET 8/10, custom `provided.al2` / `provided.al2023`) - Real stateful services: RDS runs real PostgreSQL/MySQL/MariaDB/Oracle/SQL Server/Db2 via Docker; ElastiCache runs real Redis/Valkey/Memcached via Docker @@ -57,6 +57,7 @@ - **MemoryDB** (45 ops): full control plane for Redis/Valkey clusters, shards, ACLs, users, parameter/subnet groups, snapshots, multi-region clusters, reserved nodes; persisted. Data-plane container backing is a follow-up - **EKS** (70 ops, complete): full Elastic Kubernetes Service control plane — clusters (incl. connected register/deregister), managed node groups, Fargate profiles, add-ons, access entries + access policies, OIDC identity-provider configs, pod-identity associations, upgrade insights, capabilities, encryption config, EKS Anywhere subscriptions (create/describe/list/delete, config + version updates with tracking, cluster-version/add-on/access-policy catalogues, tagging); persisted, `CREATING` -> `ACTIVE` on describe. No real Kubernetes control-plane endpoint - **AWS Backup** (115 ops, complete): full AWS Backup control plane — backup plans (+ versions) + selections, vaults (standard / logically-air-gapped / restore-access) with notifications / access policies / lock config, recovery points, backup / copy / restore / scan jobs (progressed synthetically to a terminal state; `StartBackupJob` records a synthetic recovery point that `DescribeRecoveryPoint` resolves), frameworks, report plans + jobs, legal holds, restore-testing plans + selections, tiering configurations, protected resources, tags, global / region settings; persisted. No real backup engine (control-plane emulation, matching LocalStack Community) +- **AWS Batch** (45 ops, complete): compute environments, job queues (compute- or service-environment ordered), revisioned job definitions, fair-share scheduling policies, consumable resources, service environments, quota shares and tags. `SubmitJob` runs a real container on fakecloud's ECS task engine and drives the job status off its lifecycle (`SUCCEEDED` on exit 0, `FAILED` with the real exit code), with array jobs, `dependsOn`, retries, attempt timeouts and consumable-resource gating; `CancelJob` / `TerminateJob` stop the real container; service jobs queue at `RUNNABLE` (no SageMaker training executor). - **Cloud Map** (30 ops, complete): full AWS Cloud Map (`servicediscovery`) control plane + discovery — HTTP/public-DNS/private-DNS namespaces, services (DnsConfig/HealthCheck + attributes), instance register/deregister/get/list + health status, `DiscoverInstances`/`DiscoverInstancesRevision` lookup, tagging; via the async operation model (mutations return an `OperationId` that settles `SUCCESS` on `GetOperation`); persisted - **Step Functions** (37 ops): state machines, executions, full ASL interpreter (Pass, Task, Choice, Wait, Parallel, Map, Succeed, Fail), Retry/Catch, Lambda/SQS/SNS/EventBridge/DynamoDB task integrations - **API Gateway v1** (124 ops): REST APIs, resources, methods, integrations (`MOCK`/`HTTP`/`HTTP_PROXY`/`AWS_PROXY` Lambda v1.0 proxy event), method/integration responses, deployments, stages with cache flush, models, request validators, authorizers (TOKEN/REQUEST/COGNITO), API keys + usage plans + usage plan keys, VPC links, domain names + base path mappings, client certificates, documentation parts/versions, gateway responses, exports/SDKs, tagging, account diff --git a/website/templates/index.html b/website/templates/index.html index 52c8693d4..861789b4c 100644 --- a/website/templates/index.html +++ b/website/templates/index.html @@ -12,7 +12,7 @@ "applicationCategory": "DeveloperApplication", "applicationSubCategory": "CloudTestingTool", "operatingSystem": "Linux, macOS, Windows", - "description": "Free, open-source local AWS cloud emulator. 106 services, 7,612 operations, 257,288/257,288 Smithy variants pass — true 100% conformance. Single binary, no account, no auth token.", + "description": "Free, open-source local AWS cloud emulator. 107 services, 7,657 operations, 258,545/258,545 Smithy variants pass — true 100% conformance. Single binary, no account, no auth token.", "url": "https://fakecloud.dev", "downloadUrl": "https://github.com/faiscadev/fakecloud/releases", "codeRepository": "https://github.com/faiscadev/fakecloud", @@ -56,7 +56,7 @@

fakecloud

Local AWS cloud emulator for integration tests. Run your app with normal AWS clients, stay fully local, and use fakecloud SDKs when your tests need deeper visibility.

-

106 services. 7,612 operations. 257,288/257,288 Smithy variants pass — true 100% conformance.

+

107 services. 7,657 operations. 258,545/258,545 Smithy variants pass — true 100% conformance.

Get Started Why fakecloud @@ -95,13 +95,13 @@

First-party test clients

Coverage
-

106 AWS services

+

107 AWS services

S3, SQS, SNS, EventBridge, EventBridge Pipes, EventBridge Scheduler, Lambda, EC2, DynamoDB, IAM, STS, Organizations, SSM, Secrets Manager, CloudWatch Logs, CloudWatch (Metrics & Alarms), KMS, CloudFormation, Cloud Control API, SES, Cognito User Pools, Cognito Identity, Kinesis, Firehose, RDS, RDS Data API, Aurora DSQL, Resource Groups, Resource Groups Tagging API, ElastiCache, MemoryDB, EKS, Cloud Map, Step Functions, API Gateway v1 (REST), API Gateway v2 (HTTP), Bedrock, Bedrock Agent, Bedrock Agent Runtime, Bedrock Runtime, ECR, ECS, Elastic Load Balancing v2, CloudFront, Route 53, WAF v2, Application Auto Scaling, Athena, ACM, Glue, and more.

Tested

Conformance tested

-

True 100% conformance across all 7,612 implemented API operations: 257,288/257,288 Smithy-model-generated test variants pass on every commit, backed by end-to-end tests against the official AWS SDKs.

+

True 100% conformance across all 7,657 implemented API operations: 258,545/258,545 Smithy-model-generated test variants pass on every commit, backed by end-to-end tests against the official AWS SDKs.

Real behavior
@@ -191,7 +191,7 @@

How it compares

Startup time~300ms~3s Idle memory~10 MiB~150 MiB Install size~19 MB binary~1 GB Docker image - AWS services106 at true 100% conformance30+ (paid-only past Community) + AWS services107 at true 100% conformance30+ (paid-only past Community) Test assertion SDKsTypeScript, Python, Go, PHP, Java, Rust, .NETPython, Java Cognito User Pools132 operationsPaid only SES v2116 operationsPaid only From 5e0bcace5a8dc6e06fa7d521c17005889247e64b Mon Sep 17 00:00:00 2001 From: Lucas Vieira Date: Sat, 17 Oct 2026 15:24:28 -0300 Subject: [PATCH 5/7] fix(batch): stop timed-out attempts; carry the stop reason to ECS - An attempt that overruns timeout.attemptDurationSeconds has its task stopped before the retry or the FAILED write. - TerminateJob passes the caller's reason to StopTask; background stops use a fixed reason. Stops defer ECS's snapshot and persist once. - A retry keeps the cluster its relaunch returned; a cancelled dependent job's waiter exits at once; one is_terminal predicate backs every terminal check. - Docs: only TerminateJob stops a running container. --- crates/fakecloud-batch/src/extended.rs | 5 +- crates/fakecloud-batch/src/service.rs | 76 ++++++++++++++----- .../tests/batch_real_execution.rs | 13 +++- website/content/docs/parity.md | 2 +- website/content/docs/services/_index.md | 2 +- website/content/supported-services.md | 2 +- website/static/llms-full.txt | 2 +- website/static/llms.txt | 2 +- 8 files changed, 73 insertions(+), 31 deletions(-) diff --git a/crates/fakecloud-batch/src/extended.rs b/crates/fakecloud-batch/src/extended.rs index 7fab13a78..d8c017c3f 100644 --- a/crates/fakecloud-batch/src/extended.rs +++ b/crates/fakecloud-batch/src/extended.rs @@ -250,10 +250,7 @@ pub(crate) fn admit_job(st: &mut BatchState, job_id: &str) -> Admission { let Some(job) = st.jobs.get(job_id) else { return Admission::Admitted; }; - if matches!( - job.get("status").and_then(Value::as_str), - Some("SUCCEEDED" | "FAILED") - ) { + if crate::service::is_terminal(job) { return Admission::Stopped; } let reqs = job_consumable_requirements(job); diff --git a/crates/fakecloud-batch/src/service.rs b/crates/fakecloud-batch/src/service.rs index 2735cd935..586294fc7 100644 --- a/crates/fakecloud-batch/src/service.rs +++ b/crates/fakecloud-batch/src/service.rs @@ -1488,7 +1488,10 @@ impl BatchService { for (cluster, task) in tasks { let ecs = ecs.clone(); let src = bare_request(&req.account_id, &req.region, &req.request_id); - stops.spawn(async move { stop_ecs_task(&ecs, &src, &cluster, &task).await }); + let reason = reason.clone(); + stops.spawn( + async move { stop_ecs_task(&ecs, &src, &cluster, &task, &reason).await }, + ); } while stops.join_next().await.is_some() {} ecs.persist().await; @@ -1747,7 +1750,8 @@ async fn launch_now( }; if !recorded { // Cancelled or terminated while the task launched: it must not run. - stop_ecs_task(&ecs, &src, &cluster, &task_arn).await; + stop_ecs_task(&ecs, &src, &cluster, &task_arn, STOPPED_JOB_REASON).await; + ecs.persist().await; return; } spawn_status_sync( @@ -1804,6 +1808,10 @@ fn spawn_dependency_waiter( tokio::spawn(async move { for _ in 0..1800u32 { tokio::time::sleep(std::time::Duration::from_secs(1)).await; + // Cancelled or terminated while waiting: nothing left to launch. + if job_is_terminal(&ctx.batch_state, &account, &job_id) { + return; + } let statuses: Vec = { let accounts = ctx.batch_state.read(); let jobs = accounts.get(&account).map(|s| &s.jobs); @@ -2050,6 +2058,7 @@ fn spawn_status_sync( }; let max_polls = timeout_secs.unwrap_or(900).min(900) as u32; let mut task = task_arn; + let mut cluster = cluster; let mut attempt: i64 = 1; loop { // Poll this attempt's task until it stops or the timeout elapses. @@ -2060,7 +2069,8 @@ fn spawn_status_sync( if job_is_terminal(&batch_state, &account_id, &job_id) { // Stopped by CancelJob / TerminateJob. Stop this task too in // case the handler's StopTask failed or missed it. - stop_ecs_task(&ecs, &src, &cluster, &task).await; + stop_ecs_task(&ecs, &src, &cluster, &task, STOPPED_JOB_REASON).await; + ecs.persist().await; return; } let resp = match ecs @@ -2110,7 +2120,8 @@ fn spawn_status_sync( // Cancelled or terminated while this attempt ran: keep that outcome. if job_is_terminal(&batch_state, &account_id, &job_id) { - stop_ecs_task(&ecs, &src, &cluster, &task).await; + stop_ecs_task(&ecs, &src, &cluster, &task, STOPPED_JOB_REASON).await; + ecs.persist().await; return; } if succeeded { @@ -2127,6 +2138,19 @@ fn spawn_status_sync( } // Failure (or timeout when `outcome` is None). + if outcome.is_none() { + // The attempt overran: its container must not keep running + // beside a retry or under a FAILED job. + stop_ecs_task( + &ecs, + &src, + &cluster, + &task, + "Job attempt duration exceeded timeout", + ) + .await; + ecs.persist().await; + } let (exit_code, reason) = outcome.unwrap_or((None, Some("Job attempt duration exceeded timeout".into()))); if attempt < max_attempts { @@ -2155,7 +2179,8 @@ fn spawn_status_sync( let relaunched = launch_ecs_task(&ecs, &src, &job_id, &job_name, &container).await; ecs.persist().await; match relaunched { - Ok((_, new_task)) => { + Ok((new_cluster, new_task)) => { + cluster = new_cluster; task = new_task; attempt += 1; let recorded = { @@ -2171,7 +2196,8 @@ fn spawn_status_sync( }; if !recorded { // Stopped while this attempt launched: it must not run. - stop_ecs_task(&ecs, &src, &cluster, &task).await; + stop_ecs_task(&ecs, &src, &cluster, &task, STOPPED_JOB_REASON).await; + ecs.persist().await; return; } continue; @@ -2205,16 +2231,26 @@ fn spawn_status_sync( }); } -/// True once the job reached `SUCCEEDED` / `FAILED`, including through -/// `CancelJob` / `TerminateJob` while its task was still being polled. +/// The ECS stoppedReason for a task stopped because its job already ended. +const STOPPED_JOB_REASON: &str = "Batch job stopped"; + +/// A job is terminal once `SUCCEEDED` or `FAILED`, whether it finished or was +/// stopped by `CancelJob` / `TerminateJob`. Terminal jobs never change again. +pub(crate) fn is_terminal(job: &Value) -> bool { + matches!( + job.get("status").and_then(Value::as_str), + Some("SUCCEEDED" | "FAILED") + ) +} + +/// True once the job is terminal, including through a stop that landed while +/// its task was still being polled. fn job_is_terminal(batch_state: &SharedBatchState, account_id: &str, job_id: &str) -> bool { batch_state .read() .get(account_id) .and_then(|st| st.jobs.get(job_id)) - .and_then(|j| j.get("status")) - .and_then(Value::as_str) - .is_some_and(|s| matches!(s, "SUCCEEDED" | "FAILED")) + .is_some_and(is_terminal) } /// The job's record while it can still change state, or `None` once it is @@ -2227,27 +2263,25 @@ pub(crate) fn live_job<'a>( ) -> Option<&'a mut Map> { st.jobs .get_mut(job_id) + .filter(|j| !is_terminal(j)) .and_then(Value::as_object_mut) - .filter(|j| { - !matches!( - j.get("status").and_then(Value::as_str), - Some("SUCCEEDED" | "FAILED") - ) - }) } -/// Stop an ECS task Batch launched. Best-effort: a failure is logged, and the -/// status poller retries it for a task whose job was stopped. +/// Stop an ECS task Batch launched, recording `reason` as its stoppedReason. +/// Best-effort: a failure is logged, and the status poller retries it for a +/// task whose job was stopped. Defers ECS's snapshot like a launch does; the +/// caller persists once. async fn stop_ecs_task( ecs: &fakecloud_ecs::EcsService, src: &AwsRequest, cluster: &str, task: &str, + reason: &str, ) { if let Err(e) = ecs - .handle(ecs_request( + .handle_deferring_snapshot(ecs_request( "StopTask", - json!({ "cluster": cluster, "task": task, "reason": "Batch job stopped" }), + json!({ "cluster": cluster, "task": task, "reason": reason }), src, )) .await diff --git a/crates/fakecloud-e2e/tests/batch_real_execution.rs b/crates/fakecloud-e2e/tests/batch_real_execution.rs index 2971b6ebb..2179ab7c6 100644 --- a/crates/fakecloud-e2e/tests/batch_real_execution.rs +++ b/crates/fakecloud-e2e/tests/batch_real_execution.rs @@ -327,7 +327,9 @@ async fn timeout_fails_an_overrunning_job() { return; } let s = TestServer::start().await; - let batch = aws_sdk_batch::Client::new(&s.aws_config().await); + let config = s.aws_config().await; + let batch = aws_sdk_batch::Client::new(&config); + let ecs = aws_sdk_ecs::Client::new(&config); setup(&batch, "tmo", vec!["sh", "-c", "sleep 60; exit 0"]).await; let job_id = batch .submit_job() @@ -352,6 +354,15 @@ async fn timeout_fails_an_overrunning_job() { .status_reason() .unwrap_or_default() .contains("timeout")); + // The overrunning container was stopped, not left running for its 60s. + let live = ecs + .list_tasks() + .cluster("fakecloud-batch") + .desired_status(aws_sdk_ecs::types::DesiredStatus::Running) + .send() + .await + .unwrap(); + assert!(live.task_arns().is_empty(), "{:?}", live.task_arns()); } #[tokio::test] diff --git a/website/content/docs/parity.md b/website/content/docs/parity.md index 3b1175d56..3e3b60162 100644 --- a/website/content/docs/parity.md +++ b/website/content/docs/parity.md @@ -90,7 +90,7 @@ fakecloud implements **107 AWS services** with **7,657 operations**. **258,545/2 | [Bedrock Agent Runtime](@/docs/services/bedrock-agent-runtime.md) | 31 | JSON 1.1 | Full | Partial | `InvokeAgent`, `Retrieve`, `RetrieveAndGenerate`, `InvokeFlow`, and the streaming variants are wired end-to-end with shape-correct synthetic chunks. No real foundation-model inference — see Bedrock runtime caveat above. | | [ECR](@/docs/services/ecr.md) | 58 | JSON 1.1 | Full | Full | OCI v2 push/pull is real. Lifecycle policy evaluation, image scanning, pull-through cache, registry templates, and cosign signature verification are all implemented. | | [ECS](@/docs/services/ecs.md) | 77 | JSON 1.1 (Query) | Full | Full | Real Fargate-style task execution via Docker, services with rolling deployments + blue/green lifecycle-hook pauses (`ContinueServiceDeployment`), task sets, container instances, capacity providers, and ECS Exec. Multi-container tasks, volume mounts, health checks, and `dependsOn` ordering are all implemented. | -| [Batch](@/docs/services/batch.md) | 45 | REST-JSON | Full | Full | Complete 45-op AWS Batch surface: compute environments, job queues (compute- or service-environment ordered), revisioned job definitions, fair-share scheduling policies, consumable resources, service environments, quota shares, tags and the job control plane. `SubmitJob` runs the job as a real container on fakecloud's ECS task engine and drives its status off the container's lifecycle (`SUCCEEDED` on exit 0, `FAILED` with the real exit code otherwise), including array jobs, `dependsOn`, retry strategies, attempt timeouts and consumable-resource gating; `CancelJob` / `TerminateJob` stop the real container. Service jobs (`SubmitServiceJob`) are accepted and queued but wait at `RUNNABLE`, since fakecloud's SageMaker has no training executor. With no container runtime, jobs stay `SUBMITTED` rather than reporting a fabricated success. | +| [Batch](@/docs/services/batch.md) | 45 | REST-JSON | Full | Full | Complete 45-op AWS Batch surface: compute environments, job queues (compute- or service-environment ordered), revisioned job definitions, fair-share scheduling policies, consumable resources, service environments, quota shares, tags and the job control plane. `SubmitJob` runs the job as a real container on fakecloud's ECS task engine and drives its status off the container's lifecycle (`SUCCEEDED` on exit 0, `FAILED` with the real exit code otherwise), including array jobs, `dependsOn`, retry strategies, attempt timeouts and consumable-resource gating; `TerminateJob` stops the real container. Service jobs (`SubmitServiceJob`) are accepted and queued but wait at `RUNNABLE`, since fakecloud's SageMaker has no training executor. With no container runtime, jobs stay `SUBMITTED` rather than reporting a fabricated success. | | [ELBv2](@/docs/services/elbv2.md) | 51 | JSON 1.1 (Query) | Full | Partial | Control plane (ALB/NLB/GWLB CRUD, target groups, listeners, rules, mTLS trust stores) is fully implemented. An in-process HTTP data plane for ALBs handles rule matching, forwarding, fixed-response, redirect, and sticky sessions. WAFv2 inspection is wired into the ALB data plane. NLB and GWLB data planes are not implemented. | | [CloudFront](@/docs/services/cloudfront.md) | 167 | REST-XML | Full | Partial | Control plane is fully implemented (distributions, policies, functions, key value stores, etc.). An in-process HTTP data plane serves enabled distributions from their origins on the main listener, routed by `Host` (discovered via `/_fakecloud/cloudfront/distributions`): path-pattern cache-behavior routing, S3 (REST and website) and custom origins, private S3 origins through an origin access control or legacy origin access identity (authorized by the bucket policy under `--iam`), and CustomErrorResponses (the SPA `404 -> /index.html` fallback). Not implemented: a global/geo edge network, in-path CloudFront Functions / Lambda@Edge (CloudFront Functions can still be exercised out-of-band via `TestFunction`; Lambda@Edge cannot), and TTL caching / invalidation. | | [CloudTrail](@/docs/services/cloudtrail.md) | 60 | JSON 1.1 | Full | None | Complete 60-op CloudTrail control plane: trails (create/get/update/delete, `DescribeTrails`, `ListTrails`), per-trail logging that `GetTrailStatus` reflects (`StartLogging`/`StopLogging` toggle `IsLogging`, which starts `false`), event selectors and insight selectors that persist and round-trip, CloudTrail Lake event data stores (full CRUD + `RestoreEventDataStore` PENDING_DELETION -> ENABLED + ingestion start/stop + federation enable/disable), channels, imports, Lake queries (`StartQuery`/`DescribeQuery`/`GetQueryResults` settle to `FINISHED` with empty rows, `CancelQuery`), dashboards, resource policies, organization delegated admins, event configuration, and tagging. `LookupEvents`, `ListPublicKeys`, and `ListInsightsMetricData` return real empty result sets. `@length`/`@range`/enum constraints enforced. Account-partitioned and persisted. No event-recording engine — a fake needn't record its own API activity, matching how LocalStack Community mocks CloudTrail. | diff --git a/website/content/docs/services/_index.md b/website/content/docs/services/_index.md index 253f5d978..a2910dd93 100644 --- a/website/content/docs/services/_index.md +++ b/website/content/docs/services/_index.md @@ -48,7 +48,7 @@ fakecloud implements 107 AWS services with 7,657 total operations. 258,545/258,5 | Amazon MSK | 64 | Complete Amazon MSK (Managed Streaming for Apache Kafka) control plane, signing as `kafka`: provisioned + serverless clusters (async `CREATING` -> `ACTIVE` on describe, monotonic `.../cluster//-` ARNs, name/type filters + pagination, region-scoped, `DescribeClusterV2` union), the eleven `Update*`/`RebootBroker` ops recording settling `ClusterOperation`s, broker-node synthesis (`ListNodes` + `GetBootstrapBrokers`), configurations with base64 `ServerProperties` revisions, SCRAM secrets, cluster policies, client VPC connections, replicators, topics as real control-plane state, the Kafka version catalogs, and ARN-keyed tags; persisted (in-flight lifecycle transitions reconcile on restart). Real Kafka broker data plane + CloudFormation are later batches (all 64 ops) | | Amazon S3 Glacier | 33 | Complete Glacier surface with real data plane: vaults, archive upload/delete storing the real bytes + a computed SHA-256 tree hash, multipart uploads assembled into a stored archive, retrieval + inventory jobs that settle to `Succeeded` on read so `GetJobOutput` returns the exact uploaded bytes (or a JSON inventory), vault notifications, access policy, the vault-lock state machine, tags, data-retrieval policy, provisioned capacity; persisted (archives survive restart). All 33 ops | | AWS Backup | 115 | Complete AWS Backup control plane: backup plans (+ versions) + selections, vaults (standard / logically-air-gapped / restore-access) with notifications / policies / lock, recovery points, backup / copy / restore / scan jobs (progressed to a terminal state; `StartBackupJob` records a synthetic recovery point `DescribeRecoveryPoint` resolves), frameworks, report plans + jobs, legal holds, restore testing, tiering, protected resources, tags, global / region settings; persisted. No real backup engine (all 115 ops) | -| AWS Batch | 45 | Compute environments, job queues (compute- or service-environment ordered), revisioned job definitions, fair-share scheduling policies, consumable resources, service environments, quota shares and tags. `SubmitJob` runs a real container on fakecloud's ECS task engine and drives the job status off its lifecycle (`SUCCEEDED` on exit 0, `FAILED` with the real exit code), with array jobs, `dependsOn`, retries, attempt timeouts and consumable-resource gating; `CancelJob` / `TerminateJob` stop the real container; service jobs queue at `RUNNABLE` (no SageMaker training executor). | +| AWS Batch | 45 | Compute environments, job queues (compute- or service-environment ordered), revisioned job definitions, fair-share scheduling policies, consumable resources, service environments, quota shares and tags. `SubmitJob` runs a real container on fakecloud's ECS task engine and drives the job status off its lifecycle (`SUCCEEDED` on exit 0, `FAILED` with the real exit code), with array jobs, `dependsOn`, retries, attempt timeouts and consumable-resource gating; `TerminateJob` stops the real container; service jobs queue at `RUNNABLE` (no SageMaker training executor). | | OpenSearch Service | 96 | Complete Amazon OpenSearch Service control plane sharing one domain store with Elasticsearch Service (both sign as `es`): domains (create/describe/delete/config persist; `Processing=false`/`Created=true` settle on describe with a synthetic search endpoint), packages, VPC endpoints, cross-cluster connections, applications + capabilities, per-domain data sources + indices, direct-query data sources, reserved instances, tags, instance-type/version/upgrade catalogues; persisted. No real cluster spawned (all 96 ops) | | Elasticsearch Service | 51 | Complete legacy Amazon Elasticsearch Service (`es`, 2015-01-01) over the SAME shared domain store as OpenSearch Service — a domain created through either API is one entity, surfaced here via `ElasticsearchDomainStatus`. Domains, packages, VPC endpoints, cross-cluster search connections, reserved instances, tags, instance-type/version/upgrade catalogues; persisted (all 51 ops) | | AWS AppConfig | 58 | Complete AppConfig across `appconfig` (56 ops) + `appconfigdata` (2 ops): applications, environments, configuration profiles, hosted configuration versions (raw bytes + content type stored/returned verbatim, auto-incrementing version), custom + AWS-predefined deployment strategies, deployments (settle to `COMPLETE`), extensions + associations, experiments, account settings, `ValidateConfiguration`, tags; persisted. Real data plane: `StartConfigurationSession` -> `GetLatestConfiguration` serves the deployed hosted-config bytes | diff --git a/website/content/supported-services.md b/website/content/supported-services.md index 1a2962f6b..4ffc42903 100644 --- a/website/content/supported-services.md +++ b/website/content/supported-services.md @@ -40,7 +40,7 @@ fakecloud provides 100% API conformance across 7,657 operations. Unlike mocks, f - **Amazon S3 Glacier**: 33 operations (complete). Full Glacier surface with a real data plane: vaults (create/describe/delete/list), archive upload/delete storing the real bytes with a computed SHA-256 tree hash, multipart uploads assembled into a stored archive, retrieval and inventory jobs that settle to `Succeeded` on read so `GetJobOutput` returns the exact uploaded bytes (or a JSON inventory), vault notifications, vault access policy, the vault-lock state machine (`InProgress` -> `Locked` with a 24h lock-id expiry), per-vault tags, the account data-retrieval policy, and provisioned capacity, with persistence (archives survive restart). Account-scoped paths accept the literal `-`; archive/upload/job ids and `x-amz-sha256-tree-hash` / `Location` are mirrored in response headers. - **AWS AppConfig**: 58 operations (complete). Full AppConfig across both `appconfig` (56 ops) and `appconfigdata` (2 ops): applications, environments, configuration profiles, hosted configuration versions (the raw request bytes and content type are stored and returned verbatim, with an auto-incrementing version number), custom and AWS-predefined deployment strategies, deployments (settled to `COMPLETE` synchronously with an event log), extensions and extension associations, experiment definitions and runs, account settings, `ValidateConfiguration`, and tagging, with persistence. Real data plane: `StartConfigurationSession` -> `GetLatestConfiguration` resolves a session token to the latest deployed hosted-config bytes. - **AWS Backup**: 115 operations (complete). Full AWS Backup control plane: backup plans (with versions) + selections, backup vaults (standard / logically-air-gapped / restore-access) with notifications, access policies, and lock configuration, recovery points, backup / copy / restore / scan jobs (progressed synthetically to a terminal state so Describe/List show completed work; `StartBackupJob` records a synthetic recovery point that `DescribeRecoveryPoint` resolves), frameworks, report plans + jobs, legal holds, restore-testing plans + selections, tiering configurations, protected resources, tags, and account-scoped global / region settings, with persistence. No real backup engine runs (control-plane emulation, matching LocalStack Community). -- **AWS Batch**: 45 operations (complete). Compute environments, job queues (compute- or service-environment ordered), revisioned job definitions, fair-share scheduling policies, consumable resources, service environments, quota shares and tags. `SubmitJob` runs a real container on fakecloud's ECS task engine and drives the job status off its lifecycle (`SUCCEEDED` on exit 0, `FAILED` with the real exit code), with array jobs, `dependsOn`, retries, attempt timeouts and consumable-resource gating; `CancelJob` / `TerminateJob` stop the real container; service jobs queue at `RUNNABLE` (no SageMaker training executor). +- **AWS Batch**: 45 operations (complete). Compute environments, job queues (compute- or service-environment ordered), revisioned job definitions, fair-share scheduling policies, consumable resources, service environments, quota shares and tags. `SubmitJob` runs a real container on fakecloud's ECS task engine and drives the job status off its lifecycle (`SUCCEEDED` on exit 0, `FAILED` with the real exit code), with array jobs, `dependsOn`, retries, attempt timeouts and consumable-resource gating; `TerminateJob` stops the real container; service jobs queue at `RUNNABLE` (no SageMaker training executor). ### AI & Machine Learning - **Bedrock**: 222 operations across 4 APIs (Bedrock 108, Bedrock Runtime 11, Bedrock Agent 72, Bedrock Agent Runtime 31). Guardrails, Model Customization, Provisioned Throughput, Agents, Knowledge Bases. diff --git a/website/static/llms-full.txt b/website/static/llms-full.txt index 6d9a377cf..49cf48885 100644 --- a/website/static/llms-full.txt +++ b/website/static/llms-full.txt @@ -391,7 +391,7 @@ Protocol: REST (restJson1, path-based routing, JSON responses) CancelJob, CreateComputeEnvironment, CreateConsumableResource, CreateJobQueue, CreateQuotaShare, CreateSchedulingPolicy, CreateServiceEnvironment, DeleteComputeEnvironment, DeleteConsumableResource, DeleteJobQueue, DeleteQuotaShare, DeleteSchedulingPolicy, DeleteServiceEnvironment, DeregisterJobDefinition, DescribeComputeEnvironments, DescribeConsumableResource, DescribeJobDefinitions, DescribeJobQueues, DescribeJobs, DescribeQuotaShare, DescribeSchedulingPolicies, DescribeServiceEnvironments, DescribeServiceJob, GetJobQueueSnapshot, ListConsumableResources, ListJobs, ListJobsByConsumableResource, ListQuotaShares, ListSchedulingPolicies, ListServiceJobs, ListTagsForResource, RegisterJobDefinition, SubmitJob, SubmitServiceJob, TagResource, TerminateJob, TerminateServiceJob, UntagResource, UpdateComputeEnvironment, UpdateConsumableResource, UpdateJobQueue, UpdateQuotaShare, UpdateSchedulingPolicy, UpdateServiceEnvironment, UpdateServiceJob -Features: Compute environments, job queues (compute- or service-environment ordered), revisioned job definitions, fair-share scheduling policies, consumable resources, service environments, quota shares and tags. `SubmitJob` runs a real container on fakecloud's ECS task engine and drives the job status off its lifecycle (`SUCCEEDED` on exit 0, `FAILED` with the real exit code), with array jobs, `dependsOn`, retries, attempt timeouts and consumable-resource gating; `CancelJob` / `TerminateJob` stop the real container; service jobs queue at `RUNNABLE` (no SageMaker training executor). +Features: Compute environments, job queues (compute- or service-environment ordered), revisioned job definitions, fair-share scheduling policies, consumable resources, service environments, quota shares and tags. `SubmitJob` runs a real container on fakecloud's ECS task engine and drives the job status off its lifecycle (`SUCCEEDED` on exit 0, `FAILED` with the real exit code), with array jobs, `dependsOn`, retries, attempt timeouts and consumable-resource gating; `TerminateJob` stops the real container; service jobs queue at `RUNNABLE` (no SageMaker training executor). Protocol: REST (restJson1, path-based routing, JSON responses) diff --git a/website/static/llms.txt b/website/static/llms.txt index 5295d8d43..0a3995de3 100644 --- a/website/static/llms.txt +++ b/website/static/llms.txt @@ -57,7 +57,7 @@ - **MemoryDB** (45 ops): full control plane for Redis/Valkey clusters, shards, ACLs, users, parameter/subnet groups, snapshots, multi-region clusters, reserved nodes; persisted. Data-plane container backing is a follow-up - **EKS** (70 ops, complete): full Elastic Kubernetes Service control plane — clusters (incl. connected register/deregister), managed node groups, Fargate profiles, add-ons, access entries + access policies, OIDC identity-provider configs, pod-identity associations, upgrade insights, capabilities, encryption config, EKS Anywhere subscriptions (create/describe/list/delete, config + version updates with tracking, cluster-version/add-on/access-policy catalogues, tagging); persisted, `CREATING` -> `ACTIVE` on describe. No real Kubernetes control-plane endpoint - **AWS Backup** (115 ops, complete): full AWS Backup control plane — backup plans (+ versions) + selections, vaults (standard / logically-air-gapped / restore-access) with notifications / access policies / lock config, recovery points, backup / copy / restore / scan jobs (progressed synthetically to a terminal state; `StartBackupJob` records a synthetic recovery point that `DescribeRecoveryPoint` resolves), frameworks, report plans + jobs, legal holds, restore-testing plans + selections, tiering configurations, protected resources, tags, global / region settings; persisted. No real backup engine (control-plane emulation, matching LocalStack Community) -- **AWS Batch** (45 ops, complete): compute environments, job queues (compute- or service-environment ordered), revisioned job definitions, fair-share scheduling policies, consumable resources, service environments, quota shares and tags. `SubmitJob` runs a real container on fakecloud's ECS task engine and drives the job status off its lifecycle (`SUCCEEDED` on exit 0, `FAILED` with the real exit code), with array jobs, `dependsOn`, retries, attempt timeouts and consumable-resource gating; `CancelJob` / `TerminateJob` stop the real container; service jobs queue at `RUNNABLE` (no SageMaker training executor). +- **AWS Batch** (45 ops, complete): compute environments, job queues (compute- or service-environment ordered), revisioned job definitions, fair-share scheduling policies, consumable resources, service environments, quota shares and tags. `SubmitJob` runs a real container on fakecloud's ECS task engine and drives the job status off its lifecycle (`SUCCEEDED` on exit 0, `FAILED` with the real exit code), with array jobs, `dependsOn`, retries, attempt timeouts and consumable-resource gating; `TerminateJob` stops the real container; service jobs queue at `RUNNABLE` (no SageMaker training executor). - **Cloud Map** (30 ops, complete): full AWS Cloud Map (`servicediscovery`) control plane + discovery — HTTP/public-DNS/private-DNS namespaces, services (DnsConfig/HealthCheck + attributes), instance register/deregister/get/list + health status, `DiscoverInstances`/`DiscoverInstancesRevision` lookup, tagging; via the async operation model (mutations return an `OperationId` that settles `SUCCESS` on `GetOperation`); persisted - **Step Functions** (37 ops): state machines, executions, full ASL interpreter (Pass, Task, Choice, Wait, Parallel, Map, Succeed, Fail), Retry/Catch, Lambda/SQS/SNS/EventBridge/DynamoDB task integrations - **API Gateway v1** (124 ops): REST APIs, resources, methods, integrations (`MOCK`/`HTTP`/`HTTP_PROXY`/`AWS_PROXY` Lambda v1.0 proxy event), method/integration responses, deployments, stages with cache flush, models, request validators, authorizers (TOKEN/REQUEST/COGNITO), API keys + usage plans + usage plan keys, VPC links, domain names + base path mappings, client certificates, documentation parts/versions, gateway responses, exports/SDKs, tagging, account From 3c83463f6813c7c4fb95ac6f7fe9e377e90b31a3 Mon Sep 17 00:00:00 2001 From: Lucas Vieira Date: Sat, 17 Oct 2026 15:33:40 -0300 Subject: [PATCH 6/7] fix(batch): leave timeouts and finished tasks to the poller as before Cut back the last commit's scope: the timeout-path StopTask and the poller's own re-stop are removed (the re-stop overwrote the caller's StopTask reason and an exited task's real stop reason; the timeout stop also fired on the poll cap and on lookup failures). TerminateJob still stops the task, and a task launched for an already-stopped job is still stopped. A retry records its cluster with its task, and the parity row's data plane reads Partial (service jobs never execute). --- crates/fakecloud-batch/src/service.rs | 31 +++++-------------- .../tests/batch_real_execution.rs | 28 ++++++++++------- website/content/docs/parity.md | 2 +- 3 files changed, 24 insertions(+), 37 deletions(-) diff --git a/crates/fakecloud-batch/src/service.rs b/crates/fakecloud-batch/src/service.rs index 586294fc7..20820aad8 100644 --- a/crates/fakecloud-batch/src/service.rs +++ b/crates/fakecloud-batch/src/service.rs @@ -1749,9 +1749,9 @@ async fn launch_now( } }; if !recorded { - // Cancelled or terminated while the task launched: it must not run. + // Cancelled or terminated while the task launched: it must not + // run. The caller persists ECS after the launch. stop_ecs_task(&ecs, &src, &cluster, &task_arn, STOPPED_JOB_REASON).await; - ecs.persist().await; return; } spawn_status_sync( @@ -2067,10 +2067,8 @@ fn spawn_status_sync( for _ in 0..max_polls { tokio::time::sleep(std::time::Duration::from_secs(1)).await; if job_is_terminal(&batch_state, &account_id, &job_id) { - // Stopped by CancelJob / TerminateJob. Stop this task too in - // case the handler's StopTask failed or missed it. - stop_ecs_task(&ecs, &src, &cluster, &task, STOPPED_JOB_REASON).await; - ecs.persist().await; + // Stopped by CancelJob / TerminateJob, which stopped the + // task with the caller's reason. return; } let resp = match ecs @@ -2120,8 +2118,6 @@ fn spawn_status_sync( // Cancelled or terminated while this attempt ran: keep that outcome. if job_is_terminal(&batch_state, &account_id, &job_id) { - stop_ecs_task(&ecs, &src, &cluster, &task, STOPPED_JOB_REASON).await; - ecs.persist().await; return; } if succeeded { @@ -2138,19 +2134,6 @@ fn spawn_status_sync( } // Failure (or timeout when `outcome` is None). - if outcome.is_none() { - // The attempt overran: its container must not keep running - // beside a retry or under a FAILED job. - stop_ecs_task( - &ecs, - &src, - &cluster, - &task, - "Job attempt duration exceeded timeout", - ) - .await; - ecs.persist().await; - } let (exit_code, reason) = outcome.unwrap_or((None, Some("Job attempt duration exceeded timeout".into()))); if attempt < max_attempts { @@ -2188,6 +2171,7 @@ fn spawn_status_sync( match live_job(accounts.get_or_create(&account_id), &job_id) { Some(j) => { j.insert("status".into(), json!("STARTING")); + j.insert("ecsCluster".into(), json!(cluster)); j.insert("ecsTaskArn".into(), json!(task)); true } @@ -2268,9 +2252,8 @@ pub(crate) fn live_job<'a>( } /// Stop an ECS task Batch launched, recording `reason` as its stoppedReason. -/// Best-effort: a failure is logged, and the status poller retries it for a -/// task whose job was stopped. Defers ECS's snapshot like a launch does; the -/// caller persists once. +/// Best-effort: a failure is logged. Defers ECS's snapshot like a launch +/// does; the caller persists once. async fn stop_ecs_task( ecs: &fakecloud_ecs::EcsService, src: &AwsRequest, diff --git a/crates/fakecloud-e2e/tests/batch_real_execution.rs b/crates/fakecloud-e2e/tests/batch_real_execution.rs index 2179ab7c6..c1a955ebf 100644 --- a/crates/fakecloud-e2e/tests/batch_real_execution.rs +++ b/crates/fakecloud-e2e/tests/batch_real_execution.rs @@ -327,9 +327,7 @@ async fn timeout_fails_an_overrunning_job() { return; } let s = TestServer::start().await; - let config = s.aws_config().await; - let batch = aws_sdk_batch::Client::new(&config); - let ecs = aws_sdk_ecs::Client::new(&config); + let batch = aws_sdk_batch::Client::new(&s.aws_config().await); setup(&batch, "tmo", vec!["sh", "-c", "sleep 60; exit 0"]).await; let job_id = batch .submit_job() @@ -354,15 +352,6 @@ async fn timeout_fails_an_overrunning_job() { .status_reason() .unwrap_or_default() .contains("timeout")); - // The overrunning container was stopped, not left running for its 60s. - let live = ecs - .list_tasks() - .cluster("fakecloud-batch") - .desired_status(aws_sdk_ecs::types::DesiredStatus::Running) - .send() - .await - .unwrap(); - assert!(live.task_arns().is_empty(), "{:?}", live.task_arns()); } #[tokio::test] @@ -462,6 +451,21 @@ async fn terminate_job_stops_the_running_container() { .await .unwrap(); assert!(live.task_arns().is_empty(), "{:?}", live.task_arns()); + let stopped = ecs + .list_tasks() + .cluster("fakecloud-batch") + .desired_status(aws_sdk_ecs::types::DesiredStatus::Stopped) + .send() + .await + .unwrap(); + let tasks = ecs + .describe_tasks() + .cluster("fakecloud-batch") + .set_tasks(Some(stopped.task_arns().to_vec())) + .send() + .await + .unwrap(); + assert_eq!(tasks.tasks().len(), 1, "{:?}", stopped.task_arns()); // Past the point the container would have exited 0, the job is still the // FAILED the user asked for: the status poller does not revive it. diff --git a/website/content/docs/parity.md b/website/content/docs/parity.md index 3e3b60162..6af58817e 100644 --- a/website/content/docs/parity.md +++ b/website/content/docs/parity.md @@ -90,7 +90,7 @@ fakecloud implements **107 AWS services** with **7,657 operations**. **258,545/2 | [Bedrock Agent Runtime](@/docs/services/bedrock-agent-runtime.md) | 31 | JSON 1.1 | Full | Partial | `InvokeAgent`, `Retrieve`, `RetrieveAndGenerate`, `InvokeFlow`, and the streaming variants are wired end-to-end with shape-correct synthetic chunks. No real foundation-model inference — see Bedrock runtime caveat above. | | [ECR](@/docs/services/ecr.md) | 58 | JSON 1.1 | Full | Full | OCI v2 push/pull is real. Lifecycle policy evaluation, image scanning, pull-through cache, registry templates, and cosign signature verification are all implemented. | | [ECS](@/docs/services/ecs.md) | 77 | JSON 1.1 (Query) | Full | Full | Real Fargate-style task execution via Docker, services with rolling deployments + blue/green lifecycle-hook pauses (`ContinueServiceDeployment`), task sets, container instances, capacity providers, and ECS Exec. Multi-container tasks, volume mounts, health checks, and `dependsOn` ordering are all implemented. | -| [Batch](@/docs/services/batch.md) | 45 | REST-JSON | Full | Full | Complete 45-op AWS Batch surface: compute environments, job queues (compute- or service-environment ordered), revisioned job definitions, fair-share scheduling policies, consumable resources, service environments, quota shares, tags and the job control plane. `SubmitJob` runs the job as a real container on fakecloud's ECS task engine and drives its status off the container's lifecycle (`SUCCEEDED` on exit 0, `FAILED` with the real exit code otherwise), including array jobs, `dependsOn`, retry strategies, attempt timeouts and consumable-resource gating; `TerminateJob` stops the real container. Service jobs (`SubmitServiceJob`) are accepted and queued but wait at `RUNNABLE`, since fakecloud's SageMaker has no training executor. With no container runtime, jobs stay `SUBMITTED` rather than reporting a fabricated success. | +| [Batch](@/docs/services/batch.md) | 45 | REST-JSON | Full | Partial | Complete 45-op AWS Batch surface: compute environments, job queues (compute- or service-environment ordered), revisioned job definitions, fair-share scheduling policies, consumable resources, service environments, quota shares, tags and the job control plane. `SubmitJob` runs the job as a real container on fakecloud's ECS task engine and drives its status off the container's lifecycle (`SUCCEEDED` on exit 0, `FAILED` with the real exit code otherwise), including array jobs, `dependsOn`, retry strategies, attempt timeouts and consumable-resource gating; `TerminateJob` stops the real container. Service jobs (`SubmitServiceJob`) are accepted and queued but wait at `RUNNABLE`, since fakecloud's SageMaker has no training executor. With no container runtime, jobs stay `SUBMITTED` rather than reporting a fabricated success. | | [ELBv2](@/docs/services/elbv2.md) | 51 | JSON 1.1 (Query) | Full | Partial | Control plane (ALB/NLB/GWLB CRUD, target groups, listeners, rules, mTLS trust stores) is fully implemented. An in-process HTTP data plane for ALBs handles rule matching, forwarding, fixed-response, redirect, and sticky sessions. WAFv2 inspection is wired into the ALB data plane. NLB and GWLB data planes are not implemented. | | [CloudFront](@/docs/services/cloudfront.md) | 167 | REST-XML | Full | Partial | Control plane is fully implemented (distributions, policies, functions, key value stores, etc.). An in-process HTTP data plane serves enabled distributions from their origins on the main listener, routed by `Host` (discovered via `/_fakecloud/cloudfront/distributions`): path-pattern cache-behavior routing, S3 (REST and website) and custom origins, private S3 origins through an origin access control or legacy origin access identity (authorized by the bucket policy under `--iam`), and CustomErrorResponses (the SPA `404 -> /index.html` fallback). Not implemented: a global/geo edge network, in-path CloudFront Functions / Lambda@Edge (CloudFront Functions can still be exercised out-of-band via `TestFunction`; Lambda@Edge cannot), and TTL caching / invalidation. | | [CloudTrail](@/docs/services/cloudtrail.md) | 60 | JSON 1.1 | Full | None | Complete 60-op CloudTrail control plane: trails (create/get/update/delete, `DescribeTrails`, `ListTrails`), per-trail logging that `GetTrailStatus` reflects (`StartLogging`/`StopLogging` toggle `IsLogging`, which starts `false`), event selectors and insight selectors that persist and round-trip, CloudTrail Lake event data stores (full CRUD + `RestoreEventDataStore` PENDING_DELETION -> ENABLED + ingestion start/stop + federation enable/disable), channels, imports, Lake queries (`StartQuery`/`DescribeQuery`/`GetQueryResults` settle to `FINISHED` with empty rows, `CancelQuery`), dashboards, resource policies, organization delegated admins, event configuration, and tagging. `LookupEvents`, `ListPublicKeys`, and `ListInsightsMetricData` return real empty result sets. `@length`/`@range`/enum constraints enforced. Account-partitioned and persisted. No event-recording engine — a fake needn't record its own API activity, matching how LocalStack Community mocks CloudTrail. | From 87b259c5410cf51a5428c0e6d1051c742832f5b5 Mon Sep 17 00:00:00 2001 From: Lucas Vieira Date: Sun, 18 Oct 2026 13:17:11 -0300 Subject: [PATCH 7/7] test(batch): tag a real resource in the encoded-ARN e2e TagResource now rejects an ARN naming no resource, as AWS does, so the test creates a scheduling policy and tags its ARN instead. --- .../fakecloud-e2e/tests/encoded_path_labels.rs | 17 ++++++++++++++--- 1 file changed, 14 insertions(+), 3 deletions(-) diff --git a/crates/fakecloud-e2e/tests/encoded_path_labels.rs b/crates/fakecloud-e2e/tests/encoded_path_labels.rs index ac632afd5..a53a3b2f4 100644 --- a/crates/fakecloud-e2e/tests/encoded_path_labels.rs +++ b/crates/fakecloud-e2e/tests/encoded_path_labels.rs @@ -107,17 +107,28 @@ async fn lambda_tags_with_encoded_function_arn() { async fn batch_tags_with_encoded_arn() { let server = TestServer::start().await; let batch = aws_sdk_batch::Client::new(&server.aws_config().await); - let arn = "arn:aws:batch:us-east-1:123456789012:job-queue/encoded-q"; + // Tags attach only to a resource that exists, so create one first; its + // ARN carries a `/` that the client percent-encodes into the label. + let arn = batch + .create_scheduling_policy() + .name("encoded-policy") + .send() + .await + .unwrap() + .arn() + .unwrap() + .to_string(); + assert!(arn.contains('/'), "{arn}"); batch .tag_resource() - .resource_arn(arn) + .resource_arn(&arn) .set_tags(Some(one_tag())) .send() .await .unwrap(); let out = batch .list_tags_for_resource() - .resource_arn(arn) + .resource_arn(&arn) .send() .await .unwrap();