Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
177 commits
Select commit Hold shift + click to select a range
c7e2b2f
feat(eventbridge): deepen AWS emulation parity — anything-but nested …
Jun 20, 2026
8b0ce68
feat(secretsmanager): deepen AWS emulation parity — X-Amzn-Errortype …
Jun 20, 2026
dc768d7
feat(stepfunctions): deepen AWS emulation parity — StringMatches Choi…
Jun 20, 2026
85efcc7
feat(cloudwatchlogs): deepen AWS emulation parity — FilterLogEvents s…
Jun 20, 2026
a38ec85
feat(kinesis): deepen AWS emulation parity — EndingSequenceNumber onl…
Jun 20, 2026
aeb5883
feat(sns): deepen AWS emulation parity — $or operator, cidr/wildcard,…
Jun 20, 2026
73f7ab6
feat(route53): deepen AWS emulation parity — DELETE exact-match valid…
Jun 20, 2026
3015314
feat(apigateway): deepen AWS emulation parity — real OpenAPI/Swagger …
Jun 20, 2026
927f3f4
feat(ecs): deepen AWS emulation parity — RegisterTaskDefinition conta…
Jun 20, 2026
f24f3ac
feat(sqs): deepen AWS emulation parity — recompute MD5OfMessageAttrib…
Jun 20, 2026
68e0e1e
feat(lambda): deepen AWS emulation parity — GetFunction/GetFunctionCo…
Jun 20, 2026
7aa58fc
feat(iam): deepen AWS emulation parity — enforce IAM policy grammar o…
Jun 20, 2026
9f7c945
feat(cloudwatch): deepen AWS emulation parity — TreatMissingData=igno…
Jun 20, 2026
1968f88
feat(dynamodb): deepen AWS emulation parity — append on out-of-range …
Jun 20, 2026
a22a5b7
feat(ses): deepen AWS emulation parity — templated email variable sub…
Jun 20, 2026
b20cf83
feat(kms): deepen AWS emulation parity — IncorrectKeyException for De…
Jun 20, 2026
bccc8bd
feat(ssm): deepen AWS emulation parity — parameter version/label sele…
Jun 20, 2026
6a9c3ad
feat(s3): deepen AWS emulation parity — proper 416 InvalidRange XML a…
Jun 20, 2026
1469a7a
feat(sts): deepen AWS emulation parity — strip IAM path from assumed-…
Jun 20, 2026
706e838
WIP: checkpoint (auto)
Jun 20, 2026
cdeec1d
feat(glue): add NextToken/MaxResults pagination to GetDatabases, GetT…
Jun 20, 2026
20df5db
WIP: checkpoint (auto)
Jun 20, 2026
a02fb0c
feat(athena): deepen parity — 5 AWS-accuracy fixes (go-h2imn)
Jun 20, 2026
42464e7
feat(ec2): deepen AWS emulation parity — EBS IOPS and throughput on C…
Jun 20, 2026
9ce191f
feat(firehose): error __type field + PutRecordBatch per-record Record…
Jun 20, 2026
8444077
feat(ecr): DescribeImages filter.tagStatus parity with real AWS (go-6…
Jun 20, 2026
b7fcc85
feat(eks): implement update history tracking (go-fqrva)
Jun 20, 2026
4abaff8
feat(efs): add MountTargetArn to responses + AccessPointId filter (go…
Jun 20, 2026
b0c83ca
WIP: checkpoint (auto)
Jun 20, 2026
c93f269
feat(elasticache): DescribeSnapshots SnapshotSource filter + ShowCach…
Jun 20, 2026
7e1518e
feat(batch): ListJobs summary adds jobArn/startedAt/stoppedAt + statu…
Jun 20, 2026
c93cad5
feat(redshift): SnapshotType+SnapshotCreateTime in response + Describ…
Jun 20, 2026
b9eb4d6
WIP: checkpoint (auto)
Jun 20, 2026
5829a92
feat(emr): bootstrap actions round-trip — ListBootstrapActions now re…
Jun 20, 2026
cd8f595
feat(cloudtrail): GetInsightSelectors returns InsightNotEnabledExcept…
Jun 20, 2026
aebd0dd
feat(autoscaling): emit EnabledMetrics in DescribeAutoScalingGroups r…
Jun 20, 2026
9d4ca0a
WIP: checkpoint (auto)
Jun 20, 2026
54c1295
feat(cloudfront): reject DELETE of enabled distributions with Distrib…
Jun 20, 2026
f28d692
feat(codepipeline): parity-deepen — latestExecution in GetPipelineSta…
Jun 20, 2026
47359cf
fix(organizations): TagResource/UntagResource/ListTagsForResource ret…
Jun 20, 2026
d251c99
fix(elbv2): parity-deepen — name-not-found errors + DescribeTargetHea…
Jun 20, 2026
a3fac2c
WIP: checkpoint (auto)
Jun 20, 2026
590fb65
WIP: checkpoint (auto)
Jun 20, 2026
fab6a2b
fix(transfer): goimports alignment in backend.go SSH key type constan…
Jun 20, 2026
d5367f4
WIP: checkpoint (auto)
Jun 20, 2026
6f8637f
fix(codebuild): fix lint issues in StartBuild envOverrides implementa…
Jun 20, 2026
ea87f22
WIP: checkpoint (auto)
Jun 20, 2026
0a2fd4e
fix(comprehend): parity audit — offset correctness and PII label fide…
Jun 20, 2026
11ebc30
fix(polly): speech marks — emit sentence/ssml once per unit not per w…
Jun 20, 2026
616771f
fix(dms): parity audit — 5 genuine behavioral gaps (go-ba6bv)
Jun 20, 2026
ef1aadc
WIP: checkpoint (auto)
Jun 20, 2026
6024ab2
fix(apigatewayv2): resolve gocognit and govet shadow lint violations
Jun 20, 2026
0cc6f79
fix(elasticsearch): UnprocessedDomains + AddTags duplicate key reject…
Jun 20, 2026
dba06ba
WIP: checkpoint (auto)
Jun 20, 2026
36673ad
fix(dms): parity audit — ARN validation, endpoint in-use guard, asses…
Jun 20, 2026
8829d8b
parity-deepen: wafv2 DescribeManagedRuleGroup returns real Rules and …
Jun 20, 2026
a845549
parity-deepen: transcribe MedicalTranscriptionJob returns Transcript.…
Jun 20, 2026
7855747
parity-deepen: textract AnalyzeDocument rejects unknown FeatureType s…
Jun 20, 2026
2f0b166
parity-deepen: scheduler UpdateSchedule enforces required-field valid…
Jun 20, 2026
1cdbadb
parity-deepen: guardduty DeleteDetector cleans up per-detector sub-re…
Jun 20, 2026
3cc2919
parity-deepen: kafka CreateConfiguration returns state+latestRevision…
Jun 20, 2026
ccbdc50
parity-deepen: sagemaker CreateEndpointConfig rejects empty Productio…
Jun 20, 2026
f5bbf43
parity-deepen: cognitoidp ForgotPassword rejects disabled and unconfi…
Jun 20, 2026
9eb2065
parity-deepen: waf GetSampledRequests echoes TimeWindow in response (…
Jun 20, 2026
4f08a1e
parity-deepen: vpclattice GetAuthPolicy returns 404 for unset policie…
Jun 20, 2026
84a61a2
parity-deepen: verifiedpermissions BatchGetPolicy includes definition…
Jun 20, 2026
0b34080
parity-deepen: translate TranslateText/TranslateDocument include Appl…
Jun 20, 2026
f7724da
WIP: checkpoint (auto)
Jun 20, 2026
56d8d70
parity-deepen: transfer WebApp customization validates WebAppId exist…
Jun 20, 2026
5ef1576
dynamodb: honor ReturnValuesOnConditionCheckFailure on single-item & …
claude Jun 20, 2026
6010466
dynamodb: return ItemCollectionMetrics only for LSI tables, with real…
claude Jun 20, 2026
fa10c79
dynamodb: match AWS ValidationException wording for size limits
claude Jun 20, 2026
5234956
WIP: checkpoint (auto)
Jun 20, 2026
7e6f345
parity-deepen: transcribe CallAnalytics completed jobs include Transc…
Jun 20, 2026
58100c5
parity-deepen: timestreamwrite WriteRecords validates MeasureName is …
Jun 20, 2026
0cbaa14
dynamodb: GetRecords returns nil NextShardIterator at end of a closed…
claude Jun 20, 2026
0051ed6
dynamodb: real S3-backed ImportTable/Export and not-found fidelity
claude Jun 20, 2026
eff6dee
parity-deepen: timestreamquery CreateScheduledQuery requires Notifica…
Jun 20, 2026
8e6af8a
ui(dynamodb): add Tags tab for table tag management
claude Jun 20, 2026
4c64e2c
ui(dynamodb): show approximate item size in the Items tab
claude Jun 20, 2026
f9732a9
WIP: checkpoint (auto)
Jun 20, 2026
20ffb29
parity-deepen: textract AnalyzeDocument and StartDocumentAnalysis req…
Jun 20, 2026
6acec29
parity-deepen: swf SignalWorkflowExecution requires non-empty signalN…
Jun 20, 2026
80a60d4
parity-deepen: support AddAttachmentsToSet requires non-empty attachm…
Jun 20, 2026
a774cb3
WIP: checkpoint (auto)
Jun 20, 2026
359fdda
parity-deepen: stepfunctions CreateStateMachine requires non-empty ro…
Jun 20, 2026
29b4918
WIP: checkpoint (auto)
Jun 20, 2026
a16473f
parity-deepen: ssoadmin AttachManagedPolicyToPermissionSet validates …
Jun 20, 2026
096f00f
parity-deepen: ssm PutParameter validates parameter Type is String/St…
Jun 20, 2026
0e6ca55
parity-deepen: sesv2 SendEmail validates non-empty Destination addres…
Jun 20, 2026
10961d3
WIP: checkpoint (auto)
Jun 20, 2026
56d1d65
parity-deepen: ses SendEmail validates non-empty Destination addresse…
Jun 20, 2026
c068f8c
parity-deepen: securityhub CreateInsight validates Name and GroupByAt…
Jun 20, 2026
06e6437
WIP: checkpoint (auto)
Jun 20, 2026
5e4925e
parity-deepen: secretsmanager GetSecretValue and PutSecretValue valid…
Jun 20, 2026
d36a7aa
s3: correct delete-marker GET/HEAD semantics (x-amz-delete-marker, 40…
claude Jun 20, 2026
6c02a61
s3: apply encoding-type=url to list responses
claude Jun 20, 2026
41bfc52
s3: validate object tag sets and reject illegal self-copy
claude Jun 20, 2026
b102517
s3: parent background replication to the service context, not the req…
claude Jun 20, 2026
476fd6e
s3: honor If-Range on range GETs and max-keys=0 on ListObjectVersions
claude Jun 20, 2026
f2c67bd
s3: resolve request region from the central awsmeta context
claude Jun 20, 2026
fce8d0f
dynamodb: source request region/account from the central awsmeta iden…
claude Jun 20, 2026
5408d99
parity-deepen: sagemaker CreateModel validates ExecutionRoleArn is re…
Jun 20, 2026
fe86761
WIP: checkpoint (auto)
Jun 20, 2026
ffcc718
parity-deepen: s3tables DeleteTableBucketEncryption now clears config…
Jun 20, 2026
59ea036
ui(s3): add Object Lock, Notifications, Replication, Logging, Ownersh…
claude Jun 20, 2026
75f3e43
ui(s3): add bucket ACL plus Analytics/Metrics/Inventory/Int-Tiering tabs
claude Jun 20, 2026
45eeb0e
WIP: checkpoint (auto)
Jun 20, 2026
b37e041
parity-deepen: s3control CreateAccessGrantsLocation validates IAMRole…
Jun 20, 2026
58a433d
WIP: checkpoint (auto)
Jun 20, 2026
f63486b
parity-deepen: route53resolver add handler-level parity tests for req…
Jun 20, 2026
d393b6a
parity-deepen: route53 parity tests for CreateHostedZone required fie…
Jun 20, 2026
f62b1a9
parity-deepen: iam CreateRole validates MaxSessionDuration bounds [36…
Jun 20, 2026
bf5c09a
WIP: checkpoint (auto)
Jun 20, 2026
a77545e
parity-deepen: rds CreateDBCluster validates and persists BackupReten…
Jun 20, 2026
8cb280b
parity-deepen: redshift CreateCluster validates ClusterIdentifier pat…
Jun 20, 2026
9b966f3
WIP: checkpoint (auto)
Jun 20, 2026
fb3329e
parity-deepen: firehose PutRecord/PutRecordBatch size and count limit…
Jun 20, 2026
dbcb98e
parity-deepen: forecast resource name format and length validation (g…
Jun 20, 2026
1507586
parity-deepen: fsx FileSystemType and StorageCapacity minimum validat…
Jun 20, 2026
0b51e55
ui: patch undici/fast-xml-parser advisories; drop vestigial pnpm-lock
claude Jun 20, 2026
6f955e9
ci: cancel superseded in-progress PR runs (concurrency group)
claude Jun 20, 2026
24481aa
ui: apply undici 7.28.0 + fast-xml-parser 5.7.3 overrides (lockfile)
claude Jun 20, 2026
03567e2
WIP: checkpoint (auto)
Jun 20, 2026
c385fd2
WIP: checkpoint (auto)
Jun 20, 2026
ad08afa
parity-deepen: codestarconnections deepening — validation, pagination…
Jun 20, 2026
8a031e6
parity-deepen: xray comprehensive deepening — pagination, validation,…
Jun 20, 2026
eae60c0
WIP: checkpoint (auto)
Jun 20, 2026
f070575
WIP: checkpoint (auto)
Jun 20, 2026
d4a9a7d
WIP: checkpoint (auto)
Jun 20, 2026
ebd938a
fix: resolve all golangci-lint issues in workspaces parity3 pass (go-…
Jun 20, 2026
4d4e57f
WIP: checkpoint (auto)
Jun 20, 2026
980d5c4
parity-deepen: workmail comprehensive deepening — validation, state l…
Jun 20, 2026
2561590
chore: cleanup actions storage artifacts and releases (#2336)
agbishop Jun 20, 2026
312f604
WIP: checkpoint (auto)
Jun 20, 2026
b25bb0c
WIP: checkpoint (auto)
Jun 20, 2026
814a243
WIP: checkpoint (auto)
Jun 20, 2026
d95a436
WIP: checkpoint (auto)
Jun 20, 2026
21e39cb
WIP: checkpoint (auto)
Jun 20, 2026
51e5c3b
WIP: checkpoint (auto)
Jun 20, 2026
261ceb6
WIP: checkpoint (auto)
Jun 20, 2026
6a123a6
parity: iotdataplane — behavioral fidelity (shadow semantics, validat…
Jun 20, 2026
e5432ae
WIP: checkpoint (auto)
Jun 20, 2026
73ba61c
WIP: checkpoint (auto)
Jun 20, 2026
5bbe622
fix(dax): resolve all test failures and lint violations
Jun 20, 2026
acbe009
feat(dax): add Duration support to DescribeEvents and fault error map…
Jun 20, 2026
e5adea3
WIP: checkpoint (auto)
Jun 20, 2026
6d02916
WIP: checkpoint (auto)
Jun 20, 2026
ca3c483
WIP: checkpoint (auto)
Jun 20, 2026
a51f10c
WIP: checkpoint (auto)
Jun 20, 2026
ebbe27d
WIP: checkpoint (auto)
Jun 20, 2026
698ca05
WIP: checkpoint (auto)
Jun 20, 2026
ddde75b
WIP: checkpoint (auto)
Jun 20, 2026
d59c871
WIP: checkpoint (auto)
Jun 20, 2026
f07c36c
WIP: checkpoint (auto)
Jun 20, 2026
157fecd
WIP: checkpoint (auto)
Jun 21, 2026
318bebf
feat(glacier): parity-deepen — fix RetrievalByteRange, InventorySizeI…
Jun 21, 2026
41acc90
WIP: checkpoint (auto)
Jun 21, 2026
1b91cb3
WIP: checkpoint (auto)
Jun 21, 2026
871051e
feat(resourcegroups): parity-deepen comprehensive test suite (go-acebc)
Jun 21, 2026
92cef56
feat(lambda): auto-pull base image + run /var/task/bootstrap for prov…
Jun 21, 2026
6c29b02
WIP: checkpoint (auto)
Jun 21, 2026
7775b3b
WIP: checkpoint (auto)
Jun 21, 2026
a6091f5
WIP: checkpoint (auto)
Jun 21, 2026
eaabca6
fix(appconfigdata): lint fixes for parity-deepen changes
Jun 21, 2026
921a56a
WIP: checkpoint (auto)
Jun 21, 2026
28f41aa
feat(resourcegroupstaggingapi): parity-deepen — RUNNING lifecycle, st…
Jun 21, 2026
8425564
Merge origin/main into parity-deepen; resolve ci.yml + stray appconfi…
claude Jun 21, 2026
4fef35f
feat(redshiftdata): parity-deepen — pagination, SQL LIKE patterns, st…
Jun 21, 2026
b3d11c0
Merge branch 'main' into parity-deepen
agbishop Jun 21, 2026
d96ec2d
fix(build): repair cross-service signature mismatches breaking CI build
Jun 21, 2026
1436538
chore(lint): clear golangci violations in codecommit/fis/mediatailor/s3
Jun 21, 2026
9307fbb
chore(lint): clear golangci violations in neptune/directoryservice/gl…
Jun 21, 2026
d48a215
WIP: checkpoint (auto)
Jun 21, 2026
96a1d5a
WIP: checkpoint (auto)
Jun 21, 2026
597c537
WIP: checkpoint (auto)
Jun 21, 2026
fb155bd
parity: backup service — filter/pagination/validation depth (go-awlxm)
Jun 21, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
The table of contents is too big for display.
Diff view
Diff view
  •  
  •  
  •  
7 changes: 7 additions & 0 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,13 @@ permissions:
security-events: write
packages: write

# Only the latest commit per PR (or ref) needs CI. A new push cancels any
# still-running CI for the same PR so rapid commit bursts don't pile up runs;
# CI settles on the last commit, which is all that matters.
concurrency:
group: ${{ github.workflow }}-${{ github.event.pull_request.number || github.event.workflow_run.head_branch || github.ref }}
cancel-in-progress: true

env:
GH_CI_TOKEN: ${{ secrets.GH_TOKEN != '' && secrets.GH_TOKEN || github.token }}

Expand Down
28 changes: 28 additions & 0 deletions cli.go
Original file line number Diff line number Diff line change
Expand Up @@ -2693,6 +2693,10 @@ func initializeServices(appCtx *service.AppContext) ([]service.Registerable, err
// Wire Firehose → S3 and Lambda for actual record delivery and transformation.
wireFirehoseDelivery(byName["Firehose"], byName["S3"], byName["Lambda"])

// Wire DynamoDB → S3 so ImportTable reads source objects and
// ExportTableToPointInTime writes real export data.
wireDynamoDBS3(byName["DynamoDB"], byName["S3"])

// Wire Lambda invoker → SecretsManager rotation.
wireSecretsManagerLambda(byName["SecretsManager"], byName["Lambda"])

Expand Down Expand Up @@ -5080,6 +5084,30 @@ func wireFirehoseDelivery(firehoseReg, s3Reg, lambdaReg service.Registerable) {
}
}

// wireDynamoDBS3 connects the DynamoDB backend to the S3 backend so that
// ImportTable can read source objects and ExportTableToPointInTime can write
// export data to S3.
func wireDynamoDBS3(ddbReg, s3Reg service.Registerable) {
ddbH, ok := ddbReg.(*ddbbackend.DynamoDBHandler)
if !ok {
return
}

s3H, s3Ok := s3Reg.(*s3backend.S3Handler)
if !s3Ok {
return
}

s3Bk, bkOk := s3H.Backend.(*s3backend.InMemoryBackend)
if !bkOk {
return
}

if ddbBk, ddbBkOk := ddbH.Backend.(*ddbbackend.InMemoryDB); ddbBkOk {
ddbBk.SetS3Backend(s3Bk)
}
}

// extractServiceName finds the service name for a given Echo context by checking
// which service's route matcher matches the request.
func extractServiceName(c *echo.Context, services []service.Registerable) string {
Expand Down
7 changes: 7 additions & 0 deletions pkgs/awserr/awserr.go
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,8 @@
// to match any service error against a shared sentinel.
package awserr

import "fmt"

// sentinelError is an unexported type used for constant sentinel errors.
// Using a distinct type prevents reassignment and enables reliable [errors.Is] matching.
type sentinelError string
Expand All @@ -28,6 +30,11 @@ func New(msg string, sentinel error) error {
return &wrappedError{msg: msg, cause: sentinel}
}

// Newf creates an error with a formatted message that wraps the given sentinel.
func Newf(msg string, sentinel error, args ...any) error {
return &wrappedError{msg: fmt.Sprintf(msg, args...), cause: sentinel}
}

type wrappedError struct {
cause error
msg string
Expand Down
2 changes: 2 additions & 0 deletions pkgs/container/container.go
Original file line number Diff line number Diff line change
Expand Up @@ -55,6 +55,8 @@ type Spec struct {
Mounts []string
// Cmd overrides the image's default CMD.
Cmd []string
// Entrypoint overrides the image's default ENTRYPOINT.
Entrypoint []string
}

// PooledContainer tracks a container managed by the warm pool.
Expand Down
13 changes: 13 additions & 0 deletions pkgs/container/docker_runtime.go
Original file line number Diff line number Diff line change
Expand Up @@ -200,10 +200,23 @@ func (r *DockerRuntime) CreateAndStart(ctx context.Context, spec Spec) (string,
cfg.Cmd = spec.Cmd
}

if len(spec.Entrypoint) > 0 {
cfg.Entrypoint = spec.Entrypoint
}

hostCfg := &dockercontainer.HostConfig{
Binds: spec.Mounts,
}

// Ensure the image is present before creating the container. Real AWS (and
// LocalStack) pull the runtime/base image on demand; without this a clean
// host fails container creation with "No such image".
if has, herr := r.HasImage(ctx, spec.Image); herr == nil && !has {
if perr := r.PullImage(ctx, spec.Image); perr != nil {
return "", fmt.Errorf("ensure image %q: %w", spec.Image, perr)
}
}

resp, err := r.docker.ContainerCreate(ctx, cfg, hostCfg, nil, nil, spec.Name)
if err != nil {
return "", fmt.Errorf("container create %q: %w", spec.Image, err)
Expand Down
1 change: 1 addition & 0 deletions pkgs/service/service.go
Original file line number Diff line number Diff line change
Expand Up @@ -149,6 +149,7 @@ type FISActionDefinition struct {
ActionID string // e.g., "aws:ec2:stop-instances"
Description string
TargetType string // e.g., "aws:ec2:instance"; empty if action has no targets
TargetKey string // key name used in the Targets map (e.g., "Instances", "Roles"); defaults to "Targets"
Parameters []FISParamDef
}

Expand Down
10 changes: 9 additions & 1 deletion services/apigateway/backend.go
Original file line number Diff line number Diff line change
Expand Up @@ -224,6 +224,10 @@ type StorageBackend interface {

// OpenAPI export.
GetExport(restAPIID, stageName, exportType string) (map[string]any, error)

// OpenAPI import.
ImportRestAPI(input ImportRestAPIInput) (*RestAPI, error)
PutRestAPI(input PutRestAPIInput) (*RestAPI, error)
}

const apiIDChars = "abcdefghijklmnopqrstuvwxyz0123456789"
Expand Down Expand Up @@ -265,7 +269,11 @@ const (
exportKeyBody = "body"
)

const paramLocationHeader = "header"
const (
paramLocationHeader = "header"
paramLocationPath = "path"
paramLocationQuery = "querystring"
)

// stageInvokeURL returns the gopherstack proxy path for a deployed stage.
// The full URL is relative — clients prepend their gopherstack base URL.
Expand Down
8 changes: 8 additions & 0 deletions services/apigateway/extra_coverage_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -492,6 +492,14 @@ func (n *noopBackend) GetExport(_ string, _ string, _ string) (map[string]any, e
return nil, errNoopNotImplemented
}

func (n *noopBackend) ImportRestAPI(_ apigateway.ImportRestAPIInput) (*apigateway.RestAPI, error) {
return nil, errNoopNotImplemented
}

func (n *noopBackend) PutRestAPI(_ apigateway.PutRestAPIInput) (*apigateway.RestAPI, error) {
return nil, errNoopNotImplemented
}

// restRequest sends a REST-style request (no X-Amz-Target header) to the handler.
func restRequest(t *testing.T, handler *apigateway.Handler, method, path, body string) *httptest.ResponseRecorder {
t.Helper()
Expand Down
64 changes: 63 additions & 1 deletion services/apigateway/handler.go
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import (
"fmt"
"maps"
"net/http"
"net/url"
"strings"
"sync"
"time"
Expand Down Expand Up @@ -962,6 +963,17 @@ func (h *Handler) handleRESTAPI(c *echo.Context) error {
return c.String(http.StatusInternalServerError, "internal server error")
}

// OpenAPI import (ImportRestApi / PutRestApi) carries the raw spec document
// as the HTTP body. These are detected here because they share REST paths
// with CreateRestApi (POST /restapis) and UpdateRestApi (PUT /restapis/{id})
// but are distinguished by the request method/query, and the body must be
// passed through verbatim rather than treated as a flat field object.
if importAction, importBody, isImport := detectImportRESTAPI(
c.Request().Method, action, pathParams, c.Request().URL.Query(), body,
); isImport {
return h.dispatchAndRespond(ctx, c, importAction, importBody, contentTypeJSON)
}

// GET requests have no body; normalise to an empty JSON object so that
// json.Unmarshal calls in the action handlers don't fail with
// "unexpected end of JSON input".
Expand All @@ -986,19 +998,65 @@ func (h *Handler) handleRESTAPI(c *echo.Context) error {
}
}

return h.dispatchAndRespond(ctx, c, action, body, contentTypeJSON)
}

// dispatchAndRespond runs an action through the dispatch table and writes the
// HTTP response, including correct handling of 204 No Content responses.
func (h *Handler) dispatchAndRespond(
ctx context.Context, c *echo.Context, action string, body []byte, contentType string,
) error {
statusCode, response, reqErr := h.dispatch(ctx, action, body)
if reqErr != nil {
return h.handleError(ctx, c, action, reqErr)
}

c.Response().Header().Set("Content-Type", contentTypeJSON)
c.Response().Header().Set("Content-Type", contentType)
if statusCode == http.StatusNoContent {
return c.NoContent(http.StatusNoContent)
}

return c.JSONBlob(statusCode, response)
}

// detectImportRESTAPI recognises ImportRestApi (POST /restapis?mode=import) and
// PutRestApi (PUT /restapis/{id}) requests, returning the resolved action and a
// JSON-encoded typed input whose Body field carries the raw spec document. The
// AWS SDK sends the OpenAPI/Swagger document as the verbatim HTTP body, so it
// must not be merged with path/query parameters like other operations.
func detectImportRESTAPI(
method, action string, pathParams map[string]string, query url.Values, body []byte,
) (string, []byte, bool) {
switch {
case action == opCreateRestAPI && method == http.MethodPost && query.Get("mode") == "import":
in := ImportRestAPIInput{
Body: body,
FailOnWarnings: query.Get("failonwarnings") == litTrue,
}
encoded, err := json.Marshal(in)
if err != nil {
return "", nil, false
}

return opImportRestAPI, encoded, true
case action == opPutRestAPI && method == http.MethodPut && pathParams[keyRestAPIID] != "":
in := PutRestAPIInput{
RestAPIID: pathParams[keyRestAPIID],
Mode: query.Get("mode"),
FailOnWarnings: query.Get("failonwarnings") == litTrue,
Body: body,
}
encoded, err := json.Marshal(in)
if err != nil {
return "", nil, false
}

return opPutRestAPI, encoded, true
}

return "", nil, false
}

// normalizePatchBody converts a JSON patch array (RFC 6902) to a flat JSON object.
// AWS API Gateway REST PATCH endpoints accept patch operations like
// [{"op":"replace","path":"/description","value":"foo"}].
Expand Down Expand Up @@ -1353,6 +1411,10 @@ func parseAPIGWRestAPIsDepth2(method, apiID string) (string, map[string]string,
return opDeleteRestAPI, params, true
case http.MethodPatch:
return opUpdateRestAPI, params, true
case http.MethodPut:
// PUT /restapis/{id} is PutRestApi (OpenAPI import into an existing
// API). The body is the raw spec; detectImportRESTAPI handles it.
return opPutRestAPI, params, true
}

return apiGWUnknownOp, nil, false
Expand Down
32 changes: 24 additions & 8 deletions services/apigateway/handler_stubs.go
Original file line number Diff line number Diff line change
Expand Up @@ -13,10 +13,6 @@ import (
const (
// vpcLinkStatusAvailable is the status for an available VPC Link.
vpcLinkStatusAvailable = "AVAILABLE"
// stubImportedAPIName is the placeholder name for imported REST APIs.
stubImportedAPIName = "imported-api"
// stubImportedAPIID is the placeholder ID for imported REST APIs.
stubImportedAPIID = "stub0000"
// keyAPIName is the JSON key for API name in stub responses.
keyAPIName = "name"
)
Expand Down Expand Up @@ -230,11 +226,31 @@ func (h *Handler) stubActions() map[string]actionFn {
actions[opImportDocumentationParts] = func(_ []byte) (int, any, error) {
return http.StatusOK, &documentationPartsImportStub{IDs: []string{}, Warnings: []string{}}, nil
}
actions[opImportRestAPI] = func(_ []byte) (int, any, error) {
return http.StatusCreated, map[string]any{"id": stubImportedAPIID, keyAPIName: stubImportedAPIName}, nil
actions[opImportRestAPI] = func(b []byte) (int, any, error) {
var input ImportRestAPIInput
if err := json.Unmarshal(b, &input); err != nil {
return 0, nil, err
}

api, err := h.Backend.ImportRestAPI(input)
if err != nil {
return 0, nil, err
}

return http.StatusCreated, api, nil
}
actions[opPutRestAPI] = func(_ []byte) (int, any, error) {
return http.StatusOK, map[string]any{"id": stubImportedAPIID, keyAPIName: stubImportedAPIName}, nil
actions[opPutRestAPI] = func(b []byte) (int, any, error) {
var input PutRestAPIInput
if err := json.Unmarshal(b, &input); err != nil {
return 0, nil, err
}

api, err := h.Backend.PutRestAPI(input)
if err != nil {
return 0, nil, err
}

return http.StatusOK, api, nil
}

// Usage update
Expand Down
Loading
Loading