From 83aafc032560209f114aed5abb921f1f871a7d27 Mon Sep 17 00:00:00 2001 From: Stuart Lang Date: Sat, 24 Oct 2026 00:06:44 +0100 Subject: [PATCH] fix(cloudwatch): return empty CompositeAlarms and LogAlarms from DescribeAlarms (#6363) * fix(cloudwatch): return empty CompositeAlarms and LogAlarms from DescribeAlarms AWS sends every alarm collection even when it is empty. Floci only sent MetricAlarms, so the AWS CLI's text output printed a leading None line for each missing paginator result key. Closes #6362 * test(cloudwatch): check empty alarm collections in sdk-test-python instead of bats Drop the new AWS CLI bats file and assert the empty CompositeAlarms and LogAlarms lists in the existing describe_alarms test, as boto3 leaves an absent list out of the response. (cherry picked from commit 42f5019231b446c727d6c94def9d1445263de2d7) --- .../sdk-test-python/tests/test_cloudwatch.py | 2 ++ .../metrics/CloudWatchMetricsJsonHandler.java | 2 ++ .../metrics/CloudWatchMetricsQueryHandler.java | 2 ++ .../CloudWatchMetricsJsonHandlerTest.java | 16 ++++++++++++++++ .../CloudWatchMetricsQueryHandlerTest.java | 11 +++++++++++ 5 files changed, 33 insertions(+) diff --git a/compatibility-tests/sdk-test-python/tests/test_cloudwatch.py b/compatibility-tests/sdk-test-python/tests/test_cloudwatch.py index 9a674cf..6baa173 100644 --- a/compatibility-tests/sdk-test-python/tests/test_cloudwatch.py +++ b/compatibility-tests/sdk-test-python/tests/test_cloudwatch.py @@ -203,6 +203,8 @@ class TestCloudWatchAlarms: alarm = next(a for a in alarms if a["AlarmName"] == alarm_name) assert alarm["StateValue"] == "INSUFFICIENT_DATA" + assert response["CompositeAlarms"] == [] + assert response["LogAlarms"] == [] finally: cloudwatch_client.delete_alarms(AlarmNames=[alarm_name]) diff --git a/src/main/java/io/github/hectorvent/floci/services/cloudwatch/metrics/CloudWatchMetricsJsonHandler.java b/src/main/java/io/github/hectorvent/floci/services/cloudwatch/metrics/CloudWatchMetricsJsonHandler.java index 2aef32e..35c0307 100644 --- a/src/main/java/io/github/hectorvent/floci/services/cloudwatch/metrics/CloudWatchMetricsJsonHandler.java +++ b/src/main/java/io/github/hectorvent/floci/services/cloudwatch/metrics/CloudWatchMetricsJsonHandler.java @@ -263,6 +263,8 @@ public class CloudWatchMetricsJsonHandler { if (a.getStateReasonData() != null) node.put("StateReasonData", a.getStateReasonData()); node.put("StateUpdatedTimestamp", a.getStateUpdatedTimestamp()); } + response.putArray("CompositeAlarms"); + response.putArray("LogAlarms"); return Response.ok(response).build(); } diff --git a/src/main/java/io/github/hectorvent/floci/services/cloudwatch/metrics/CloudWatchMetricsQueryHandler.java b/src/main/java/io/github/hectorvent/floci/services/cloudwatch/metrics/CloudWatchMetricsQueryHandler.java index d3a0542..7b55d0e 100644 --- a/src/main/java/io/github/hectorvent/floci/services/cloudwatch/metrics/CloudWatchMetricsQueryHandler.java +++ b/src/main/java/io/github/hectorvent/floci/services/cloudwatch/metrics/CloudWatchMetricsQueryHandler.java @@ -259,6 +259,8 @@ public class CloudWatchMetricsQueryHandler { toAlarmXml(xml, a); } xml.end("MetricAlarms"); + xml.start("CompositeAlarms").end("CompositeAlarms"); + xml.start("LogAlarms").end("LogAlarms"); return Response.ok(AwsQueryResponse.envelope("DescribeAlarms", null, xml.build())).build(); } diff --git a/src/test/java/io/github/hectorvent/floci/services/cloudwatch/metrics/CloudWatchMetricsJsonHandlerTest.java b/src/test/java/io/github/hectorvent/floci/services/cloudwatch/metrics/CloudWatchMetricsJsonHandlerTest.java index 2d3a2a2..cc81e98 100644 --- a/src/test/java/io/github/hectorvent/floci/services/cloudwatch/metrics/CloudWatchMetricsJsonHandlerTest.java +++ b/src/test/java/io/github/hectorvent/floci/services/cloudwatch/metrics/CloudWatchMetricsJsonHandlerTest.java @@ -350,6 +350,22 @@ class CloudWatchMetricsJsonHandlerTest { assertEquals("Count", alarm.get("Unit").asText()); } + // AWS sends every alarm collection even when it is empty. The CLI's text output writes a + // None line for each paginator result key missing from the page (floci-io/floci#6362). + @Test + void describeAlarms_returnsEmptyCompositeAndLogAlarmCollections() { + handler.handle("PutMetricAlarm", alarmRequest("MetricOnly"), REGION); + + ObjectNode response = (ObjectNode) handler.handle("DescribeAlarms", MAPPER.createObjectNode(), REGION) + .getEntity(); + + assertEquals(1, response.get("MetricAlarms").size()); + assertTrue(response.get("CompositeAlarms").isArray()); + assertTrue(response.get("CompositeAlarms").isEmpty()); + assertTrue(response.get("LogAlarms").isArray()); + assertTrue(response.get("LogAlarms").isEmpty()); + } + // An alarm the caller never gave an M for reports no DatapointsToAlarm at all. AWS omits the // member in that case, and echoing EvaluationPeriods instead reads as a change to any client // that re-plans against its own configuration, which is what floci-io/floci#3660 hit. diff --git a/src/test/java/io/github/hectorvent/floci/services/cloudwatch/metrics/CloudWatchMetricsQueryHandlerTest.java b/src/test/java/io/github/hectorvent/floci/services/cloudwatch/metrics/CloudWatchMetricsQueryHandlerTest.java index 881de60..27869c6 100644 --- a/src/test/java/io/github/hectorvent/floci/services/cloudwatch/metrics/CloudWatchMetricsQueryHandlerTest.java +++ b/src/test/java/io/github/hectorvent/floci/services/cloudwatch/metrics/CloudWatchMetricsQueryHandlerTest.java @@ -242,4 +242,15 @@ class CloudWatchMetricsQueryHandlerTest { verify(metricsService).putMetricData(eq("NS"), captor.capture(), eq(REGION)); assertEquals(List.of(" "), captor.getValue().stream().map(MetricDatum::getMetricName).toList()); } + + @Test + void describeAlarmsReturnsEmptyCompositeAndLogAlarmCollections() { + when(metricsService.describeAlarms(any(), any(), eq(REGION))).thenReturn(List.of()); + + String xml = (String) handler.handle("DescribeAlarms", new MultivaluedHashMap<>(), REGION).getEntity(); + + assertTrue(xml.contains(""), xml); + assertTrue(xml.contains(""), xml); + assertTrue(xml.contains(""), xml); + } }