feat(aws): ECS SDK-compat service (#159) + EC2 managed-resource visibility (#300) - #302
Conversation
…ce visibility (stackshy#300) Two related AWS features (ECS Managed Instances are a producer of managed EC2 instances, so stackshy#300 composes with stackshy#159). ## ECS SDK-compat service (stackshy#159) New AWS ECS service so the real aws-sdk-go-v2/service/ecs client works against cloudemu over AWS JSON 1.1 (X-Amz-Target AmazonEC2ContainerServiceV20141113.*), across all four layers (driver → in-memory provider → portable API → SDK-compat server) and registered in the AWS server. All 19 operations from the ticket: - Clusters: CreateCluster, ListClusters, DescribeClusters, DeleteCluster - Task definitions: RegisterTaskDefinition (auto-incrementing revision per family), ListTaskDefinitions, DescribeTaskDefinition (family / family:revision / ARN; latest ACTIVE for a bare family), DeregisterTaskDefinition (→ INACTIVE) - Tasks: RunTask, StopTask, ListTasks, DescribeTasks - Services: CreateService (duplicate name → conflict), UpdateService, ListServices, DescribeServices, DeleteService - Container instances: ListContainerInstances, DescribeContainerInstances Resources complete synchronously (RunTask → RUNNING, CreateService reaches desiredCount). The batch Describe* ops and RunTask return partial success — unresolved ids appear in failures[]{arn,reason:"MISSING"} rather than erroring. DescribeClusters honors the include parameter (TAGS / SETTINGS gated), matching real ECS. Cluster defaults to "default" when omitted. ## EC2 managed-resource visibility (stackshy#300) Emulate AWS managed-resource visibility on DescribeInstances: - Instance model carries an Operator marker (Managed, Principal); instances can be seeded managed at launch (InstanceConfig.Managed/Principal) or via SetManaged. Managed instances carry aws:*-prefixed system tags (e.g. aws:ec2:managed-launch), which round-trip. - Account-level managed-resource-visibility setting (visible|hidden) via SetManagedResourceVisibility on the Go API. - DescribeInstances honors IncludeManagedResources. Behavior matrix (managed instance in state): hidden+no-flag → omitted; hidden+flag → included; visible → always included. Applies to list, filtered, and explicit-InstanceIds requests alike. Non-managed instances are always returned. Works on both the Go API and the query-protocol SDK server; the response Operator block (<operator><managed>/<principal>/<hiddenByDefault>) matches the ec2 SDK. The compute driver's DescribeInstances gained a trailing variadic options param (source-compatible for existing 3-arg callers); all implementers (GCP/Azure/ chaos/ssm) updated. ## Deps Add aws-sdk-go-v2/service/ecs v1.89.1; bump service/ec2 to v1.317.1 (adds the DescribeInstances IncludeManagedResources request parameter). ## Verified go build/vet/gofmt clean; golangci-lint introduces zero new findings; go test ./... passes (210 packages, 0 failures); go test -race clean on the ecs/ec2 packages; and exhaustive real-SDK end-to-end runs against the full server pass for both features (all 19 ECS ops + the full EC2 visibility matrix). docs/ services.md updated (new Container Orchestration service + EC2 managed-visibility section + counts). Closes stackshy#159 Closes stackshy#300
…-support # Conflicts: # docs/services.md
b9d1e89 to
36e18ef
Compare
CodeQL flagged a high-severity 'slice memory allocation with excessive size value' at providers/aws/ecs/tasks.go: the RunTask result slice was allocated with make([]Task, 0, count) using the caller-supplied count unbounded, so a large count could drive an excessive allocation. Reject count > 10 with a ValidationException before allocating (matching the AWS ECS RunTask limit), which bounds the allocation to a constant maximum. Added a regression test covering the max, over-max, and absurd-count cases.
NitinKumar004
left a comment
There was a problem hiding this comment.
Review — ECS SDK-compat (#159) + EC2 managed-resource visibility (#300)
Deep end-to-end pass (5-agent fan-out + direct verification): architecture conformance, all edge cases, blast radius/side-effects, and a full "what's left for real ECS parity" gap analysis. Build/tests pass.
✅ Architecture conformance — PASS
The ECS service is idiomatic and matches the bedrock reference: all 4 layers (driver → memstore-backed provider Mock → portable services/ecs/ecs.go with a real do() wrapper, incl. a doBatch[T] generic that threads the partial-success shape → AWS JSON 1.1 handler dispatched on X-Amz-Target prefix AmazonEC2ContainerServiceV20141113.). idgen for every ARN (correct long-ARN shapes), SortedValues() for list determinism, registration wired in both factories.
✅ Blast radius — clean (no regressions)
- The compute-driver
DescribeInstancesgained a variadicopts ...DescribeInstancesOptions, so all existing 3-arg callers compile and behave identically; the chaos wrapper and portable layer forwardopts...(not dropped). - Managed instances are visible by default — hiding needs explicit opt-in on both axes (
Managed=trueANDSetManagedResourceVisibility("hidden")), and non-managed instances are provably never filtered. So no existing launch-then-describe flow changes. - ECS
X-Amz-Targetprefix is disjoint from every other JSON handler (no shadowing);go mod verifyclean; factory wiring additive/nil-safe.
🔴 HIGH — real correctness bugs (ECS)
- Concurrency + aliasing data corruption. ECS omits both halves of the repo's copy-on-write discipline (bedrock does both). Mutators write through the stored pointer with no lock —
StopTask(tasks.go:80-87, incl.t.Containers[i].LastStatus),DeleteCluster(clusters.go:86),DeregisterTaskDefinition(taskdefs.go:103),UpdateService/DeleteService(services.go:67,126) — while Describe/List return*storedshallow copies that aliasContainers/Tags/Settings/ContainerDefinitions. Concrete:RunTask/DescribeTaskshand back a task whoseContainersaliases the store; a laterStopTaskflipsLastStatusin that shared backing array, retroactively mutating every previously-returned snapshot — plus a genuine-racedata race. There is no concurrency test, so the reported "-race clean" is vacuous for this class. Fix: copy-on-write on mutate + deep-copy slice fields on read (mirrorcloneGuardrail). DeleteClusterhas no cascade guard. It just flipsStatus=INACTIVEregardless of active services / running tasks / registered container instances (clusters.go:78). Real ECS refuses withClusterContainsServicesException/ClusterContainsTasksException/ClusterContainsContainerInstancesException— none of which exist inerrors.go. So a non-empty cluster deletes and orphans its services.DeleteServiceignoresdesiredCountand has noforce. AWS rejects deleting a service scaled >0 withoutforce(InvalidParameterException); the mock always deletes, andforceisn't even representable (absent from the driver, portable, and wire request). The SDK round-trip test cements the wrong behavior.
🟠 MEDIUM (ECS correctness/fidelity)
- No referential integrity on writes:
RunTask/CreateService/DeleteService/ListTasksdon't validate the cluster exists (should beClusterNotFoundException);CreateServicedoesn't validate the task-def exists (should beClientException). You can run a task into aghostcluster. RunTaskwith a missing task-def returnsfailures[]{MISSING}— real ECS throwsClientExceptionsynchronously (failures[] is for placement/capacity only). The batchDescribe*partial-success is correct; RunTask isn't.CreateServiceblocks reusing an INACTIVE (deleted) service name (Hasis true for the lingering INACTIVE record) — AWS only blocks while ACTIVE, so delete-then-recreate fails.- Cluster counters are always 0 —
RunningTasksCount/PendingTasksCount/ActiveServicesCount/RegisteredContainerInstancesCountare never updated, soDescribeClustersreports every cluster idle regardless of load (and makes a counter-based cascade check impossible). ListTaskDefinitionsmis-orders at ≥10 revisions —SortedValues()sorts thefamily:revisionstore key withsort.Strings, soweb:10<web:2(verified:[web:1 web:10 web:11 web:2 web:3]). ThesortASC/DESC param is also not decoded.- Error mapping loses typed exceptions — all
NotFound→ genericClientException; SDK callers matchingerrors.As(&types.ClusterNotFoundException{})/ServiceNotFoundExceptionwon't match. Notable for a "SDK-compat" PR. - Create TOCTOU —
CreateCluster/CreateServiceuseHas-then-Setinstead ofmemstore.SetIfAbsent; concurrent same-name creates both pass the guard.
🟠 MEDIUM — EC2 visibility cross-subsystem inconsistencies (silent, undocumented)
"Hidden" is only a DescribeInstances-layer filter; the instance stays fully live elsewhere, and internal callers that route through DescribeInstances without passing opts inherit the default-hide:
- CloudWatch metrics leak the hidden instance (metrics emitted on launch/lifecycle with the real
InstanceIddimension) — the sharpest leak: hidden in Describe, visible in monitoring. - Mutable by ID — Start/Stop/Reboot/Terminate resolve by
Getwith no visibility check. - resourcediscovery/RE2, topology
CanConnect, and SSMRunCommandall drop hidden managed instances (each callsDescribeInstances(ctx, …, )with no opts) → topology reports a running instance as NotFound; SSM rejects it asInvalidInstanceId. None documented. - LOW:
managedResourceVisibilityis unsynchronized shared state (-raceif toggled concurrently; contradicts the "all mocks use RWMutex" rule);SetManagedResourceVisibilityaccepts any string (a typo silently means "visible");<hiddenByDefault>XML is a cloudemu invention the real SDK ignores (the "matches the deserializer" comment is inaccurate); explicit-ID describe of a hidden instance returns empty rather thanInvalidInstanceID.NotFound.
📋 Gap analysis — what's left for 100% real-AWS ECS (the headline ask)
The PR models cluster/task-def/task/service/container-instance identity and lifecycle well, but almost none of the scheduling, networking, or task-runtime semantics that make ECS ECS. Most advanced fields aren't even decoded off the wire (absent), and the few that are decoded are echoed but never acted on (accepted-but-ignored → misleading).
1. Launch types — Fargate vs EC2 (the core gap): launchType is a pass-through label — FARGATE / EC2 / EXTERNAL all take the identical path and spring to RUNNING. No validation against the task-def's requiresCompatibilities. No EC2 placement at all: tasks are never placed onto a container instance, no CPU/memory reservation/decrement, no bin-pack/spread, no "no capacity → PENDING/RESOURCE failure" — you can RunTask launchType=EC2 into a cluster with zero container instances and get RUNNING. capacityProviderStrategy / capacity providers entirely absent (no FARGATE_SPOT, no ASG providers, no PutClusterCapacityProviders). Fargate: platformVersion not modeled; networkConfiguration.awsvpcConfiguration (subnets/SGs) not decoded and not required (real Fargate errors without it); no ENI attachments[]/private IP on tasks; no CPU/memory-combo validation.
2. Networking modes: networkMode stored opaque — awsvpc/bridge/host/none indistinguishable; networkConfiguration absent everywhere.
3. Load balancer / service discovery: loadBalancers and serviceRegistries not decoded on CreateService — silently dropped (no target registration, no health-check grace).
4. Service scheduling/deployments: CreateService fakes instant convergence (RunningCount=DesiredCount, no actual tasks launched, no service→task linkage). No deployments[]/events[] (the steady-state signal callers poll), no deploymentController/deploymentConfiguration, no circuit-breaker/rollback, DAEMON vs REPLICA not acted on, no task sets, no Application Auto Scaling. UpdateService supports only taskDefinition+desiredCount.
5. Task-definition runtime surface (silently dropped on register): ContainerDefinition models 8 basic fields; absent are healthCheck, dependsOn/ordering, essential-exit semantics, stopTimeout, secrets/environmentFiles, volumes/mountPoints (EFS/bind/docker), ephemeralStorage, logConfiguration (awslogs), resourceRequirements (GPU), ulimits/linuxParameters, pidMode/ipcMode, runtimePlatform, proxyConfiguration; taskRoleArn/executionRoleArn stored but never enforced. No RegisterTaskDefinition validation (empty containers registers fine).
6. Missing operations: Tag/Untag/ListTagsForResource; account settings (Put/List/Delete); RegisterContainerInstance / DeregisterContainerInstance / UpdateContainerInstancesState (DRAINING); capacity-provider ops; task-set ops; ExecuteCommand (ECS Exec); UpdateCluster/UpdateClusterSettings; Put/Delete/ListAttributes; ListTaskDefinitionFamilies; ListServicesByNamespace.
7. Container instances are test-only: the sole way one exists is the Go helper SeedContainerInstance (callers: tests only) — there's no RegisterContainerInstance API, so over the real SDK List/DescribeContainerInstances is always empty. And despite the "#300 produces managed EC2 instances" framing, ECS and EC2 are not wired — SeedContainerInstance takes an opaque EC2InstanceID string, never creates an EC2 instance or calls SetManaged, so #159 and #300 don't actually compose.
Docs (services.md §24) acknowledge only "resources complete synchronously"; everything above is a silent gap. Suggest documenting the accepted-but-ignored fields (launchType, loadBalancers, essential, task/execution roles) so callers aren't misled, and prioritizing: EC2 placement+capacity → launch-type branching+Fargate networkConfiguration → real service deployment model → LB/serviceRegistries → task-def runtime fields → the missing ops.
Bottom line
Architecture and blast radius are solid — this is a well-structured, idiomatic scaffold. But before it's "faithful" it needs the HIGH fixes (the copy-on-write/aliasing corruption with a -race test, DeleteCluster cascade guards, DeleteService force/desiredCount), the MEDIUM referential-integrity + typed-error + revision-ordering items, and a decision on the EC2 hidden-instance cross-subsystem consistency. The parity gap is large and mostly about scheduling/runtime semantics (Fargate-vs-EC2 placement first) — reasonable to land in waves, but the currently accepted-but-ignored fields should be documented so they don't read as working.
Addresses NitinKumar004's deep review on PR stackshy#302 across four waves. Wave 1 — correctness bugs (HIGH+MEDIUM): - Copy-on-write on every mutator + deep-copy slice/map fields on read (clone.go), killing the aliasing/data-race class; added -race concurrency tests. - DeleteCluster cascade guard (ClusterContainsServices/Tasks/ContainerInstances exceptions) backed by real cluster counters. - DeleteService force + desiredCount guard (InvalidParameterException without force). - Referential integrity: RunTask/CreateService/DeleteService/ListTasks validate the cluster (ClusterNotFoundException); CreateService validates the task-def. - RunTask on a missing task-def now errors (ClientException) instead of failures[]. - CreateService allows reusing an INACTIVE service name; cluster counters maintained; ListTaskDefinitions sorts by NUMERIC revision + honors sort ASC/DESC. - Typed exceptions mapped so SDK errors.As matches; Create* use SetIfAbsent (no TOCTOU). - EC2 stackshy#300: visibility setting RWMutex-guarded + validated; explicit-id describe of a hidden instance → InvalidInstanceID.NotFound; managed instances suppress instance-dimensioned CloudWatch metrics; resourcediscovery/topology/SSM include managed instances; CodeQL slice-alloc fixed (constant capacity + count<=10 guard). Wave 2 — EC2/Fargate placement + capacity engine: - Container instances carry CPU/memory capacity; EC2 RunTask/services are first-fit placed (reserve on run, release on stop); no capacity -> failures[]/PENDING. - launchType validated vs requiresCompatibilities; FARGATE requires awsvpc networkConfiguration + cpu/memory and synthesizes an ENI attachment + platformVersion. Wave 3 — service deployment model: - Real service->task convergence (launches DesiredCount tasks, running/pending counts), deployments[]/events[], DAEMON vs REPLICA, deploymentController/configuration, loadBalancers + serviceRegistries round-trip, full UpdateService surface. Wave 4 — task-def runtime fields + missing ops + EC2 compose: - Container/task-def runtime surface (portMappings, environment, secrets, healthCheck, logConfiguration, mountPoints, ulimits, resourceRequirements, volumes, ephemeralStorage, runtimePlatform, proxyConfiguration, ...) round-trips; register validation (essential defaults true, duplicate/empty names rejected). - Added ops: Tag/Untag/ListTagsForResource, PutAccountSetting(+Default)/List/Delete, RegisterContainerInstance/DeregisterContainerInstance/UpdateContainerInstancesState, UpdateCluster/UpdateClusterSettings/PutClusterCapacityProviders, Put/Delete/ListAttributes, ListTaskDefinitionFamilies, ExecuteCommand (37 ops total). - ECS now composes with stackshy#300: RegisterContainerInstance provisions a backing managed EC2 instance (Operator.Managed, principal ecs.amazonaws.com, aws:ec2:managed-launch) subject to managed-resource visibility. docs/services.md updated (§24 rewritten, counts). Verified: go build/vet/gofmt clean, golangci-lint 0 new findings, go test ./... (210 pkgs, 0 failures), go test -race clean on ecs/ec2, and 66 real-SDK end-to-end checks across ECS core/services + the EC2 visibility matrix + the ECS<->EC2 compose path.
|
Thanks @NitinKumar004 — exceptionally thorough review. I took the whole thing, including the parity gap, and reworked ECS in four waves (pushed in 🔴 HIGH — all fixed
🟠 MEDIUM (ECS) — all fixed
🟠 MEDIUM (EC2 #300 cross-subsystem) — all fixed
📋 Parity gap — built out (the headline ask)Rather than defer, I implemented the scheduling/runtime surface across waves 2–4:
Remaining scope intentionally not modeled (documented in
|
thzgajendra
left a comment
There was a problem hiding this comment.
Deep re-review — ECS SDK-compat (#159) + EC2 managed-visibility (#300)
Full 8-lens pass (concurrency/COW, lifecycle & referential integrity, placement/capacity & Fargate, service deployment model, EC2 visibility + cross-subsystem, architecture/layering, tests, docs), verifying against source and the vendored SDK. The rework in 36f9f1d is real and high-quality — every HIGH/MEDIUM from @NitinKumar004's review is genuinely fixed, and this is real logic, not accept-and-echo. Verified:
- Copy-on-write + deep-copy-on-read is correct and regression-guarded.
clone.godeep-copies every nested slice/map; all mutators clone→mutate→Setunder lock; all reads return clones. I empirically reverted clone-on-read in a scratch copy andgo test -race ./providers/aws/ecs/...firedDATA RACE; restored it → clean. The concurrency tests (concurrency_race_test.go, 8 goroutines × 50 iters, mutators vs Describe/List) are genuine, not vacuous — they'd fail if the fix were reverted. - All 9 lifecycle/referential fixes hold: DeleteCluster cascade guard (3 real
ClusterContains*exceptions), DeleteService force+desiredCount, cluster/task-def validation (no more ghost-cluster runs), RunTask-missing-taskdef→ClientException, INACTIVE name reuse, live cluster counters, numeric revision ordering, context-specific typed errors (errors.Asmatches),SetIfAbsentTOCTOU. - Placement/capacity accounting is correct and concurrency-safe (reserve on run, release on stop, no leak/double-release, first-fit with correct AGENT/RESOURCE failure reasons). CodeQL slice-allocation alert is genuinely resolved —
countis bounded to [1,10] before anymake(), which uses the constant. - EC2 #300 cross-subsystem leaks all closed: visibility matrix correct for list/filtered/explicit-ID (explicit hidden ID →
InvalidInstanceID.NotFound), CloudWatch metric leak fixed, resourcediscovery/topology/SSM passIncludeManagedResources=true(correct — internal control-plane must see the real fleet), RWMutex + invalid-value rejection.OperatorResponse.HiddenByDefaultIS a realaws-sdk-go-v2/service/ec2 v1.317.1field (verified in the vendored deserializer) — the earlier doubt was unfounded; the author is right. Variadic driver change is non-breaking (all implementers forward/accept opts). #159↔#300 genuinely composes:RegisterContainerInstanceprovisions a real managed EC2 instance, proven end-to-end over the SDK. - Architecture is textbook-conformant to the bedrock/rds reference (4 layers,
do()/doBatch[T], idgen ARNs, disjointX-Amz-Target).go test ./...and-racepass.
New findings from this pass (none block the core value)
1. [Medium] Fargate cpu/memory combo validation is missing — contradicts the stated claim. validateLaunch (providers/aws/ecs/tasks.go) only checks td.CPU/Memory != ""; there is no valid-pairing check, so cpu=256, memory=1000000 (or cpu=999) is accepted and springs to RUNNING. Real Fargate rejects with "No Fargate configuration exists for given values." The PR/docs claim a "valid awsvpc cpu/memory combo" — either add the combo table or soften the claim so a real-SDK caller isn't misled into thinking the emulator catches bad pairings.
2. [Medium] docs/services.md §24 under-discloses accept-and-echo fields. The code is honest (these are pure store-and-echo), but §24 doesn't say so for capacityProviderStrategy (silently falls through to EC2 placement), loadBalancers/serviceRegistries (no target registration/health checks), deployment circuit-breaker/rollback, and Service Connect. The repo already has the convention for this — see the RDS "accepted but not [simulated]" note at services.md:1206. Also the "full container/task runtime surface" wording (§24) risks over-reading: task-def fields (secrets/healthCheck/logConfiguration/volumes/…) are round-tripped on the definition but not reflected onto launched containers (containersFor copies only name/image/status). Suggest "accepted and round-tripped on the task definition (not executed)."
3. [Low–med] Deployment ACTIVE entries are never pruned. redeployService demotes prior deployments to ACTIVE (counts zeroed) and prepends the new PRIMARY, but never removes drained ones — so deployments[] grows unbounded across repeated UpdateService calls. Real ECS drops a deployment once drained; a caller polling for eventual single-PRIMARY steady state will see accumulating stale entries.
4. [Low–med] DAEMON silently overwrites a caller-supplied desiredCount (desiredForStrategy sets it to the placeable-instance count). Real ECS rejects CreateService with desiredCount set under DAEMON (InvalidParameterException). Accepting-and-overwriting could mislead.
5. [Low] Lost-update on concurrent same-key RMW. UpdateService/mutateCluster/TagResource do Get→(unlock)→clone→mutate→Set — two concurrent writers to the same key silently drop one update (last-writer-wins). Not a -race data race (each mutates its own clone), but a real correctness race. The store already exposes an atomic Update(key, fn) that would close it; it's unused. Fine for a mock, worth a note.
6. [Low] Miscellaneous fidelity/robustness: DeregisterContainerInstance force=true leaves its tasks RUNNING on a deleted instance (real ECS stops them); UpdateContainerInstancesState(DRAINING) blocks new placement but doesn't drain/reschedule existing tasks; launchManagedInstance silently falls back to a synthesized i-… string if the launcher errors (a future misconfig would regress compose to the opaque-string behavior invisibly — surface the error instead); serviceFromInput/applyServiceRefs store the caller's LoadBalancers/ServiceRegistries slices without copying (input-aliasing on write); no test covers RunTask count<=0 (defaults to 1) even though the >10 cap is tested.
Bottom line: correct, well-tested, idiomatic, and honestly built out far beyond the original scaffold. I'd resolve #1 (claim-vs-code) and #2 (doc disclosure) before merge; #3–#6 are good follow-up candidates. Deferring the merge decision to you.
…iasing - RunTask/CreateService now validate the Fargate task cpu/memory pair against the supported configuration table (was presence-only), rejecting unsupported pairings with the AWS "No Fargate configuration exists" error. - UpdateService no longer accumulates deployments: the superseded (synchronously drained) deployment is dropped, leaving just the new PRIMARY. - CreateService rejects a caller-supplied desiredCount on a DAEMON service (AWS InvalidParameterException) instead of silently overwriting it. - Service create/update clone their LoadBalancers/ServiceRegistries/ capacity-provider/config reference fields, so the stored record never aliases the caller's input. - RegisterContainerInstance surfaces a launcher failure (ServerException) rather than silently synthesizing an id; force-deregistering an instance stops the tasks placed on it. - docs/services.md §24 discloses the accept-and-echo fields (capacityProviderStrategy, loadBalancers/serviceRegistries, deployment circuit-breaker) and clarifies that the task-def runtime surface is round-tripped, not reflected onto containers. Covered by tests for each fix.
|
Thanks for the thorough 8-lens re-review. Addressed in #1 — Fargate cpu/memory combo (fixed). #2 — docs §24 disclosure (fixed). §24 now lists the accepted but not simulated fields — #3 — deployment accumulation (fixed). #4 — DAEMON desiredCount (fixed). #6 — misc:
Addressed as documented limitations (per your "fine for a mock" note):
Build, |
Completes the review findings not covered by 37cf9f5: - Finding 5 (atomic read-modify-write): TagResource/UntagResource and mutateCluster did Get -> clone -> Set, so two concurrent tag or cluster mutations on the same resource could lose one another's changes (last-writer-wins). Route them through memstore.Update (with SetIfAbsent seeding the tag entry) so the whole RMW runs under the store lock. - Wire error messages leaked the internal "<Code>: " prefix (e.g. "InvalidArgument: No Fargate configuration...") because the server used err.Error(). Add wireMessage() to unwrap *cerrors.Error and surface the bare Message, matching how real AWS SDK exceptions read.
|
Thanks @thzgajendra — that's an unusually thorough pass, and the empirical clone-on-read revert to confirm the race guard is exactly the kind of verification I'd hoped for. All six findings are now resolved across two commits ( 1 — Fargate cpu/memory combo validation 2 — 3 — Deployment 4 — DAEMON silently overwrites caller 5 — Lost-update on concurrent same-key RMW One deliberate exception: 6 — Miscellaneous fidelity/robustness
Left as a documented follow-up (not this PR): Bonus (
|
NitinKumar004
left a comment
There was a problem hiding this comment.
Deep review — AWS ECS SDK-compat (#159) + EC2 managed-resource visibility (#300)
Reviewed end-to-end in an isolated worktree (gate matrix, static/coverage layer, architecture-pillar fit, and a multi-lens corner-case pass with adversarial verification), then re-verified against the latest fix commits.
Gates (verified): build ✓ vet ✓ test ✓ -race ✓ gofmt ✓ go mod tidy ✓ — CI-blocking gates pass; the ec2 bump is necessary (adds IncludeManagedResources), deps are tidy with no version skew, and golangci-lint is clean on changed files.
Architecture: strong, and the recent fixes are clean. The DescribeInstances variadic ripple is fully sound (chaos/ssm/topology/discovery forward opts, gce/vm accept-and-ignore, rds-driver is a separate interface). EC2 visibility matrix, Operator XML, and system-tag round-trip are correct. ECS wire shape is correct (37 ops routed + exercised, typed exceptions + failures[] modeled, include gating implemented, epoch timestamps), read-path aliasing is clone-on-read, revision auto-increment is race-safe, the DeleteCluster cascade guard is correct, factory/Matches don't collide with ECR, and docs op-counts are exact 3-way. The latest fix commits (deployment accumulation → single PRIMARY, DAEMON+desiredCount rejected, Fargate cpu/memory validation, atomic tag/cluster RMW, force-deregister without deadlock, wire error typing) were verified and introduce no new bugs.
The items below are correctness/fidelity polish.
Medium
- INACTIVE (deregistered) task def still launches tasks/services —
resolveTaskDefreturns an explicitfamily:revision/ARN with no ACTIVE check.Deregister("web:3")thenRunTask(taskDefinition="web:3")launches RUNNING; real ECS rejects it. (inline) - INACTIVE (deleted) cluster still accepts RunTask/CreateService —
clusterExistsis presence-only;DeleteClustermarks INACTIVE but leaves the tombstone.DeleteCluster("prod")thenRunTask(cluster="prod")stores a RUNNING task on a dead cluster (real ECS:ClusterNotFoundException); the tombstone also blocks recreating the name. (inline) UpdateContainerInstancesStatecapacity lost-update race — it does copy-on-write withoutplaceMu, whilereserve/release/deregisterall take it. A concurrentRunTask(capacity reserve) +UpdateContainerInstancesState(DRAINING)silently reverts the reservation; invisible to-raceand untested. (inline)SetManagedis a test-only helper in production code — zero non-test callers (the production managed-launch path usesLaunchManaged). Violates the project's "no test-only helpers in production" rule. (inline)ListServicesreturns INACTIVE (deleted) service tombstones — no status filter; real ECS lists only ACTIVE/DRAINING. Create → delete → list still shows the deleted service. (inline)- The two headline "must-verify" behaviors are unasserted —
server/aws/ecs/sdk_roundtrip_test.gohas 0ClientExceptionrefs and noinclude=[TAGS]gating assertion, so the typed-exception-vs-failures[]distinction and theincludegating are never checked;PutAccountSettingDefaultis untested. Coverage is low onservices/ecs(55.6%) and the server handlers. If either headline behavior regresses, the suite stays green.
Low
UpdateClusterstoresConfiguration json.RawMessagewithout cloning — write-side aliasing (read paths are safe). (inline)- Nested
ContainerDefinitionslices (Environment/PortMappings/Secrets) still alias on register (the top-level slice is now cloned). SetManagedmutatesinst.Operatorwithoutm.mu;UpdateServicedoesn't reject DAEMON+desiredCount (CreateService does).- Intentional parity gaps (ECS→CloudWatch metrics, ECS cost rate) — consistent with the EKS precedent; worth a tracked follow-up rather than a blocker.
Verdict: comment. Gate-green, architecturally clean, deps tidy, docs accurate, and the recent fixes are solid; the remaining items are fidelity/correctness polish. I'd prioritize the INACTIVE-resource guards and the SetManaged rule violation.
| } | ||
|
|
||
| // resolveTaskDef looks up a task definition by family, family:revision, or ARN. | ||
| func (m *Mock) resolveTaskDef(id string) (*driver.TaskDefinition, bool) { |
There was a problem hiding this comment.
[Medium] INACTIVE task def still launches. resolveTaskDef returns an explicit family:revision/ARN via a bare Get with no ACTIVE check, so RunTask/CreateService/UpdateService accept a deregistered definition. → Deregister("web:3") then RunTask(taskDefinition="web:3") launches RUNNING; real ECS rejects running new tasks from an INACTIVE task def. (Bare-family lookups are fine via latestActive.)
There was a problem hiding this comment.
Fixed in f27e99a. Added resolveLaunchableTaskDef (resolves and requires ACTIVE); RunTask/CreateService/UpdateService route launches through it, so a deregistered family:revision is now rejected with a ClientException (InvalidParameter — it exists, just isn't runnable). DescribeTaskDefinition/DeregisterTaskDefinition/tag resolution still use resolveTaskDef and keep seeing INACTIVE defs. Tests: TestRunTaskRejectsInactiveTaskDef + Create/Update variants.
|
|
||
| // clusterExists reports whether a cluster with the given bare name is present. | ||
| // The implicit "default" cluster is always treated as present, matching AWS. | ||
| func (m *Mock) clusterExists(name string) bool { |
There was a problem hiding this comment.
[Medium] INACTIVE (deleted) cluster still accepts work. clusterExists is presence-only; DeleteCluster marks the cluster INACTIVE but leaves the tombstone in the store, and RunTask/CreateService gate only on clusterExists. → DeleteCluster("prod") then RunTask(cluster="prod") stores a RUNNING task on a dead cluster (real ECS: ClusterNotFoundException); the tombstone also makes CreateCluster("prod") fail AlreadyExists, so the name can never be recreated. Add a status check (reject non-ACTIVE clusters).
There was a problem hiding this comment.
Fixed in f27e99a. Added clusterActive (present and ACTIVE); RunTask/CreateService gate on it, so work against a deleted cluster now returns ClusterNotFoundException instead of landing on the tombstone. And CreateCluster reuses a deleted name — only an ACTIVE same-name cluster is an AlreadyExists conflict; an INACTIVE tombstone is overwritten — under a new clusterMu compare-and-set. Tests: TestRunTaskRejectsDeletedCluster, TestCreateClusterReusesDeletedName.
| // UpdateContainerInstancesState sets each resolved instance to ACTIVE or | ||
| // DRAINING; unresolved ids become failures. Only ACTIVE and DRAINING are valid | ||
| // target states. | ||
| func (m *Mock) UpdateContainerInstancesState( |
There was a problem hiding this comment.
[Medium] Capacity lost-update race. UpdateContainerInstancesState mutates a clone and Sets it back without placeMu, while reserve (capacity.go), release/StopTask, and DeregisterContainerInstance all take placeMu for their instance RMW. → A concurrent RunTask (reserve: decrement RemainingCPU, RunningTasksCount++) + UpdateContainerInstancesState(DRAINING) (reads the pre-decrement copy) → whichever Set lands last wins, silently reverting the reservation. memstore.Set is per-key locked so -race won't flag it; it's a logical lost update and concurrency_race_test.go doesn't exercise this combo. Take placeMu here too.
There was a problem hiding this comment.
Fixed in f27e99a. UpdateContainerInstancesState now holds placeMu for the whole resolve→clone→Set window, serializing it with reserve/release/DeregisterContainerInstance so a concurrent RunTask reservation can no longer be silently reverted. Resolving under the lock also means the clone reflects the freshest capacity counts.
|
|
||
| // SetManaged marks an existing instance as a service-provider-managed resource. | ||
| // This is a test helper for exercising managed-resource visibility. | ||
| func (m *Mock) SetManaged(instanceID, principal string) error { |
There was a problem hiding this comment.
[Medium] Test-only helper in production code. SetManaged has zero non-test callers — the production managed-launch path is LaunchManaged (consumed by providers/aws/ecs/containerinstances.go). This violates the project's "no test-only helpers in production" rule (dead exported API a maintainer may mistake for a real flow). Move the marking into the test package, or fold it into RunInstances(Managed:true).
There was a problem hiding this comment.
Fixed in f27e99a. Removed SetManaged — zero non-test callers; the production managed-launch path is LaunchManaged → RunInstances(Managed:true, Principal:…). Its dedicated TestSetManaged went with it; TestManagedResourceVisibility already covers hidden/revealed/opt-in/explicit-id via the real path. This also removes the m.mu-less inst.Operator mutation you flagged under Low.
| } | ||
|
|
||
| // ListServices returns services in a cluster in deterministic order. | ||
| func (m *Mock) ListServices(_ context.Context, cluster string) ([]driver.Service, error) { |
There was a problem hiding this comment.
[Medium] ListServices returns INACTIVE tombstones. No status filter, so a deleted service (markServiceDeleted sets Status=INACTIVE but keeps the record) still appears. → Create → DeleteService → ListServices still lists the deleted service; real ECS ListServices returns only ACTIVE/DRAINING. Filter out INACTIVE.
There was a problem hiding this comment.
Fixed in f27e99a. ListServices now skips INACTIVE tombstones (returns only live ACTIVE/DRAINING services); DescribeServices still resolves a deleted service by name. Test: TestListServicesExcludesInactive (asserts a deleted svc is absent from List but present in Describe).
| } | ||
|
|
||
| if in.Configuration != nil { | ||
| c.Configuration = in.Configuration |
There was a problem hiding this comment.
[Low] Write-side aliasing on UpdateCluster. c.Configuration = in.Configuration stores the caller's json.RawMessage without cloning, so a caller mutating the byte slice it passed corrupts the store (read paths are safe via cloneCluster/cloneRaw). Clone it like the other reference-typed fields.
There was a problem hiding this comment.
Fixed in f27e99a. c.Configuration = cloneRaw(in.Configuration) so the store no longer aliases the caller's byte slice. Test: TestUpdateClusterDoesNotAliasConfiguration (mutates the input after the call, asserts the store is unchanged).
…ly helper Addresses the re-review findings: - INACTIVE task definition still launched: resolveTaskDef accepts a deregistered family:revision, so RunTask/CreateService/UpdateService would run new tasks from it. Add resolveLaunchableTaskDef (requires ACTIVE) and route the three launch paths through it; describe/deregister/tag paths keep seeing INACTIVE defs. - INACTIVE (deleted) cluster still accepted work: clusterExists is presence-only, so RunTask/CreateService landed on a deleted-cluster tombstone. Add clusterActive (present AND ACTIVE) for the launch gates, and let CreateCluster reuse a deleted name (only an ACTIVE same-name cluster is a conflict) under a new clusterMu compare-and-set. - UpdateContainerInstancesState capacity lost-update: it mutated a clone and Set it back without placeMu, racing reserve/release/deregister. Hold placeMu for the whole read-modify-write. - SetManaged was a test-only helper in production EC2 code (zero non-test callers; the real path is LaunchManaged). Removed it and its dedicated test; managed-visibility coverage stays via TestManagedResourceVisibility. - ListServices returned INACTIVE service tombstones; filter to live (non-INACTIVE) services like real ECS. - UpdateService now rejects desiredCount under DAEMON (parity with CreateService). - Write-side input aliasing: RegisterTaskDefinition deep-clones the task def (nested ContainerDefinition slices + task-level reference fields) before storing; UpdateCluster clones the caller's Configuration raw JSON. Tests: new provider-level guards in nitin_review_test.go, plus server roundtrip assertions for the ClientException typed-error path, DescribeClusters include=[TAGS] gating, and PutAccountSettingDefault.
|
Thanks @NitinKumar004 — the isolated-worktree pass and the adversarial corner-case verification are much appreciated. Every Medium and every actionable Low is now fixed in MediumHeadline behaviors were unasserted (test coverage). Added the missing server-roundtrip assertions in
Plus provider-level guards in Low
Deferred (tracked follow-up, not this PR)
Gates re-verified locally: |
NitinKumar004
left a comment
There was a problem hiding this comment.
Re-review — all findings addressed ✅
Verified the fix commit (f27e99a) against my earlier review. Every finding is resolved, correctly and without over-correcting, with tests added and gates green (build ✓ vet ✓ test ✓ -race ✓ gofmt ✓ go mod tidy ✓; lint clean on the changed files).
- INACTIVE task def launches →
resolveActiveTaskDefrejects a deregistered def on RunTask/CreateService as a typedClientException;DescribeTaskDefinitionstill resolves INACTIVE (no over-correction). - INACTIVE cluster accepts work → new
clusterActiveguard on the launch paths only;ListTasks/StopTaskstill work on a deleting cluster. UpdateContainerInstancesStatecapacity race → now holdsplaceMufor the whole read-modify-write.SetManagedtest-only helper → removed.- Unasserted
ClientException/include-gating → now asserted in the SDK round-trip tests. ListServicesINACTIVE tombstones → filtered out (DescribeServicesstill returns them).UpdateClusterconfig aliasing →cloneRaw.
No regressions; the INACTIVE guards are scoped to launch paths so read/describe/list/stop on INACTIVE resources still behave correctly. LGTM.
Residual (non-blocking): the portable services/ecs and server/aws/ec2 packages remain under the 90% coverage pillar, and the intentional ECS→CloudWatch metrics / cost-rate gaps are still there (consistent with EKS — worth a tracked follow-up).
Summary
Adds two related AWS features (ECS Managed Instances are a primary producer of managed EC2 instances, so #300 composes naturally with #159):
#159— AWS ECS SDK-compat handler: a complete new ECS service so realaws-sdk-go-v2/service/ecsclients work against cloudemu.#300— EC2 managed-resource visibility:Operator.Managed+IncludeManagedResourcesonDescribeInstances, with a hidden-by-default account setting.Both are implemented end-to-end across the established layers and verified with real-SDK round-trip + live-socket e2e.
Closes #159
Closes #300
#159— ECS SDK-compat serviceNew service (AWS JSON 1.1,
X-Amz-Target: AmazonEC2ContainerServiceV20141113.*) across all four layers —services/ecs/driver(interface) →providers/aws/ecs(in-memoryMock) →services/ecs(portable API with the cross-cuttingdo()wrapper) →server/aws/ecs(restJson/AwsJson handler) — and registered inserver/aws/aws.go+providers/aws/aws.go.All 19 operations from the ticket:
Behavior (AWS-faithful):
RunTaskreturnsRUNNINGtasks,CreateServicereachesdesiredCount, task-definition revisions auto-increment per family.DescribeTaskDefinitionresolvesfamily(→ latest ACTIVE),family:revision, or full ARN; a missing definition errors (ClientException), not afailures[]entry.Describe*ops andRunTaskreturn partial success — unresolved ids land infailures[]{arn, reason: "MISSING"}rather than failing the whole call.DescribeClustershonors theincludeparameter —tags/settingsare only returned whenTAGS/SETTINGSis requested (matches real ECS).DeregisterTaskDefinition/DeleteService/DeleteClustersoft-transition toINACTIVE; duplicateCreateServicein a cluster is rejected."default"when omitted. ARNs viaidgen.#300— EC2 managed-resource visibilityInstance model: added an
Operatorblock (Managed,Principal) to the computeInstance; instances can be seeded managed at launch (InstanceConfig.Managed/Principal) or viaSetManaged(id, principal).aws:-prefixed system tags (e.g.aws:ec2:managed-launch) round-trip unchanged.Account setting:
cloud.EC2.SetManagedResourceVisibility("visible"|"hidden").DescribeInstanceshonorsIncludeManagedResources:IncludeManagedResourcesThe rule applies uniformly to list, filtered, and explicit-
InstanceIdsrequests; non-managed instances are always returned.Works on both surfaces — the typed Go API and the query-protocol SDK server. The response
Operatorblock (<operator><managed>/<principal>/<hiddenByDefault></operator>) matches theec2SDK deserializer, so a realaws-sdk-go-v2client seesInstance.Operator.Managed/Principal.The compute driver's
DescribeInstancesgained a trailing variadic options param (existing 3-arg callers compile unchanged); all implementers (GCP/Azure/chaos/ssm) were updated.Dependencies
github.com/aws/aws-sdk-go-v2/service/ecs v1.89.1.github.com/aws/aws-sdk-go-v2/service/ec2tov1.317.1(adds theDescribeInstancesIncludeManagedResourcesrequest parameter).Testing
go build ./...,go vet,gofmt— clean.golangci-lint— zero new findings (the 7 pre-existing findings live in untouchedserver/aws/ec2files).go test ./...— 210 packages, 0 failures.go test -raceon the ecs/ec2 packages — clean.awsserver.New(awsserver.DriversFrom(cloud))over a live socket:failuresacross clusters/tasks/services/instances, container-instance round-trip,include=TAGSgating).Operator/system-tag round-trip.docs/services.mdupdated: new Container Orchestration service (§24), an EC2 managed-resource visibility subsection with Go-API + SDK examples, and the service/op-count tables.