Skip to content

fix(aws-keyspaces): GetTable ttl/clientSideTimestamps omission and rpc-v2-cbor route - #1680

Merged
Satyam-Trivedi-ZS merged 3 commits into
stackshy:developmentfrom
Satyam-Trivedi-ZS:fix/aws-keyspaces-followups-1666
Oct 23, 2026
Merged

Satyam-Trivedi-ZS merged 3 commits into
stackshy:developmentfrom
Satyam-Trivedi-ZS:fix/aws-keyspaces-followups-1666

Conversation

@Satyam-Trivedi-ZS

@Satyam-Trivedi-ZS Satyam-Trivedi-ZS commented Oct 22, 2026 •

Copy link
Copy Markdown
Collaborator

Builds on #1645 (merged); rebased onto development, so the diff is now only this PR's changes.

Part of #1666

Summary

  • GetTable defaults (M): FIXED. ttl and clientSideTimestamps are now returned only once they are enabled, so a Terraform aws_keyspaces_table that sets neither no longer plans a replacement on every run.
  • rpc-v2-cbor (M): FIXED. cloudemu serve now 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.
  • 50 tags per resource (L): UNVERIFIABLE, left unenforced. No doc or SDK source names the error code for this quota.

Changes

1. GetTable ttl / clientSideTimestamps (FIXED)

What real Keyspaces does:

  • In the model, TimeToLiveStatus and ClientSideTimestampsStatus have a single enum value, ENABLED (botocore keyspaces/2022-02-10/service-2.json; aws-sdk-go-v2 types/enums.go). DISABLED is 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.
  • The CreateTable and UpdateTable docs say both settings are disabled by default and can't be turned off once enabled.
  • In hashicorp/aws 6.66.0 (internal/service/keyspaces/table.go), both blocks are Optional and not Computed. Read stores them only when table.Ttl / table.ClientSideTimestamps are non-nil. ForceNewIfChange("client_side_timestamps") forces a replacement when state has the block and config doesn't. That is why an always-present DISABLED caused a replacement on every plan.

Fix:

  • providers/aws/keyspaces: the table records "" for a setting that was never set. An explicit DISABLED, the documented default, also maps to "" (enabledOnly). On UpdateTable, DISABLED no longer switches an enabled setting off, matching "can't disable it".
  • Persistence: these are existing Table fields, so the snapshot keeps whether each setting was set. On restore, normalizeRestoredTablesLocked clears the DISABLED that older builds stored on every table. It runs under the mock lock and uses memstore Update.
  • server/aws/keyspaces: GetTable emits ttl / clientSideTimestamps only when they are set. pointInTimeRecovery (whose enum has DISABLED) and cdcSpecification are unchanged.

2. rpc-v2-cbor (FIXED)

What the SDK does:

  • The aws-sdk-go-v2/service/keyspaces v1.37.0 CHANGELOG says: "This release adds Smithy RPC v2 CBOR as an additional protocol alongside the existing AWS JSON 1.0. The SDK will prioritize its most performant protocol." In 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.
  • smithy-go v1.28.5 (transport/http/protocol/internal/cbor): ReadTime requires CBOR tag 1, and ReadInt32 / ReadInt64 accept only CBOR integers.
  • Root cause in cloudemu: CloudWatch's Matches claims every Smithy-Protocol: rpc-v2-cbor request under /service/, so Keyspaces CBOR calls got 400 UnknownOperationException from CloudWatch. Under --enforce-auth they were checked as cloudwatch:<Op>.

Fix (wire layer only, no business logic):

  • New keyspaces.CBORHandler (server/aws/keyspaces/cbor.go). It matches only Smithy-Protocol: rpc-v2-cbor on /service/KeyspacesService/operation/* and is registered before CloudWatch.
  • It re-encodes the CBOR request as AWS JSON 1.0 and runs it through the existing JSON Handler, so both protocols share one dispatch. A tag-1 restoreTimestamp becomes epoch seconds.
  • It re-encodes the response as CBOR. Integral numbers become CBOR integers. The model's four Timestamp members (creationTimestamp, lastModifiedTimestamp, earliestRestorableTimestamp, lastUpdateToPayPerRequestTimestamp, checked against the v1.37.2 schemas.go) become tag 1.
  • Errors keep the JSON status and code, sent as a CBOR {__type, message} map with the Smithy-Protocol response header.
  • IAM: CBORHandler is an awsauthz.Resolver that names cassandra:<Operation> from the path ServeHTTP dispatches on, with the resource unknown as on the JSON path. It is also a DenyWriter, so a deny is a CBOR AccessDeniedException.
  • docs/sdk-server.md: dispatch table updated.

3. 50 tags per resource (UNVERIFIABLE, not enforced)

Provider Coverage

  • AWS
  • Azure: N/A (Amazon Keyspaces only)
  • GCP: N/A
  • OCI: N/A

Checklist

  • All tests pass (go test -p 2 over all 657 packages, in 22 chunks of 30)
  • Linter: golangci-lint run --new-from-rev=origin/fix/aws-keyspaces-parity-1512 on the touched packages reports no new findings in touched packages
  • Every provider the change applies to implements the same behavior (AWS-only service)
  • Integration tests added to cloudemu_test.go: covered by the wire tests and the full-server dispatch and authz tests below
  • Unit tests added to provider test files
  • Regenerated docs: no driver interface or factory change, so docs/coverage is unchanged. docs/sdk-server.md is 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.

  • Provider tests: TestTTLAndClientSideTimestampsTrackSet and TestSnapshotTracksTTLAndClientSideTimestamps (current snapshots and older ones carrying DISABLED). Before the fix both failed with ttl="DISABLED" cst="DISABLED", want "".
  • Wire (aws-sdk-go-v2 JSON): TestSDKGetTableOmitsUnsetTTLAndClientSideTimestamps. Before the fix: ttl=&{Status:DISABLED}.
  • Wire (raw CBOR):
    • TestCBORTableRoundTrip: tag-1 creationTimestamp, integer capacity units, tag-1 restoreTimestamp in.
    • TestCBORErrors: ResourceNotFound, Validation, future restore, unknown operation.
    • TestCBORRejectsMalformedBody (SerializationException) and TestCBORMatches (does not claim CloudWatch's path).
  • Full server:
    • TestKeyspacesCBORDispatch. Before the fix: 400 UnknownOperationException from CloudWatch.
    • TestKeyspacesCBORAuthorization (--enforce-auth: allowed op 200; denied op 403 CBOR AccessDeniedException with no side effect). Before the fix it was checked as cloudwatch:ListKeyspaces.
  • go test -race ./providers/aws/keyspaces/... ./server/aws/keyspaces/... ./persist/...: ok. persist (completeness) passes.
  • go build ./... and go vet on the touched packages: clean.

Real-user checks against cloudemu serve -providers aws -aws-port 4602

AWS CLI 1.44.87 (botocore 1.42.97, AWS JSON 1.0), with --endpoint-url http://127.0.0.1:4602:

aws keyspaces create-keyspace --keyspace-name app                                    -> resourceArn
aws keyspaces create-table --keyspace-name app --table-name plain --schema-definition 'allColumns=[{name=id,type=int}],partitionKeys=[{name=id}]'
aws keyspaces create-table ... --table-name on ... --ttl status=ENABLED --client-side-timestamps status=ENABLED
aws keyspaces get-table --keyspace-name app --table-name plain                       -> no ttl / clientSideTimestamps keys; pointInTimeRecovery DISABLED, cdcSpecification DISABLED
aws keyspaces get-table --keyspace-name app --table-name on                          -> ttl {status: ENABLED}, clientSideTimestamps {status: ENABLED}
aws keyspaces update-table --keyspace-name app --table-name on --ttl status=DISABLED -> ok; get-table still ttl ENABLED
aws keyspaces update-table --keyspace-name app --table-name plain --client-side-timestamps status=ENABLED
                                                                                     -> clientSideTimestamps ENABLED, ttl still absent
aws keyspaces get-table --keyspace-name app --table-name ghost                       -> ResourceNotFoundException (exit 255)

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 with application/cbor:

  • Calls that succeeded: CreateKeyspace (with tags), CreateTable (PROVISIONED 5/7, PITR), GetTable, RestoreTable, ListTables, ListTagsForResource, CreateType, GetType and DeleteTable.
  • GetTable decoded creationTimestamp, rcu=5 wcu=7, ttl=<nil> cst=<nil>.
  • Errors came back as typed exceptions: ResourceNotFoundException, ConflictException for a duplicate keyspace, and ValidationException for a bad name.

Terraform isn't installed in this environment, so I didn't run the apply / plan / destroy cycle. 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

  • CloudWatch's rpc-v2-cbor matcher is still broad: it claims any other /service/* CBOR path. Keyspaces registers ahead of it rather than narrowing CloudWatch, to keep this PR from changing CloudWatch dispatch.
  • As with CloudWatch CBOR, CloudTrail management-event recording names events from X-Amz-Target or Action, so Keyspaces CBOR calls aren't recorded under an event name.
  • UpdateTable with ttl/clientSideTimestamps DISABLED on 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.lastModifiedTimestamp is still omitted, as before (pre-existing).

@NitinKumar004 NitinKumar004 left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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, TimeToLiveStatus and ClientSideTimestampsStatus both have the single value ENABLED, while PointInTimeRecoveryStatus has ENABLED and DISABLED. That supports omitting ttl and clientSideTimestamps from 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, so docs/coverage doesn't change. docs/sdk-server.md is updated.
  • Gates on 3d7697d: go build ./... ok. go vet ok on providers/aws/keyspaces, server/aws/keyspaces and server/aws. go test ok on those three plus persist and compat/aws. -race ok on both keyspaces packages. gofmt is clean on the touched files. go mod tidy and go 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 ttl nor client_side_timestamps plans - client_side_timestamps { # forces replacement right after apply (2 to add, 2 to destroy). With 6.70.0, CreateKeyspace fails with 400 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, uses POST /service/KeyspacesService/operation/* with rpc-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 added ttl { 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 (targetValue 70.5 came back as 70.5 from GetTableAutoScalingSettings), GetTable (creationTimestamp decoded, 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, an aws: tag key, a future restoreTimestamp and deleting system, and ResourceNotFoundException for a missing table. I also read smithy-go's CBOR decoder: readFloat64 accepts CBOR integers, so writing integral doubles as integers is safe, and ReadTime requires 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 and Foo/Bar both return ValidationException. A missing keyspace returns ResourceNotFoundException. Every response carries Content-Type: application/cbor and Smithy-Protocol: rpc-v2-cbor. Without the Smithy-Protocol header the request isn't claimed.
  • AWS CLI (JSON): CreateTable with --ttl status=DISABLED --client-side-timestamps status=DISABLED succeeds and GetTable has neither member. UpdateTable client-side-timestamps ENABLED and then DISABLED leaves it ENABLED. 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 still DISABLED.
  • Commits and the PR body have no attribution lines.

High

None.

Medium

None.

Low

  • cborValueToJSON turns a tag-1 time into seconds with t.UnixNano() (server/aws/keyspaces/cbor.go:319). That overflows after the year 2262, so over CBOR a RestoreTable with restoreTimestamp of 2300-01-01 or 3000-01-01 is accepted and the table is restored. The same values over JSON (AWS CLI) get ValidationException: restoreTimestamp must not be in the future. float64(t.Unix()) + float64(t.Nanosecond())/1e9 avoids 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 DISABLED for ttl and clientSideTimestamps and treat it as "not set". Both enums hold only ENABLED, so real Keyspaces most likely rejects DISABLED with 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 use UnknownOperationException for 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 a cborErrorType const would keep the touched packages at the baseline count.


return t
case time.Time:
return float64(t.UnixNano()) / float64(time.Second)

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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),

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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})

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.
@Satyam-Trivedi-ZS
Satyam-Trivedi-ZS force-pushed the fix/aws-keyspaces-followups-1666 branch from 3d7697d to 7391738 Compare October 23, 2026 20:45
@Satyam-Trivedi-ZS
Satyam-Trivedi-ZS marked this pull request as ready for review October 23, 2026 20:45
@Satyam-Trivedi-ZS
Satyam-Trivedi-ZS merged commit 07436a7 into stackshy:development Oct 23, 2026
23 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants