Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
56 changes: 55 additions & 1 deletion docs/coverage/aws/ecs.md
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,60 @@ AWS's `ecs` service · portable interface `driver.ECS` · [AWS index](./README.m
| `UpdateContainerInstancesState` | |
| `UpdateService` | |

## Optional capabilities

Discovered by type assertion; only some providers implement these.

### ServiceDeployments

ServiceDeployments exposes the deployment history of ECS-controller services.

| Operation | Description |
| --- | --- |
| `DescribeServiceDeployments` | |
| `DescribeServiceRevisions` | |
| `ListServiceDeployments` | |
| `StopServiceDeployment` | |

### ServiceNamespaces

ServiceNamespaces lists the services that joined a Service Connect namespace.

| Operation | Description |
| --- | --- |
| `ListServicesByNamespace` | |

### TaskProtection

TaskProtection reads and changes the scale-in protection of service tasks.

| Operation | Description |
| --- | --- |
| `GetTaskProtection` | |
| `UpdateTaskProtection` | |

### TaskSets

TaskSets manages task sets, the task groups of EXTERNAL-controller services.

| Operation | Description |
| --- | --- |
| `CreateTaskSet` | |
| `DeleteTaskSet` | |
| `DescribeTaskSets` | |
| `UpdateServicePrimaryTaskSet` | |
| `UpdateTaskSet` | |

## Not in scope

