Skip to content
Merged
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
2 changes: 1 addition & 1 deletion Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ test-compat:
cd tests/compatibility && pip install -q -r requirements.txt && pytest -v

codegen:
go run ./cmd/codegen -models ./smithy-models -output ./internal/generated -templates ./internal/codegen/templates
go run ./cmd/codegen -models ./smithy-models -output ./internal/generated -templates ./internal/codegen/templates -scaffold-output ./internal/services

codegen-s3:
go run ./cmd/codegen -models ./smithy-models -output ./internal/generated -services s3 -templates ./internal/codegen/templates
Expand Down
4 changes: 2 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -27,8 +27,8 @@ DevCloud is an **on-ramp to the cloud**, not a replacement for it. The goal is t

## Features

- **213 AWS services registered, 209 serving at least one operation** — the remaining 4 are routed and decline with a clean AWS error rather than letting the call bill a real account. See [coverage.md](docs/coverage.md) for what the numbers do and do not promise.
- **boto3-compatible** — a 1,156-test suite runs in CI (`make test-compat`) across every registered service. Unsupported operations return a clean AWS error, never a false success.
- **431 AWS services registered, 426 serving at least one operation** — the remaining 5 are routed and decline with a clean AWS error rather than letting the call bill a real account. See [coverage.md](docs/coverage.md) for what the numbers do and do not promise.
- **boto3-compatible** — a 1,530-test suite runs in CI (`make test-compat`) across every registered service. Unsupported operations return a clean AWS error, never a false success.
- **Cross-service integration** — CloudFormation provisioning, DynamoDB Streams → Lambda, EventBridge targets, S3 → Lambda
- **Smithy-driven codegen** — Go types, routers and error catalogues generated from AWS models, with a weekly sync workflow that keeps them current
- **Single binary, zero-config** — one Docker image, one port (4747), no config file required; override with `DEVCLOUD_SERVICES`, `DEVCLOUD_DATA_DIR`, `DEVCLOUD_PORT`
Expand Down
6 changes: 6 additions & 0 deletions changes/unreleased/Added-20260913-160000.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
kind: Added
body: 'The 218 AWS services that had generated routers but no provider package are
now registered from the codegen scaffold template, so their SDK calls are answered
locally instead of reaching a real AWS account — coverage moves to 431 registered
and 426 serving at least one operation'
Issue: "162"
6 changes: 6 additions & 0 deletions changes/unreleased/Fixed-20260913-161500.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
kind: Fixed
body: 'The CRUD engine no longer answers an operation it cannot classify with a
broader sibling''s route — chime''s AssociatePhoneNumberWithUser was returning
UpdateUser''s 200 — because the registry now carries every REST-bound operation
and declines the unclassifiable ones instead of letting their paths fall through'
Issue: "162"
218 changes: 218 additions & 0 deletions cmd/devcloud/imports.go

Large diffs are not rendered by default.

50 changes: 50 additions & 0 deletions cmd/devcloud/routing_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,9 @@ import (
"testing"

"github.com/skyoo2003/devcloud/internal/gateway"
"github.com/skyoo2003/devcloud/internal/shared/crud"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)

// TestContestedDataPlanesResolveToThemselves covers the three Phase 2 services
Expand Down Expand Up @@ -47,3 +49,51 @@ func TestContestedDataPlanesResolveToThemselves(t *testing.T) {
})
}
}

// TestUnclassifiableRouteDeclinesInsteadOfAnsweringAsASibling is the
// end-to-end half of the fabricated-success fix, against the registry the
// binary actually ships rather than a synthetic one.
//
// Both operations below bind to a path a classified sibling also claims,
// separated only by a query constraint: chime's AssociatePhoneNumberWithUser
// against UpdateUser, apigateway's ImportRestApi against CreateRestApi. While
// the registry held only classified operations, httproute.Match had no route
// specific enough to prefer, so the sibling answered 200 for an operation
// nothing implements.
//
// It lives here for the same reason the test above does: this package
// blank-imports every service and the generated crudregistry, so the route
// table under test is the real one. The compatibility suite cannot reach
// either case — _unserved_probe picks one operation per service and prefers
// Describe/List/Get, and "Import" is excluded as mutating outright.
func TestUnclassifiableRouteDeclinesInsteadOfAnsweringAsASibling(t *testing.T) {
cases := []struct{ name, service, method, uri string }{
{"chime_associate_phone_number", "chime", "POST",
"/accounts/a1/users/u1?operation=associate-phone-number"},
{"apigateway_import_rest_api", "apigateway", "POST", "/restapis?mode=import"},
}
for _, c := range cases {
t.Run(c.name, func(t *testing.T) {
res, err := crud.Handle(crud.Call{
Service: c.service, Protocol: "rest-json",
Method: c.method, URI: c.uri, Body: []byte("{}"),
})
require.ErrorIs(t, err, crud.ErrUnclassified,
"an operation the engine cannot classify must decline, not borrow a sibling's answer")
assert.Nil(t, res)
})
}

// The other direction, so declining never becomes the easy way to pass:
// the classified sibling still answers at its own path.
t.Run("sibling_still_serves_its_own_path", func(t *testing.T) {
res, err := crud.Handle(crud.Call{
Service: "chime", Protocol: "rest-json",
Method: "POST", URI: "/accounts/a1/users/u1",
Body: []byte(`{"LicenseType":"Pro"}`),
})
require.NoError(t, err)
require.NotNil(t, res)
assert.Equal(t, 200, res.Status, "chime UpdateUser must still be served")
})
}
2 changes: 1 addition & 1 deletion docs/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@

| Page | What it covers |
|---|---|
| [Coverage](coverage.md) | 213 registered / 209 serving — what the counts promise, and the target |
| [Coverage](coverage.md) | 431 registered / 426 serving — what the counts promise, and the target |
| [Compatibility Policy](compatibility-policy.md) | What v1.0 guarantees across 1.x, what it does not, and how deprecation works |
| [Fidelity Manifest](fidelity-manifest.md) | Per-operation tiers: how much to trust any given call |
| [CRUD Engine](crud-engine.md) | How engine-served operations behave, and where they stop |
Expand Down
12 changes: 6 additions & 6 deletions docs/compatibility-policy.md
Original file line number Diff line number Diff line change
Expand Up @@ -73,7 +73,7 @@ appears, and every operation the CRUD engine serves is present and not filed as
[`cmd/devcloud/fidelity_test.go`](../cmd/devcloud/fidelity_test.go).

What no test can catch is an operation that never reaches the manifest at all. That is bounded
rather than eliminated: for the 194 services with an in-tree Smithy model the operation universe
rather than eliminated: for the 420 services with an in-tree Smithy model the operation universe
comes from the model, so an operation losing its implementation reclassifies to `unimplemented`
instead of disappearing. For the 11 without one, the universe *is* what the providers serve, so
the manifest lists no unimplemented tail for them. `modelBacked` on
Expand All @@ -93,7 +93,7 @@ the operation. `CreateFunction` in `test_lambda.py` shows all three cases at onc
| `FunctionArn` | present | presence only — not that it stays ARN-shaped |
| `Runtime`, `Handler`, `MemorySize` | not asserted | nothing, though today's response includes them |

That narrowness is the point: it is the promise the repo can actually keep. The suite — 1,156
That narrowness is the point: it is the promise the repo can actually keep. The suite — 1,530
tests driving real boto3 clients — runs in CI on every push and again against the tagged commit
before a release publishes, so breaking an assertion fails the build rather than depending on
review discipline. Anything the suite does not assert rests on nothing but intent. Widening the
Expand All @@ -103,20 +103,20 @@ promise means adding or strengthening assertions, and such contributions are wel

Depending on any of the following will break, and breaking it is **not** a major-version event.

- **`auto-crud` response content.** 5,193 operations are served by the
- **`auto-crud` response content.** 10,871 operations are served by the
[generic CRUD engine](crud-engine.md) at fidelity that is deliberately *plausible, not
faithful*: store-backed responses echoing your input plus synthesized ids and ARNs, with no
validation, no cross-resource integrity, no pagination correctness and no business logic.
Their shape and content may change in any release. Use them to wire an SDK up, nothing more.
- **Hand-verified operations with no compatibility test.** Of 4,497 hand-verified operations,
- **Hand-verified operations with no compatibility test.** Of 4,528 hand-verified operations,
only what the suite covers is promised. The rest are best-effort.
- **Data durability.** Stores are local development stores. Several are in-memory and
per-process; on-disk layouts under `data_dir` may change format between releases without a
migration. Do not treat DevCloud as a database.
- **`unimplemented` → served transitions.** An operation that returns an error today may start
returning a response. This is additive, and ships in a minor release.
- **Service coverage.** New services may be added in a minor release. The 205 services registered
today are a floor, not a ceiling — and not a promise of depth either: 4 of them serve no
- **Service coverage.** New services may be added in a minor release. The 431 services registered
today are a floor, not a ceiling — and not a promise of depth either: 5 of them serve no
operation and only decline cleanly. See [coverage.md](coverage.md).
- **Error codes, HTTP status and message wording.** What *is* guaranteed for an `unimplemented`
operation is that it **fails** — an AWS-shaped error, never a fabricated success. *Which* error
Expand Down
97 changes: 67 additions & 30 deletions docs/coverage.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,22 +6,24 @@ alone.

| Number | What it means | Today |
|---|---|---|
| **Registered** | The gateway routes the service, so the call reaches DevCloud instead of real AWS. | **213** |
| **Serving ≥1 operation** | At least one operation returns a real, store-backed answer. | **209** |
| **Registered-only** | Routed, but every operation declines with a clean AWS error. | **4** |
| **Compatibility-tested** | A boto3 test exercises the service in CI and passes. | **211** |
| **Registered** | The gateway routes the service, so the call reaches DevCloud instead of real AWS. | **431** |
| **Serving ≥1 operation** | At least one operation returns a real, store-backed answer. | **426** |
| **Registered-only** | Routed, but every operation declines with a clean AWS error. | **5** |
| **Compatibility-tested** | A boto3 test exercises the service in CI and passes. | **426** |

Per operation, from the [fidelity manifest](fidelity-manifest.md):

| Tier | Operations |
|---|---|
| `hand-verified` | 4,528 |
| `auto-crud` | 5,193 |
| `unimplemented` | 2,717 |
| **total known** | **12,438** |
| `auto-crud` | 10,871 |
| `unimplemented` | 3,802 |
| **total known** | **19,201** |

