Skip to content

feat: serve query and rest-xml from the generic CRUD engine - #143

Merged
skyoo2003 merged 10 commits into
mainfrom
feat/non-json-crud-engine
Sep 5, 2026
Merged

skyoo2003 merged 10 commits into
mainfrom
feat/non-json-crud-engine

Conversation

@skyoo2003

@skyoo2003 skyoo2003 commented Sep 5, 2026 •

Copy link
Copy Markdown
Owner

Summary

The generic CRUD engine now serves every protocol DevCloud registers. rest-xml and query were the last two, and they were the whole of PRD Milestone 5 — "Query / EC2 Query / REST-XML services within the demand set either meet a stated floor or are excluded from the published claim."

They are served, not excluded. The milestone permitted either; the measurement decided it.

Stacked on #142 — review that one first. Base is main rather than fix/s3-control-routing, so this PR's diff currently includes #142's four commits. That is deliberate: every workflow in this repo is gated on pull_request: branches: [main], so a PR based on another branch gets no CI at all beyond the labeler. Once #142 merges, this diff shrinks to its own 9 commits on its own.

Related Issue

Refs #

Before After
Registered 205 205
Serving ≥1 operation 199 201
Registered-only 6 4
auto-crud operations 4,858 4,970
unimplemented operations 3,053 2,941

The 112 moved operations are 94 in s3-control and 18 in elastic-load-balancing. Protocol is no longer a reason any registered service serves nothing — the four that remain have no CRUD-shaped operation anywhere in their API.

Changes

rest-xml was excluded on a false premise. It was assumed to have no modelled path. It has one: all 97 of s3-control's operations carry a method and URI template, so the same route table that classifies rest-json classifies it.

query genuinely is different — its operation is the Action field of a form body, not a header or a path — so it registers no route and the engine matches it from the body instead.

  • internal/shared/crud/xml.go — the one genuinely new thing: a map[string]any → AWS XML encoder. Both protocols needed it; neither needed anything else.
  • crud.Servable / crud.NeedsBody are now separate predicates, and the router asks the second one. See below.
  • internal/codegen/gen_crud_meta.go — engineServable admits both. This gate and crud.Servable answer the same question and a test now holds them together.

The blast radius is exactly two services, measured not assumed

Of the 20 in-tree awsQuery / ec2Query / restXml models, only elasticloadbalancing and s3control return plugin.ErrUnhandledOp. Every other one — s3, cloudfront, route-53, iam, sts, rds, ec2, elbv2, … — answers its own unknown operations and never enters the engine.