_Not documented yet. See the [emulator boundary](../../../README.md) for cloudemu-wide non-goals._
- No workload runs unless a container engine is attached, so the usage-driven metrics (`CPUUtilization`, `MemoryUtilization`, network and storage series) are never published. Only metrics derived from state the emulator holds are: `AWS/ECS` `CPUReservation`, `MemoryReservation` and `LiveTaskCount`, and the `ECS/ContainerInsights` task, service, instance, deployment and task-set counts (only for clusters with `containerInsights` enabled or enhanced, or enabled on the account).
- Task lifecycle events cover PROVISIONING (awsvpc tasks), PENDING, ACTIVATING, RUNNING, DEACTIVATING, STOPPING, DEPROVISIONING (awsvpc tasks) and STOPPED. The transient `DELETED` state is not published. A task counts as awsvpc when it has an ENI attachment, which today only Fargate tasks do.
- Container-instance state-change events omit `versionInfo` (agent and Docker versions), since the emulator runs no agent.
- `ECS Deployment State Change` events are published for the in-progress and completed states. The deployment circuit breaker is stored and echoed but never trips, so `SERVICE_DEPLOYMENT_FAILED` and automatic rollback do not happen. `SERVICE_TASK_PLACEMENT_FAILURE` is published when a service create or update leaves tasks unplaced, not when a replacement task cannot be placed.
- `SERVICE_DESIRED_COUNT_UPDATED` is not published: it signals scheduler or Service Auto Scaling changes, which the emulator does not model.
- Service deployments and revisions exist for services that use the ECS deployment controller; they complete immediately, so `StopServiceDeployment` only succeeds while the deployment is still in progress, which is the `--async-settle` window. A stop of a completed deployment is a `ConflictException`. The lifecycle stage is not reported, and blue/green, linear and canary strategies, lifecycle hooks, alarms and `ContinueServiceDeployment` are not modelled. The newest 100 deployments of a service are kept.
- Task sets run their tasks directly and replace a stopped task to restore `computedDesiredCount`. They are not registered with load balancer target groups or service discovery, and a capacity provider strategy on a task set is stored and echoed but not used for placement (the same as for services).
- `ListServicesByNamespace` matches a service by the namespace string (name or ARN) it was configured with. There is no Cloud Map in the emulator to resolve one to the other, so an unknown namespace lists nothing instead of returning `NamespaceNotFoundException`.
- Where AWS does not document an exact message or exception, the emulator picks the closest documented exception and its own wording: a task-set call on a service that does not use the EXTERNAL controller, an invalid `launchType`, `ExecuteCommand` on a task that is not RUNNING, and `StopTask` on an already-stopped task (it keeps the original reason).
- `DeleteTaskSet` drains the set immediately whether or not `force` is set.
- `CreateCluster` on an existing ACTIVE name returns that cluster unchanged and ignores the tags and settings in the new request.
62 changes: 62 additions & 0 deletions docs/coverage/coverage.json
Original file line number Diff line number Diff line change
Expand Up @@ -6713,6 +6713,68 @@
"name": "UpdateService"
}
],
"optionalCapabilities": [
{
"name": "ServiceDeployments",
"doc": "ServiceDeployments exposes the deployment history of ECS-controller services.",
"operations": [
{
"name": "DescribeServiceDeployments"
},
{
"name": "DescribeServiceRevisions"
},
{
"name": "ListServiceDeployments"
},
{
"name": "StopServiceDeployment"
}
]
},
{
"name": "ServiceNamespaces",
"doc": "ServiceNamespaces lists the services that joined a Service Connect namespace.",
"operations": [
{
"name": "ListServicesByNamespace"
}
]
},
{
"name": "TaskProtection",
"doc": "TaskProtection reads and changes the scale-in protection of service tasks.",
"operations": [
{
"name": "GetTaskProtection"
},
{
"name": "UpdateTaskProtection"
}
]
},
{
"name": "TaskSets",
"doc": "TaskSets manages task sets, the task groups of EXTERNAL-controller services.",
"operations": [
{
"name": "CreateTaskSet"
},
{
"name": "DeleteTaskSet"
},
{
"name": "DescribeTaskSets"
},
{
"name": "UpdateServicePrimaryTaskSet"
},
{
"name": "UpdateTaskSet"
}
]
}
],
"providers": {
"aws": "ECS"
}
Expand Down
11 changes: 11 additions & 0 deletions docs/coverage/nongoals/ecs.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
- No workload runs unless a container engine is attached, so the usage-driven metrics (`CPUUtilization`, `MemoryUtilization`, network and storage series) are never published. Only metrics derived from state the emulator holds are: `AWS/ECS` `CPUReservation`, `MemoryReservation` and `LiveTaskCount`, and the `ECS/ContainerInsights` task, service, instance, deployment and task-set counts (only for clusters with `containerInsights` enabled or enhanced, or enabled on the account).
- Task lifecycle events cover PROVISIONING (awsvpc tasks), PENDING, ACTIVATING, RUNNING, DEACTIVATING, STOPPING, DEPROVISIONING (awsvpc tasks) and STOPPED. The transient `DELETED` state is not published. A task counts as awsvpc when it has an ENI attachment, which today only Fargate tasks do.
- Container-instance state-change events omit `versionInfo` (agent and Docker versions), since the emulator runs no agent.
- `ECS Deployment State Change` events are published for the in-progress and completed states. The deployment circuit breaker is stored and echoed but never trips, so `SERVICE_DEPLOYMENT_FAILED` and automatic rollback do not happen. `SERVICE_TASK_PLACEMENT_FAILURE` is published when a service create or update leaves tasks unplaced, not when a replacement task cannot be placed.
- `SERVICE_DESIRED_COUNT_UPDATED` is not published: it signals scheduler or Service Auto Scaling changes, which the emulator does not model.
- Service deployments and revisions exist for services that use the ECS deployment controller; they complete immediately, so `StopServiceDeployment` only succeeds while the deployment is still in progress, which is the `--async-settle` window. A stop of a completed deployment is a `ConflictException`. The lifecycle stage is not reported, and blue/green, linear and canary strategies, lifecycle hooks, alarms and `ContinueServiceDeployment` are not modelled. The newest 100 deployments of a service are kept.
- Task sets run their tasks directly and replace a stopped task to restore `computedDesiredCount`. They are not registered with load balancer target groups or service discovery, and a capacity provider strategy on a task set is stored and echoed but not used for placement (the same as for services).
- `ListServicesByNamespace` matches a service by the namespace string (name or ARN) it was configured with. There is no Cloud Map in the emulator to resolve one to the other, so an unknown namespace lists nothing instead of returning `NamespaceNotFoundException`.
- Where AWS does not document an exact message or exception, the emulator picks the closest documented exception and its own wording: a task-set call on a service that does not use the EXTERNAL controller, an invalid `launchType`, `ExecuteCommand` on a task that is not RUNNING, and `StopTask` on an already-stopped task (it keeps the original reason).
- `DeleteTaskSet` drains the set immediately whether or not `force` is set.
- `CreateCluster` on an existing ACTIVE name returns that cluster unchanged and ignores the tags and settings in the new request.
67 changes: 64 additions & 3 deletions docs/services.md
Original file line number Diff line number Diff line change
Expand Up @@ -2370,8 +2370,69 @@ real EC2 instance subject to managed-resource visibility.
| Container instances | RegisterContainerInstance, DeregisterContainerInstance, UpdateContainerInstancesState (DRAINING), ListContainerInstances, DescribeContainerInstances |
| Tagging | TagResource, UntagResource, ListTagsForResource |
| Account & attributes | PutAccountSetting(+Default), ListAccountSettings, DeleteAccountSetting, PutAttributes, DeleteAttributes, ListAttributes |
| Capacity providers | CreateCapacityProvider, DescribeCapacityProviders, UpdateCapacityProvider, DeleteCapacityProvider |

**Total: 37 operations.**
**Behaviour added for real-cloud parity.** `ExecuteCommand` needs a task started with
`enableExecuteCommand` that is RUNNING (the task reports the `ExecuteCommandAgent`);
`CreateCluster` on an ACTIVE name returns that cluster; `assignPublicIp` defaults to
`DISABLED`; an invalid `launchType` is an `InvalidParameterException`; a repeated
`StopTask` keeps the first stop reason; awsvpc containers report `networkInterfaces`.
Tasks publish one EventBridge event per lifecycle state (`PROVISIONING` for awsvpc tasks,
`PENDING`, `ACTIVATING`, `RUNNING`, `DEACTIVATING`, `STOPPING`, `DEPROVISIONING` for
awsvpc tasks, `STOPPED`) with `detail.version` +1 each, plus ECS Deployment State Change,
Container Instance State Change and more Service Action events. With a CloudWatch backend
wired, ECS publishes `ECS/ContainerInsights` counts (clusters with `containerInsights`
enabled or enhanced) and `AWS/ECS` `CPUReservation`, `MemoryReservation` and `LiveTaskCount`;
no usage-driven series. What is deliberately not emulated is listed in
[coverage/nongoals/ecs.md](coverage/nongoals/ecs.md).

### Task Sets (optional capability: `TaskSets`)

Task sets of services that use the `EXTERNAL` deployment controller. Each set runs its own
tasks (`computedDesiredCount` = the service desired count x scale, rounded up), starts
`ACTIVE`, and `UpdateServicePrimaryTaskSet` makes one `PRIMARY` (the previous primary becomes
`ACTIVE`). `DeleteTaskSet` drains the set immediately; `force` is accepted but has no visible effect. `DescribeServices`
reports a service's task sets.

| Operation | Signature |
|-----------|-----------|
| `CreateTaskSet` | `(ctx, CreateTaskSetInput) (*TaskSet, error)` |
| `UpdateTaskSet` | `(ctx, UpdateTaskSetInput) (*TaskSet, error)` |
| `DeleteTaskSet` | `(ctx, DeleteTaskSetInput) (*TaskSet, error)` |
| `DescribeTaskSets` | `(ctx, cluster, service, ids) ([]TaskSet, []Failure, error)` |
| `UpdateServicePrimaryTaskSet` | `(ctx, cluster, service, primary) (*TaskSet, error)` |

### Task Protection (optional capability: `TaskProtection`)

Scale-in protection (1-2880 minutes, default 120) for service tasks; a scale-in or redeployment
leaves protected tasks running.