> **The coverage target is 205 services, not 431.** It was 431, and the evidence
> did not support it — see [The target](#the-target).
> **The target is depth for 205 services, not breadth for 431.** All 431 are
> registered — the codegen scaffold made breadth nearly free — but registration is
> not the promise. What the evidence refused was a promise of *depth* across 431,
> and it still refuses it. See [The target](#the-target).

Every figure on this page is asserted against the binary by
`go test ./cmd/devcloud/`. Editing one here without the code moving fails CI, and
Expand All @@ -37,20 +39,28 @@ the second still stops it.

| Protocol | Services | Operation name comes from |
|---|---|---|
| `rest-json` | 99 | HTTP method + path (`internal/shared/httproute`) |
| `json-1.1` | 66 | the `X-Amz-Target` header |
| `json-1.0` | 17 | the `X-Amz-Target` header |
| `rest-json` | 251 | HTTP method + path (`internal/shared/httproute`) |
| `json-1.1` | 100 | the `X-Amz-Target` header |
| `json-1.0` | 48 | the `X-Amz-Target` header |
| `query` | 15 | the `Action` form field |
| `rest-xml` | 4 | HTTP method + path |
| no in-tree model | 12 | n/a — hand-written providers |
| unrecognised protocol | 1 | n/a — `partnercentralrevenuemeasurement` is `rpcv2Cbor` |

**The operation is not CRUD-shaped.** `GetThing`, `ListThings` and `CreateThing`
map onto a generic store. `ExecuteStatement`, `InvokeEndpoint` and
`QueryForecast` do not, and the engine refuses them rather than inventing an
answer. This is now the *only* reason a service serves nothing, and it applies to
exactly four: `forecastquery`, the two SageMaker Runtime variants
`sagemaker-runtime` and `sagemakerruntimehttp2`, and `rds-data`. No protocol
change reaches them.
answer. This applies to four services: `forecastquery`, the two SageMaker Runtime
variants `sagemaker-runtime` and `sagemakerruntimehttp2`, and `rds-data`. No
protocol change reaches them.

**The protocol is one the parser does not read.** This applies to exactly one
service, and it is a different failure from the four above.
`partnercentralrevenuemeasurement` speaks `smithy.protocols#rpcv2Cbor`, which
`internal/codegen/parser.go` does not recognise, so *none* of its operations is
classified — not because their names are unshaped, but because the model never
reached the classifier. Teaching the parser a sixth protocol would reach it; no
amount of CRUD-shaping would.

Registering a service the engine cannot serve is deliberate. The alternative is
worse: an *unregistered* service is not routed, so the SDK call leaves the
Expand All @@ -64,17 +74,31 @@ when a provider returns `plugin.ErrUnhandledOp`, so a hand-written provider that
refuses unknown operations itself (`apigatewayv2`, `xray`) never reaches it. The
manifest records this per service as `EngineWired`.

## Why compatibility-tested is 211, not 213
## Why compatibility-tested is 426, not 431

`tests/compatibility/test_service_smoke.py` parametrises over the generated
service list rather than a hand-written one, so a service cannot be registered
and quietly go untested — which is what 31 of them were until this was measured.

Two are excluded, and they are not a backlog: botocore publishes no client for
`sagemakerruntimehttp2` or `transcribestreaming` (`sagemaker-runtime` and
`transcribe` are different clients with different APIs). No boto3 test can exist
for a client that does not exist. That is a property of the AWS SDK, and it is
the ceiling on this number.
Five are excluded, and they are not a backlog. In every one it is botocore, not
DevCloud, that stops the request, so no answer DevCloud could give would change
the outcome. Each stays registered: the call is still answered locally instead
of reaching a billed AWS account.

**No client exists** (2). botocore publishes none for `sagemakerruntimehttp2` or
`transcribestreaming` (`sagemaker-runtime` and `transcribe` are different clients
with different APIs). No boto3 test can exist for a client that does not exist.

**The client exists but cannot be pointed at localhost** (3). `codecatalyst`
authenticates with a bearer token rather than SigV4, so botocore raises
`NoAuthTokenError` before the request is built. `cloudfront-keyvaluestore`
resolves its endpoint from a KVS ARN and so never honours `endpoint_url`.
`partnercentralrevenuemeasurement` decodes every reply with botocore's CBOR
parser, and DevCloud has no CBOR encoder, so even a clean decline reads as a
corrupt frame — see the protocol table above.

Both sets are pinned in `tests/compatibility/_coverage.py` and asserted, so
adding a sixth is a deliberate edit that moves this figure with it.

## Contested signing names

Expand Down Expand Up @@ -114,6 +138,17 @@ wants. Before committing to building ~283 of them, the assumption was tested
against three independent projects that each only add a service when someone
asks. It did not hold.

**All 431 are now registered, and the decision above still stands.** What it
refused was the *cost* — hand-building 283 services on the assumption someone
wanted them. The codegen scaffold removed that cost: registering the remaining
226 became a flag on `make codegen`, not a programme of work, and the services it
reached are served by the generic [CRUD engine](crud-engine.md) at engine
fidelity. So breadth was taken because it turned out to be nearly free, and the
target stayed where the evidence put it, because the target was never a count of
registrations — it is where DevCloud promises to be worth trusting. Read the
table below as two different claims, not one: 431 services answer locally instead
of billing a real account, and 205 are the ones whose depth is a commitment.

**The rule was fixed before the numbers were seen** — four outcomes written down
in advance, including one for "the method itself failed", specifically so the
result could not be argued into whichever answer was most convenient. Full
Expand All @@ -139,15 +174,17 @@ does not reach every service, and saying so is cheaper than a fabricated success

| | Services |
|---|---|
| Registered today | **205** |
| Registered today | **431** |
| Target: registered + demonstrated demand | **205 — met** |
| Explicitly not targeted | 226 |
| Registered, scaffold-served, outside the target | 226 |

Four fifths of the AWS surface DevCloud does not cover is surface that three
projects with far more history and staffing have collectively declined to build.
That is what a long tail looks like. The 226 are not refused — any of them can be
onboarded when someone asks. What changed is that they are no longer work
DevCloud has promised.
Four fifths of the AWS surface is surface that three projects with far more
history and staffing have collectively declined to build. That is what a long
tail looks like, and it is why the 226 carry no promise of depth: each is
registered and engine-served, so a call to one is answered locally in AWS's error
vocabulary instead of reaching a billed account, but nothing here commits to
making any of them faithful. That commitment follows demand, and the instrument
below is what measures it.

**What this verdict is not.** The three sources measure *emulator and provider
effort*, not user demand. They are a proxy, chosen because DevCloud had no
Expand Down Expand Up @@ -251,7 +288,7 @@ re-derive it with `python3 scripts/model_churn.py --upstream`.
make codegen # regenerate the manifest from the models
make stats # registered services and hand-written operations
go test ./cmd/devcloud/ # asserts every number on this page against the binary
make test-compat # the compatibility-tested number, over all 213 services
make test-compat # the compatibility-tested number, over all 431 services
```

Every figure comes from `internal/generated/fidelity/manifest_gen.go` and nothing
Expand Down
2 changes: 1 addition & 1 deletion docs/faq.md
Original file line number Diff line number Diff line change
Expand Up @@ -38,7 +38,7 @@ For services with a persistent backend (S3, DynamoDB, Lambda, IAM/STS), yes —
## Compatibility

**Will my existing boto3 code work?**
Most apps using the core services (S3, SQS, DynamoDB, Lambda, IAM, STS) and common integration services (SNS, CloudWatch, KMS, Secrets Manager, EventBridge, CloudFormation) work with only an `endpoint_url` change. The 1,156-test boto3 suite runs green in CI on every push; what that does and does not promise is spelled out in [compatibility-policy.md](compatibility-policy.md#wire-behaviour--scoped-to-the-compatibility-suite).
Most apps using the core services (S3, SQS, DynamoDB, Lambda, IAM, STS) and common integration services (SNS, CloudWatch, KMS, Secrets Manager, EventBridge, CloudFormation) work with only an `endpoint_url` change. The 1,530-test boto3 suite runs green in CI on every push; what that does and does not promise is spelled out in [compatibility-policy.md](compatibility-policy.md#wire-behaviour--scoped-to-the-compatibility-suite).

**What about Terraform / CDK?**
Point the AWS provider or CDK at `http://localhost:4747` with dummy credentials. Common resources (`aws_s3_bucket`, `aws_dynamodb_table`, `aws_lambda_function`) work out of the box. Complex IAM policies and deeply CSP-coupled resources are out of scope.
Expand Down
Loading