fix(cloudwatch): retain metric-math alarm query definitions (#4594) #6

Merged
Zakaria merged 9 commits from feat/cloudwatch-upstream-fixes into main 2026-10-25 03:08:55 +00:00
5 changed files with 33 additions and 0 deletions
Showing only changes of commit 83aafc0325 - Show all commits
@@ -203,6 +203,8 @@ class TestCloudWatchAlarms:
alarm = next(a for a in alarms if a["AlarmName"] == alarm_name) alarm = next(a for a in alarms if a["AlarmName"] == alarm_name)
assert alarm["StateValue"] == "INSUFFICIENT_DATA" assert alarm["StateValue"] == "INSUFFICIENT_DATA"
assert response["CompositeAlarms"] == []
assert response["LogAlarms"] == []
finally: finally:
cloudwatch_client.delete_alarms(AlarmNames=[alarm_name]) cloudwatch_client.delete_alarms(AlarmNames=[alarm_name])
@@ -263,6 +263,8 @@ public class CloudWatchMetricsJsonHandler {
if (a.getStateReasonData() != null) node.put("StateReasonData", a.getStateReasonData()); if (a.getStateReasonData() != null) node.put("StateReasonData", a.getStateReasonData());
node.put("StateUpdatedTimestamp", a.getStateUpdatedTimestamp()); node.put("StateUpdatedTimestamp", a.getStateUpdatedTimestamp());
} }
response.putArray("CompositeAlarms");
response.putArray("LogAlarms");
return Response.ok(response).build(); return Response.ok(response).build();
} }
@@ -259,6 +259,8 @@ public class CloudWatchMetricsQueryHandler {
toAlarmXml(xml, a); toAlarmXml(xml, a);
} }
xml.end("MetricAlarms"); xml.end("MetricAlarms");
xml.start("CompositeAlarms").end("CompositeAlarms");
xml.start("LogAlarms").end("LogAlarms");
return Response.ok(AwsQueryResponse.envelope("DescribeAlarms", null, xml.build())).build(); return Response.ok(AwsQueryResponse.envelope("DescribeAlarms", null, xml.build())).build();
} }
@@ -350,6 +350,22 @@ class CloudWatchMetricsJsonHandlerTest {
assertEquals("Count", alarm.get("Unit").asText()); 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 // 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 // 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. // that re-plans against its own configuration, which is what floci-io/floci#3660 hit.
@@ -242,4 +242,15 @@ class CloudWatchMetricsQueryHandlerTest {
verify(metricsService).putMetricData(eq("NS"), captor.capture(), eq(REGION)); verify(metricsService).putMetricData(eq("NS"), captor.capture(), eq(REGION));
assertEquals(List.of(" "), captor.getValue().stream().map(MetricDatum::getMetricName).toList()); 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("<MetricAlarms></MetricAlarms>"), xml);
assertTrue(xml.contains("<CompositeAlarms></CompositeAlarms>"), xml);
assertTrue(xml.contains("<LogAlarms></LogAlarms>"), xml);
}
} }