| Operation | Signature |
|-----------|-----------|
| `GetTaskProtection` | `(ctx, cluster, tasks) ([]ProtectedTask, []Failure, error)` |
| `UpdateTaskProtection` | `(ctx, UpdateTaskProtectionInput) ([]ProtectedTask, []Failure, error)` |

### Service Deployments (optional capability: `ServiceDeployments`)

The deployment history of ECS-controller services: every create, force-new-deployment or
deploying update records a service deployment and a service revision (the newest 100 are
kept). `StopServiceDeployment` works while a deployment is still in progress (the
`--async-settle` window); a completed deployment is a `ConflictException`.

| Operation | Signature |
|-----------|-----------|
| `ListServiceDeployments` | `(ctx, ListServiceDeploymentsInput) ([]ServiceDeployment, nextToken, error)` |
| `DescribeServiceDeployments` | `(ctx, arns) ([]ServiceDeployment, []Failure, error)` |
| `DescribeServiceRevisions` | `(ctx, arns) ([]ServiceRevision, []Failure, error)` |
| `StopServiceDeployment` | `(ctx, arn, stopType) (arn, error)` |

### Service Connect Namespaces (optional capability: `ServiceNamespaces`)

| Operation | Signature |
|-----------|-----------|
| `ListServicesByNamespace` | `(ctx, namespace, maxResults, nextToken) ([]arn, nextToken, error)` |

**Total: 41 operations (+12 optional)**

---

Expand Down Expand Up @@ -3250,7 +3311,7 @@ delete) is still enforced.
| Machine Learning: Azure AI (CognitiveServices + MachineLearningServices + data plane) | 92 |
| Machine Learning: GCP Vertex AI (Go API/driver) | 128 |
| AI Search: Azure AI Search (control + data plane) | 53 |
| Container Orchestration: AWS ECS | 37 |
| Container Orchestration: AWS ECS | 41 (+12 optional) |
| DNS Resolver: AWS Route 53 Resolver | 72 |
| Application Networking: AWS VPC Lattice | 73 |
| Key Management: AWS KMS | 45 |
Expand All @@ -3266,7 +3327,7 @@ delete) is still enforced.
| Data Integration: AWS Glue | 299 |
| Threat Detection: Amazon GuardDuty | 87 |
| Streaming: Amazon MSK | 59 |
| **Grand Total** | **2749** (+138 optional) |
| **Grand Total** | **2753** (+150 optional) |

