Skip to content

fix(cloudwatch): awsJson1_0 protocol and request validation parity (CW-X12, CW-X13) - #1371

Merged
NitinKumar004 merged 5 commits into
developmentfrom
fix/cloudwatch-json-protocol
Sep 27, 2026
Merged

NitinKumar004 merged 5 commits into
developmentfrom
fix/cloudwatch-json-protocol

Conversation

@NitinKumar004

@NitinKumar004 NitinKumar004 commented Sep 27, 2026 •

Copy link
Copy Markdown
Collaborator

Summary

Closes tracker rows CW-X12 and CW-X13.

CW-X12: botocore 1.43+ (boto3, awscli v1) talks to CloudWatch over awsJson1_0: a POST with X-Amz-Target: GraniteServiceVersion20100801.<Op> and Content-Type: application/x-amz-json-1.0. The CloudWatch handler only served query and rpc-v2-cbor, so every such call got a 501 "no handler registered". boto3.client("cloudwatch", endpoint_url=...).describe_alarms() failed on the first call.

CW-X13: a set of request-validation and error-code gaps that looked the same on every protocol. These are listed below.

What changed

awsJson1_0 codec (CW-X12)

  • server/aws/cloudwatch/json_protocol.go: a third codec over the same per-op cores. CloudWatch's JSON and CBOR shapes share member names, so the codec turns the JSON body into CBOR, runs the same per-op handler and <op>Core as the CBOR path, and turns the result back into JSON. Every op served on query or CBOR now works on JSON, and there is no per-op JSON twin to drift.
    • Timestamps are epoch seconds and blobs are base64.
    • Doubles stay doubles (20.0, not 20). Integral numbers such as 60.0 are accepted for int members.
    • Empty lists and maps come back as [] / {}. Null members are dropped.
    • An empty body counts as {}. Anything after the top-level object is rejected with SerializationException.
  • Errors return {"__type","message"} with X-Amzn-Errortype and the right HTTP status. CloudWatch is awsQueryCompatible, so __type holds the model shape name (for example InvalidParameterValueException) and X-Amzn-Query-Error carries the query code (InvalidParameterValue;Sender). As a result botocore's ClientError code and modeled exception class match what the query protocol gives. The shared wire.WriteJSONError already sets X-Amzn-Errortype.
  • handler.go: Matches claims only targets that start with GraniteServiceVersion20100801., so requests for other awsJson services are left alone.
  • server/aws/authzgate.go: maps GraniteServiceVersion20100801. to the cloudwatch IAM service. Without this, every boto3 CloudWatch call under serve -enforce-auth failed closed with AccessDenied, even when a policy allowed cloudwatch:*. SigV4 verification needs no change, because it checks the signature against whatever credential scope the client signed with (monitoring).
  • request_encoding.go: PutMetricData has the requestCompression trait, so SDKs gzip bodies over 10 KiB. The handler now unwraps Content-Encoding: gzip on all three protocols. Before this, a large PutMetricData failed on JSON, on query, and on the Go SDK's CBOR path too.

Validation and error parity (CW-X13), same on every protocol

  • On an alarm or metric stream ARN that does not exist, TagResource, UntagResource and ListTagsForResource return 404 ResourceNotFoundException. The CBOR tag handlers now go through the same routing helpers as query, so the two copies are gone.
  • PutMetricData rejects a datum with more than 30 dimensions (InvalidParameterValue).
  • PutMetricAlarm now returns ValidationError in these cases:
    • Period is not 10, 20, 30 or a multiple of 60.
    • EvaluationPeriods * Period is over 86400 (one day) when Period is under an hour, or over 604800 (seven days) when Period is an hour or more.
    • Statistic is outside the enum.
    • DatapointsToAlarm is greater than EvaluationPeriods.
  • PutDashboard with a body that is not a JSON object returns InvalidParameterInput (the DashboardInvalidInputError shape).
  • Metric stream validation errors use the InvalidParameterValue code, and their messages no longer carry the internal InvalidArgument: prefix. PutMetricStream also rejects a FirehoseArn or RoleArn that is not an ARN.
  • MetricAlarm output includes AlarmConfigurationUpdatedTimestamp. mondriver.AlarmInfo gets the field and the AWS provider fills it from the stored config time.

GetMetricWidgetImage stays out of scope (CW-8).

Tests (all fail on the old code)

  • json_protocol_test.go: a lifecycle table that drives all 29 ops over JSON. It also covers ListMetrics paging, error envelopes (status, __type, both headers, message), trailing data, empty collections, and Matches.
  • json_transcode_test.go: value rules for timestamps, blobs, doubles vs ints, 60.0 into an int member, empty maps and lists, and bad or trailing bodies.
  • request_compression_test.go: gzip PutMetricData on JSON, query and the Go SDK (CBOR).
  • validation_parity_test.go: every CW-X13 case plus the accepted edges (Period 30, 30 dimensions), each run over query, CBOR and JSON. It also checks AlarmConfigurationUpdatedTimestamp on all three.
  • server/aws/cloudwatch_json_dispatch_test.go: on the full AWS server, a Granite target reaches CloudWatch, while DynamoDB, Logs and Kinesis targets still reach their own handlers.
  • server/aws/authzgate_cloudwatch_test.go: under enforce-auth, cloudwatch:* allows DescribeAlarms and PutMetricData, and an explicit deny still blocks DeleteAlarms.

E2E against cloudemu serve

  • boto3 1.43.103, with all 40 calls over awsJson1_0:
    • Operations: put_metric_data, get_metric_statistics, get_metric_data with an expression, list_metrics paging (520 metrics, paginator), put_metric_alarm, describe_alarms (including AlarmConfigurationUpdatedTimestamp), set_alarm_state, describe_alarm_history, put_dashboard/get_dashboard, tag_resource/list_tags_for_resource/untag_resource.
    • Error cases: 16 in total, including every CW-X13 case. Each one checks the ClientError code, message, status and modeled exception class.
  • boto3 under serve -enforce-auth with IAM users restored from --persist:
    • A user with cloudwatch:* can put and describe, and gets AccessDenied on the explicitly denied DeleteAlarms.
    • A user with no CloudWatch policy gets AccessDenied.
    • An unknown key gets InvalidClientTokenId.
  • aws CLI v2.31 (query) and awscli v1.46 (botocore 1.43, JSON) both pass the same flow, including the dashboard, Period and missing-ARN error codes.
  • Terraform aws_cloudwatch_metric_alarm (Go SDK, CBOR): apply, then a clean plan (exit 0), then destroy.

… (CW-X12)

Adds a JSON codec over the shared CBOR per-op cores and unwraps gzip request bodies on all three protocols.
}

func (j *jsonWriter) Header() http.Header { return j.w.Header() }
func (j *jsonWriter) Write(b []byte) (int, error) { return j.w.Write(b) }
…ity (CW-X13)

Maps the Granite target to cloudwatch in the authz gate, tightens the JSON codec, and fixes tag, dimension, alarm period and statistic, dashboard body and metric stream error codes on every protocol. Adds AlarmConfigurationUpdatedTimestamp to MetricAlarm output.
@NitinKumar004 NitinKumar004 changed the title fix(cloudwatch): serve the awsJson1_0 protocol used by botocore 1.43+ (CW-X12) fix(cloudwatch): awsJson1_0 protocol and request validation parity (CW-X12, CW-X13) Sep 27, 2026
@NitinKumar004
NitinKumar004 marked this pull request as ready for review September 27, 2026 13:08
@NitinKumar004
NitinKumar004 merged commit 56f00ef into development Sep 27, 2026
22 of 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