They gained CRUD registry entries and produced zero manifest changes, because BuildFidelityData gates auto-crud on EngineWired. The gate written for exactly this case did its job, and the fleet-wide flip that dominated the rest-json change (#139) did not happen here.

rest-xml bodies are still never buffered

S3 speaks rest-xml and its bodies are large binary uploads. The engine serves rest-xml without the body — every CRUD-shaped S3 Control operation addresses its resource with a path label or query term — so what used to be a side effect of the protocol being unservable is now an explicit rule, crud.NeedsBody, with a test double that fails if the gateway reads it.

Test Plan

Every change went RED first. Two RED runs worth quoting:

--- FAIL: TestServiceRouter_RESTXMLReachesEngineWithoutBuffering
    the gateway read a rest-xml request body; S3 uploads must keep streaming

That is the regression the predicate split exists to prevent, caught before it shipped.

--- FAIL: TestServiceRouter_QueryReachesEngine   (Servable("query") = false, want true)

GREEN:

CGO_ENABLED=0 go test ./...        all packages ok
make codegen (x2)                  identical hashes — idempotent
generate-imports.sh                imports.go unchanged
make build                         clean
pytest tests/compatibility/        854 passed, 2 skipped  (was 846/3)

Coverage on the changed packages: httproute 96.8%, crud 86.5%, gateway 84.8%, codegen 83.8%.

Manual, against ./dist/devcloud:

elb create -> describe round-trips:  ['manual-lb']
unclassifiable op still declines:    InvalidAction
s3control access point round-trips:  True
S3 10 MB put_object:                 0.02s, 10000000 bytes back

The boto3 tests assert on parsed members, not status codes, because the failure they exist to catch is silent: botocore's query parser given the wrong envelope returns a result with nothing in it rather than an error, so a 200 whose body it discards looks identical to a working service from Go's side of the wire.

What this does not claim

Breadth, not depth — unchanged from the PRD's stated scope.

  • ELB serves 18 of its 29 operations. The 11 it does not include ConfigureHealthCheck and RegisterInstancesWithLoadBalancer, which a Terraform aws_elb resource does call. Tested to decline with a parseable code, not to work.
  • S3 Control serves 94 of 97.
  • s3control is absent from ENGINE_SERVED_SERVICES and proved by its own file instead: all 97 of its operations require an AccountId, so the generic harness has no parameterless read to pick. The budgets precedent.
  • ec2-query is still not served, deliberately. Only EC2 speaks it and its provider is hand-written.

Checklist

  • Self-reviewed the code
  • Added/updated tests
  • Lint/format passes (pre-commit: gofmt, go vet, go build, ruff)
  • Updated documentation (docs/coverage.md, docs/crud-engine.md)
  • Added a Changie changelog fragment for user-facing changes

@github-actions github-actions Bot added documentation Improvements or additions to documentation tests Test code and test infrastructure codegen Smithy codegen and generated code labels Sep 5, 2026
@skyoo2003
skyoo2003 changed the base branch from fix/s3-control-routing to main September 5, 2026 17:07
@skyoo2003 skyoo2003 closed this Sep 5, 2026
@skyoo2003 skyoo2003 reopened this Sep 5, 2026
RED: TestEncodeXMLEnvelopes / TestEncodeXMLValues (9 subtests) /
TestEncodeXMLIsDeterministic / TestEncodeXMLDeclaresItsHeader all fail
against an unimplemented encodeXML — got "", want the envelope.

The engine's only serializer is okJSON, so query and rest-xml have no
success path at all, which is why both are registered-only. The stub
keeps the tree compiling (pre-commit runs go vet) so the RED is a real
assertion diff rather than a build error.

Decisions pinned here rather than discovered later: query gets the
<OpResponse><OpResult> envelope botocore's query parser looks for,
rest-xml the bare <OpResult>; lists wrap in <member>; an empty list is
an empty element, not an omitted key; keys are sorted for byte
stability; dotted query keys are dropped; values are escaped.
GREEN: all 13 encodeXML tests pass (4 top-level, 9 value subtests);
go test ./... fully green.

One encoder, two dialects. query gets <OpResponse><OpResult> plus a
ResponseMetadata/RequestId, because botocore's query parser reads the
result out of that nesting and returns an empty one without it;
rest-xml gets the bare <OpResult>, because its parser maps the root's
children onto the output shape and ignores the root's name.

No xmlns: botocore strips namespaces before matching element names, so
declaring one is bytes nothing reads. If that changes it belongs on
OpMeta from the model's trait, not guessed at in the encoder.
…edicate

RED: Servable("rest-xml") = false, want true; NeedsBody("query") =
false, want true; TestEngineRESTXMLRoundTrip and
TestEngineRESTXMLContentType fail.

Replaces TestEngineRESTXMLStillDeclines, which pinned the old
boundary. rest-xml is not on the wrong side of it: all 97 of
s3-control's operations are bound to a method and URI template, so the
route table that classifies rest-json classifies it too. What survives
is TestEngineQueryStillDeclines, until Task 4.

The new predicate is the point of this task. "Can the engine serve
this protocol" and "must the gateway buffer the body" are different
questions, and rest-xml is where they come apart: S3 speaks it, and S3
bodies are large binary uploads that must keep streaming.
GREEN: go test ./... fully green; make build clean; codegen idempotent
(identical hashes across two runs); imports.go unchanged.

rest-xml was excluded on the assumption that it had no modelled path.
It has one — all 97 of s3-control's operations carry a method and URI
template — so the route table that classifies rest-json classifies it
too. 94 operations move unimplemented -> auto-crud.

Blast radius is exactly one service, measured rather than assumed.
s3, cloudfront and route53 gain registry entries but not a single
manifest change, because BuildFidelityData gates auto-crud on
EngineWired and none of their providers returns ErrUnhandledOp. The
gate written for this case did its job.

Servable and NeedsBody are now separate questions and the router asks
the second one. The engine serves rest-xml without the body, which is
what keeps S3's large binary uploads streaming; a test double that
fails on Read proves the gateway never touches them.
RED: Servable("query") = false, want true; TestEngineQueryRoundTrip,
TestEngineQueryDropsProtocolFields, TestEngineQueryBodyIsNotJSON and
TestEngineQueryContentType all fail.

query is the last protocol and the odd one: its operation is neither a
header nor a path but the Action field of a form body. Two traps are
specified rather than left to be hit — Action and Version describe the
request, not the resource, so they must not be stored and echoed back
as members; and Handle's JSON body parse must not treat a form-encoded
body as a malformed JSON one, which would decline every query call
with a SerializationException.
GREEN: go test ./... fully green; make build clean; codegen idempotent.

query is the last protocol and the odd one out. Its operation is
neither a header nor a path but the Action field of a form body, which
is why it outlived rest-json and rest-xml as a gap — and why it
registers no route: a query operation has no method or URI at all.

18 operations move unimplemented -> auto-crud, all in
elasticloadbalancing. Thirteen other query services gain registry
entries and not one manifest change, including elbv2 sitting directly
beside it, because EngineWired gates the label and none of their
hand-written providers returns ErrUnhandledOp.

Two traps closed by construction rather than by luck: Action and
Version are dropped before storage, so a created resource does not
echo <Action> back as a member; and the JSON body parse is now an
else-branch, because reaching it with a form-encoded body would have
declined every query call with a SerializationException.
GREEN: 854 passed, 2 skipped (was 846/3); go test ./... green.

Both leave REGISTERED_ONLY_SERVICES. elb joins the generic smoke list;
s3control cannot, and that is a finding rather than a test to loosen —
all 97 of its operations take a required AccountId, so the harness has
no parameterless read to pick. It is proved in test_s3control.py
instead, the budgets precedent.

These tests assert on parsed members, not status codes, because the
failure mode they exist to catch is silent: botocore's query parser
given the wrong envelope returns a result with nothing in it rather
than an error, so a 200 whose body it discards is indistinguishable
from a working service on the Go side of the wire.

Both files also pin the edge of the claim. ConfigureHealthCheck is one
of ELB's 11 unclassifiable operations and a Terraform aws_elb resource
calls it; it must decline with a parseable code, not an empty success.
205 registered / 201 serving >=1 / 4 registered-only, and 4,496
hand-verified / 4,970 auto-crud / 2,941 unimplemented. Every number is
read out of internal/generated/fidelity/manifest_gen.go, not out of
the plan; go test ./cmd/devcloud/ asserts the manifest against the
live registry.

The interesting edit is a deletion. coverage.md listed two reasons a
registered service can serve nothing; one of them is gone. Protocol is
no longer a reason, so the four that remain are all the same case —
no CRUD-shaped operation anywhere in their API — and the page says so
rather than leaving a category with nothing in it.

crud-engine.md documents the limits each new protocol brings, because
they are real: rest-xml request bodies are never read, query keeps
flat form keys only and drops Action and Version, lists are always
<member>-wrapped, no xmlns is emitted. The PRD retires the non-JSON
risk as closed rather than as excluded.
errcheck flagged both xml.EscapeText calls. The error is the Writer's
and a strings.Builder never fails a write, so the discard is explicit
with the reason attached rather than implicit.

Found by running golangci-lint locally: every CI workflow in this repo
is gated on `pull_request: branches: [main]`, so a PR stacked on
another branch gets no CI at all beyond the labeler.
@skyoo2003
skyoo2003 force-pushed the feat/non-json-crud-engine branch from 64dc8d1 to 9804d7b Compare September 5, 2026 17:18
@skyoo2003
skyoo2003 merged commit 0f75856 into main Sep 5, 2026
8 checks passed
@skyoo2003
skyoo2003 deleted the feat/non-json-crud-engine branch September 5, 2026 17:23
skyoo2003 added a commit that referenced this pull request Sep 5, 2026
…ions

GREEN: 126 passed, 17 skipped, 2 xfailed. The six failures this change targets
are gone; the two that remain are different defects, marked xfail(strict) with
what was observed rather than a guessed cause.

The default branch of each provider returned 200 with an empty
<ActionResponse/> for any action it did not implement. Two consequences, both
bad: the caller was told an operation succeeded when nothing ran, and the body
carried no <ActionResult> wrapper, so botocore's query parser raised KeyError
instead of returning a value. neptune.describe_db_cluster_endpoints() crashed
the SDK outright.

Each now returns plugin.ErrUnhandledOp, which is what every other provider in
the fleet does and what the CRUD engine has been reachable through since
query was admitted in #143. 223 operations move from unimplemented to
auto-crud — they are served from the real store now — and the rest decline
with InvalidAction.

docs/coverage.md's tier table moves with them: auto-crud 4,970 -> 5,193,
unimplemented 2,941 -> 2,718, total unchanged. That edit was not optional;
the gate added in #144 failed until the published figures matched the
manifest, naming both numbers.
skyoo2003 added a commit that referenced this pull request Sep 5, 2026
…ions (#145)

* test: add reproducer for fabricated successes on unserved operations

RED: 8 failed, 120 passed, 17 skipped.

    FAILED [autoscaling.DescribeAdjustmentTypes]
    FAILED [cloudformation.DescribeChangeSetHooks]
    FAILED [elasticache.DescribeCacheEngineVersions]
    FAILED [elasticloadbalancingv2.DescribeCapacityReservation]
    FAILED [rds.DescribeBlueGreenDeployments]
    FAILED [redshift.DescribeAccountAttributes]
    FAILED [resourcegroups.Tag]
    FAILED [s3.ListBucketAnalyticsConfigurations]

Each returned HTTP 200 for an operation the fidelity manifest does not list as
served. docs/coverage.md calls that the one thing DevCloud must never do.

test_service_smoke.py already asserted this for services that serve nothing at
all. The gap was the other case: a service that serves plenty and answers an
operation it does not implement with an empty success anyway.

* fix: eight query providers fabricated a success for unimplemented actions

GREEN: 126 passed, 17 skipped, 2 xfailed. The six failures this change targets
are gone; the two that remain are different defects, marked xfail(strict) with
what was observed rather than a guessed cause.

The default branch of each provider returned 200 with an empty
<ActionResponse/> for any action it did not implement. Two consequences, both
bad: the caller was told an operation succeeded when nothing ran, and the body
carried no <ActionResult> wrapper, so botocore's query parser raised KeyError
instead of returning a value. neptune.describe_db_cluster_endpoints() crashed
the SDK outright.

Each now returns plugin.ErrUnhandledOp, which is what every other provider in
the fleet does and what the CRUD engine has been reachable through since
query was admitted in #143. 223 operations move from unimplemented to
auto-crud — they are served from the real store now — and the rest decline
with InvalidAction.

docs/coverage.md's tier table moves with them: auto-crud 4,970 -> 5,193,
unimplemented 2,941 -> 2,718, total unchanged. That edit was not optional;
the gate added in #144 failed until the published figures matched the
manifest, naming both numbers.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

codegen Smithy codegen and generated code documentation Improvements or additions to documentation tests Test code and test infrastructure

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant