Repository navigation
fix(aws-keyspaces): GetTable ttl/clientSideTimestamps omission and rpc-v2-cbor route - #1680
Conversation
NitinKumar004
left a comment
There was a problem hiding this comment.
What I checked
- Issue #1666: both Medium items are fixed. The Low item (50 tags per resource) is left out, and the PR body says why: the docs name no error code for it. That is reasonable.
- Model check: in the Keyspaces service model,
TimeToLiveStatusandClientSideTimestampsStatusboth have the single valueENABLED, whilePointInTimeRecoveryStatushasENABLEDandDISABLED. That supports omittingttlandclientSideTimestampsfrom GetTable until they are enabled, and leaving PITR and CDC alone. - Architecture: the "never set" state and the "cannot be turned off" rule are in the provider (
enabledOnly,orKeep). The handler only drops the nil members. The CBOR handler only translates: it re-encodes to AWS JSON 1.0, runs the existing JSON handler and re-encodes the answer, so both protocols share one dispatch. No driver or factory change, sodocs/coveragedoesn't change.docs/sdk-server.mdis updated. - Gates on 3d7697d:
go build ./...ok.go vetok onproviders/aws/keyspaces,server/aws/keyspacesandserver/aws.go testok on those three pluspersistandcompat/aws.-raceok on both keyspaces packages. gofmt is clean on the touched files.go mod tidyandgo generate ./...leave no diff. golangci-lint on the three packages, run without--new-from-rev, gives 9 issues on the head and 8 on 0be1e71. The only new one is a goconst on"__type"in cbor.go (see Low). CI is green. - Base reproduction (0be1e71 built and served): with hashicorp/aws 6.66.0, a table that sets neither
ttlnorclient_side_timestampsplans- client_side_timestamps { # forces replacementright after apply (2 to add, 2 to destroy). With 6.70.0, CreateKeyspace fails with400 UnknownOperationException: unknown operation: CreateKeyspace, which comes from the CloudWatch CBOR handler. - Terraform on the head, each version with its own private provider download. 6.66.0 uses JSON (
X-Amz-Target: KeyspacesService.*). 6.70.0, the latest, usesPOST /service/KeyspacesService/operation/*withrpc-v2-cbor, as the TF_LOG output shows. For each version the config had a keyspace and a PAY_PER_REQUEST table (tagged, no ttl or client_side_timestamps), plus a PROVISIONED table with PITR. Each version went through: apply, then plan exit 0. Then I changed the RCU and tags, applied (3 changed in place), and plan exited 0. Then I addedttl { status = "ENABLED" }, applied (1 changed in place), and plan exited 0. Destroy then removed all 3 resources. - aws-sdk-go-v2/service/keyspaces v1.37.3 with smithy-go v1.28.6, which uses CBOR, against
serve. These worked: CreateKeyspace with tags, CreateTable PROVISIONED with PITR and an auto scaling policy (targetValue70.5 came back as 70.5 from GetTableAutoScalingSettings), GetTable (creationTimestampdecoded, rcu 5, ttl and cst nil), UpdateTable ttl ENABLED with defaultTimeToLive 3600, RestoreTable, and ListTables paging with maxResults 1 over two pages. So did CreateType, GetType, DeleteType, UntagResource, UpdateKeyspace, DeleteTable, DeleteKeyspace and ListKeyspaces. These errors came back typed: ConflictException for a duplicate keyspace, ValidationException for a bad name, anaws:tag key, a future restoreTimestamp and deletingsystem, and ResourceNotFoundException for a missing table. I also read smithy-go's CBOR decoder:readFloat64accepts CBOR integers, so writing integral doubles as integers is safe, andReadTimerequires tag 1, which the handler emits. - Raw CBOR probes: an empty body and
{}both work. An array body and a truncated body both return SerializationException. An empty operation andFoo/Barboth return ValidationException. A missing keyspace returns ResourceNotFoundException. Every response carriesContent-Type: application/cborandSmithy-Protocol: rpc-v2-cbor. Without theSmithy-Protocolheader the request isn't claimed. - AWS CLI (JSON): CreateTable with
--ttl status=DISABLED --client-side-timestamps status=DISABLEDsucceeds and GetTable has neither member. UpdateTableclient-side-timestamps ENABLEDand thenDISABLEDleaves itENABLED. A missing table gives ResourceNotFoundException. - Snapshot upgrade: I took a snapshot from the base server, where every table stored
DISABLED, and posted it to the head server. GetTable on the restored plain table shows no ttl or cst, and PITR is stillDISABLED. - Commits and the PR body have no attribution lines.
High
None.
Medium
None.
Low
cborValueToJSONturns a tag-1 time into seconds witht.UnixNano()(server/aws/keyspaces/cbor.go:319). That overflows after the year 2262, so over CBOR a RestoreTable withrestoreTimestampof 2300-01-01 or 3000-01-01 is accepted and the table is restored. The same values over JSON (AWS CLI) getValidationException: restoreTimestamp must not be in the future.float64(t.Unix()) + float64(t.Nanosecond())/1e9avoids the overflow. It's an edge case, but it is a difference between the two protocols that the handler is meant to keep identical.- CreateTable and UpdateTable accept
DISABLEDforttlandclientSideTimestampsand treat it as "not set". Both enums hold onlyENABLED, so real Keyspaces most likely rejectsDISABLEDwith a ValidationException. The PR body notes this for UpdateTable. Accepting it is the safer choice until someone confirms the error against real AWS, but please add it to #1666 so it isn't lost. - An unknown operation over CBOR returns
400 ValidationException(it comes from the JSON handler's default case). The CloudWatch CBOR handler and real Smithy services useUnknownOperationExceptionfor this. The JSON path has the same behaviour on the base, so a follow-up is enough. - New goconst finding:
"__type"appears twice in cbor.go (lines 205 and 212). Lint is advisory in CI, but acborErrorTypeconst would keep the touched packages at the baseline count.
|
|
||
| return t | ||
| case time.Time: | ||
| return float64(t.UnixNano()) / float64(time.Second) |
There was a problem hiding this comment.
UnixNano() overflows for times after 2262. Over CBOR, a RestoreTable with restoreTimestamp 2300-01-01 or 3000-01-01 succeeds and the table is restored. Over JSON the same values get ValidationException: restoreTimestamp must not be in the future. float64(t.Unix()) + float64(t.Nanosecond())/1e9 keeps the two paths the same.
| EncryptionSpecification: resolveEncryption(cfg.EncryptionSpecification), | ||
| PointInTimeRecoveryStatus: disabledIfEmpty(cfg.PointInTimeRecovery), | ||
| TTLStatus: disabledIfEmpty(cfg.TTLStatus), | ||
| TTLStatus: enabledOnly(cfg.TTLStatus), |
There was a problem hiding this comment.
Minor: both enums hold only ENABLED, so real CreateTable/UpdateTable probably reject DISABLED with a ValidationException rather than treating it as unset. Accepting it seems fine until that is confirmed against real AWS. Could you add it to #1666 so it isn't lost?
| } | ||
|
|
||
| func writeCBORError(w http.ResponseWriter, status int, errType, msg string) { | ||
| writeCBOR(w, status, map[string]any{"__type": errType, "message": msg}) |
There was a problem hiding this comment.
Nit: golangci-lint flags "__type" here (goconst, 2 occurrences with line 212). A small const keeps the package at its baseline lint count.
…bled
GetTable always returned ttl and clientSideTimestamps as {status: DISABLED},
even for a table that never set them. Both model enums (TimeToLiveStatus,
ClientSideTimestampsStatus) hold only ENABLED, so real Keyspaces never reports
DISABLED and omits the members on such a table. hashicorp/aws reads a present
block into state, so a table that sets neither planned a forced replacement on
every run.
The provider now records "" for a setting that was never enabled (DISABLED,
the documented default, maps to "" too) and GetTable emits the members only
when set. An UpdateTable DISABLED no longer turns an enabled setting off,
matching the docs ("can't disable it"). Restore clears the DISABLED that older
builds stored on every table.
aws-sdk-go-v2/service/keyspaces v1.37.0 added Smithy RPC v2 CBOR beside AWS JSON 1.0 and prefers it, so hashicorp/aws 6.69+ sends every Keyspaces call to POST /service/KeyspacesService/operation/<Operation> with Smithy-Protocol: rpc-v2-cbor. CloudWatch claimed every rpc-v2-cbor request under /service/ and answered UnknownOperationException, so that provider could not even create a keyspace. keyspaces.CBORHandler matches only the KeyspacesService path and registers before CloudWatch. It only converts the wire format: the CBOR body is re-encoded as JSON and run through the existing JSON handler, and the answer is re-encoded as CBOR, with integral numbers as CBOR integers and the four model Timestamp members as tag 1, which the SDK requires. Errors keep their status and code as a CBOR __type/message map. Under --enforce-auth the handler is a Resolver naming cassandra:<Operation> from the path, and a deny is a CBOR AccessDeniedException.
3d7697d to
7391738
Compare
Builds on #1645 (merged); rebased onto
development, so the diff is now only this PR's changes.Part of #1666
Summary
ttlandclientSideTimestampsare now returned only once they are enabled, so a Terraformaws_keyspaces_tablethat sets neither no longer plans a replacement on every run.cloudemu servenow routes Smithy RPC v2 CBOR on/service/KeyspacesService/operation/*, the protocol aws-sdk-go-v2 keyspaces v1.37.0+ (and so hashicorp/aws 6.69.x) uses.Changes
1. GetTable
ttl/clientSideTimestamps(FIXED)What real Keyspaces does:
TimeToLiveStatusandClientSideTimestampsStatushave a single enum value,ENABLED(botocorekeyspaces/2022-02-10/service-2.json; aws-sdk-go-v2types/enums.go).DISABLEDis not a valid value, so real GetTable can never return{status: DISABLED}. For a table that never enabled these settings it leaves the members out.internal/service/keyspaces/table.go), both blocks areOptionaland notComputed. Read stores them only whentable.Ttl/table.ClientSideTimestampsare non-nil.ForceNewIfChange("client_side_timestamps")forces a replacement when state has the block and config doesn't. That is why an always-presentDISABLEDcaused a replacement on every plan.Fix:
providers/aws/keyspaces: the table records""for a setting that was never set. An explicitDISABLED, the documented default, also maps to""(enabledOnly). On UpdateTable,DISABLEDno longer switches an enabled setting off, matching "can't disable it".Tablefields, so the snapshot keeps whether each setting was set. On restore,normalizeRestoredTablesLockedclears theDISABLEDthat older builds stored on every table. It runs under the mock lock and uses memstoreUpdate.server/aws/keyspaces:GetTableemitsttl/clientSideTimestampsonly when they are set.pointInTimeRecovery(whose enum hasDISABLED) andcdcSpecificationare unchanged.2. rpc-v2-cbor (FIXED)
What the SDK does:
api_client.go,options.Protocol = rpcv2.NewCBOR(schemas.KeyspacesService).request_snapshot/GetTable.request.snap:POST /service/KeyspacesService/operation/GetTable,Smithy-Protocol: rpc-v2-cbor,Content-Type: application/cbor.transport/http/protocol/internal/cbor):ReadTimerequires CBOR tag 1, andReadInt32/ReadInt64accept only CBOR integers.Matchesclaims everySmithy-Protocol: rpc-v2-cborrequest under/service/, so Keyspaces CBOR calls got400 UnknownOperationExceptionfrom CloudWatch. Under--enforce-auththey were checked ascloudwatch:<Op>.Fix (wire layer only, no business logic):
keyspaces.CBORHandler(server/aws/keyspaces/cbor.go). It matches onlySmithy-Protocol: rpc-v2-cboron/service/KeyspacesService/operation/*and is registered before CloudWatch.Handler, so both protocols share one dispatch. A tag-1restoreTimestampbecomes epoch seconds.creationTimestamp,lastModifiedTimestamp,earliestRestorableTimestamp,lastUpdateToPayPerRequestTimestamp, checked against the v1.37.2schemas.go) become tag 1.{__type, message}map with theSmithy-Protocolresponse header.CBORHandleris anawsauthz.Resolverthat namescassandra:<Operation>from the path ServeHTTP dispatches on, with the resource unknown as on the JSON path. It is also aDenyWriter, so a deny is a CBORAccessDeniedException.docs/sdk-server.md: dispatch table updated.3. 50 tags per resource (UNVERIFIABLE, not enforced)
ServiceQuotaExceededExceptionandValidationException, and it doesn't say which one this quota returns. The botocore model and the SDK don't say either.TagListbound from fix(aws-keyspaces): tag existence and restrictions, name validation, missing-keyspace error, GetTable omissions #1645 is still enforced.Provider Coverage
Checklist
go test -p 2over all 657 packages, in 22 chunks of 30)golangci-lint run --new-from-rev=origin/fix/aws-keyspaces-parity-1512on the touched packages reports no new findings in touched packagescloudemu_test.go: covered by the wire tests and the full-server dispatch and authz tests belowdocs/coverageis unchanged.docs/sdk-server.mdis updated by hand.Test Plan
Each new test fails before its fix and passes after it. I checked this by reverting only the non-test source.
TestTTLAndClientSideTimestampsTrackSetandTestSnapshotTracksTTLAndClientSideTimestamps(current snapshots and older ones carryingDISABLED). Before the fix both failed withttl="DISABLED" cst="DISABLED", want "".TestSDKGetTableOmitsUnsetTTLAndClientSideTimestamps. Before the fix:ttl=&{Status:DISABLED}.TestCBORTableRoundTrip: tag-1creationTimestamp, integer capacity units, tag-1restoreTimestampin.TestCBORErrors: ResourceNotFound, Validation, future restore, unknown operation.TestCBORRejectsMalformedBody(SerializationException) andTestCBORMatches(does not claim CloudWatch's path).TestKeyspacesCBORDispatch. Before the fix:400 UnknownOperationExceptionfrom CloudWatch.TestKeyspacesCBORAuthorization(--enforce-auth: allowed op 200; denied op 403 CBORAccessDeniedExceptionwith no side effect). Before the fix it was checked ascloudwatch:ListKeyspaces.go test -race ./providers/aws/keyspaces/... ./server/aws/keyspaces/... ./persist/...: ok.persist(completeness) passes.go build ./...andgo veton the touched packages: clean.Real-user checks against
cloudemu serve -providers aws -aws-port 4602AWS CLI 1.44.87 (botocore 1.42.97, AWS JSON 1.0), with
--endpoint-url http://127.0.0.1:4602:aws-sdk-go-v2/service/keyspaces v1.37.2 (smithy-go v1.28.5, which sends rpc-v2-cbor) in a separate module against the same server. The wire log shows every call as
POST /service/KeyspacesService/operation/<Op> Smithy-Protocol="rpc-v2-cbor", answered withapplication/cbor:creationTimestamp,rcu=5 wcu=7,ttl=<nil> cst=<nil>.ResourceNotFoundException,ConflictExceptionfor a duplicate keyspace, andValidationExceptionfor a bad name.Terraform isn't installed in this environment, so I didn't run the
apply/plan/destroycycle. The ttl/cst fix follows from the provider source quoted above, and the CBOR path was exercised with the same SDK version hashicorp/aws 6.69.x builds on.Known limits
/service/*CBOR path. Keyspaces registers ahead of it rather than narrowing CloudWatch, to keep this PR from changing CloudWatch dispatch.X-Amz-TargetorAction, so Keyspaces CBOR calls aren't recorded under an event name.ttl/clientSideTimestampsDISABLEDon an enabled table is now a no-op. Real AWS likely rejects it, but its error code isn't documented, so I didn't add a rejection.GetType.lastModifiedTimestampis still omitted, as before (pre-existing).