Optional operations are capabilities a driver may implement but is not required
to; see the sections marked "optional capability". They are counted separately
Expand Down
2 changes: 2 additions & 0 deletions internal/settle/settle.go
Original file line number Diff line number Diff line change
Expand Up @@ -51,6 +51,8 @@ const (
DefaultECSTaskStartSettle = 2 * time.Second // ECS task PROVISIONING/PENDING->RUNNING
DefaultECSTaskStopSettle = 1 * time.Second // ECS task STOPPING/DEPROVISIONING->STOPPED

DefaultECSDeploymentSettle = 3 * time.Second // ECS service deployment IN_PROGRESS->SUCCESSFUL

DefaultTargetHealthSettle = 2 * time.Second // ELBv2 target initial->healthy
DefaultTargetDrainSettle = 2 * time.Second // ELBv2 target draining->removed

Expand Down
1 change: 1 addition & 0 deletions providers/aws/aws.go
Original file line number Diff line number Diff line change
Expand Up @@ -417,6 +417,7 @@ func newProvider(o *config.Options, shared *GlobalServices) *Provider {
p.CloudWatchLogs.SetMonitoring(p.CloudWatch)
p.SNS.SetMonitoring(p.CloudWatch)
p.ECR.SetMonitoring(p.CloudWatch)
p.ECS.SetMonitoring(p.CloudWatch)
p.EventBridge.SetMonitoring(p.CloudWatch)
p.RDS.SetMonitoring(p.CloudWatch)
p.Kinesis.SetMonitoring(p.CloudWatch)
Expand Down
52 changes: 51 additions & 1 deletion providers/aws/ecs/clone.go
Original file line number Diff line number Diff line change
Expand Up @@ -200,7 +200,7 @@ func cloneTask(t *driver.Task) driver.Task {
}

// cloneContainers deep-copies a slice of containers and each container's
// NetworkBindings slice.
// NetworkBindings, NetworkInterfaces and ManagedAgents slices.
func cloneContainers(in []driver.Container) []driver.Container {
if len(in) == 0 {
return nil
Expand All @@ -211,6 +211,8 @@ func cloneContainers(in []driver.Container) []driver.Container {
for i := range in {
c := in[i]
c.NetworkBindings = append([]driver.NetworkBinding(nil), in[i].NetworkBindings...)
c.NetworkInterfaces = append([]driver.ContainerNetworkInterface(nil), in[i].NetworkInterfaces...)
c.ManagedAgents = append([]driver.ManagedAgent(nil), in[i].ManagedAgents...)
out[i] = c
}

Expand Down Expand Up @@ -250,6 +252,7 @@ func cloneService(s *driver.Service) driver.Service {
out.DeploymentConfiguration = cloneDeploymentConfig(s.DeploymentConfiguration)
out.NetworkConfiguration = cloneNetworkConfig(s.NetworkConfiguration)
out.HealthCheckGracePeriodSeconds = cloneIntPtr(s.HealthCheckGracePeriodSeconds)
out.ServiceConnect = cloneServiceConnect(s.ServiceConnect)

return out
}
Expand Down Expand Up @@ -331,3 +334,50 @@ func cloneManagedScaling(in *driver.ManagedScaling) *driver.ManagedScaling {

return &out
}

// cloneTaskSet deep-copies a task set's slice and pointer fields.
func cloneTaskSet(ts *driver.TaskSet) driver.TaskSet {
out := *ts
out.CapacityProviderStrategy = append([]driver.CapacityProviderStrategyItem(nil), ts.CapacityProviderStrategy...)
out.NetworkConfiguration = cloneNetworkConfig(ts.NetworkConfiguration)
out.LoadBalancers = append([]driver.LoadBalancer(nil), ts.LoadBalancers...)
out.ServiceRegistries = append([]driver.ServiceRegistry(nil), ts.ServiceRegistries...)
out.Tags = copyTags(ts.Tags)

return out
}

// cloneServiceConnect deep-copies a Service Connect configuration.
func cloneServiceConnect(in *driver.ServiceConnectConfiguration) *driver.ServiceConnectConfiguration {
if in == nil {
return nil
}

return &driver.ServiceConnectConfiguration{Namespace: in.Namespace, Raw: append([]byte(nil), in.Raw...)}
}

// cloneServiceDeployment deep-copies a service deployment.
func cloneServiceDeployment(d *driver.ServiceDeployment) driver.ServiceDeployment {
out := *d
out.SourceServiceRevisions = append([]driver.ServiceRevisionSummary(nil), d.SourceServiceRevisions...)
out.DeploymentConfiguration = cloneDeploymentConfig(d.DeploymentConfiguration)

if d.Rollback != nil {
rollback := *d.Rollback
out.Rollback = &rollback
}

return out
}

// cloneServiceRevision deep-copies a service revision.
func cloneServiceRevision(r *driver.ServiceRevision) driver.ServiceRevision {
out := *r
out.CapacityProviderStrategy = append([]driver.CapacityProviderStrategyItem(nil), r.CapacityProviderStrategy...)
out.NetworkConfiguration = cloneNetworkConfig(r.NetworkConfiguration)
out.LoadBalancers = append([]driver.LoadBalancer(nil), r.LoadBalancers...)
out.ServiceRegistries = append([]driver.ServiceRegistry(nil), r.ServiceRegistries...)
out.ServiceConnect = cloneServiceConnect(r.ServiceConnect)

return out
}
Loading
Loading