@kensio/yulin 1.20.14 → 1.20.16

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (154) hide show
  1. package/README.md +13 -0
  2. package/{dist/config/oxlint/cffjs2.oxlintrc.json → cffjs2.oxlintrc.json} +1 -1
  3. package/dist/config/oxlint/cffjs2.oxlint.config.js +4 -3
  4. package/dist/sdk/module/sim-sdk-module-client-interceptor.d.ts +5 -0
  5. package/dist/sdk/module/sim-sdk-module-client-interceptor.js +14 -0
  6. package/dist/sdk/module/sim-sdk-static-client-factory.d.ts +26 -0
  7. package/dist/sdk/module/sim-sdk-static-client-factory.js +59 -0
  8. package/dist/sdk/send-patch.d.ts +8 -0
  9. package/dist/sdk/send-patch.js +11 -0
  10. package/dist/service/aws/caller/sim-aws-caller-resolver.d.ts +28 -6
  11. package/dist/service/aws/caller/sim-aws-caller-resolver.js +18 -11
  12. package/dist/service/aws/caller/sim-aws-caller.d.ts +9 -0
  13. package/dist/service/aws/caller/sim-aws-resolved-caller.d.ts +19 -0
  14. package/dist/service/aws/caller/sim-aws-resolved-caller.js +26 -0
  15. package/dist/service/aws/factory/sim-aws-account-region-service-builder.d.ts +6 -0
  16. package/dist/service/aws/factory/sim-aws-account-region-service-builder.js +3 -0
  17. package/dist/service/aws/factory/sim-aws-account-service-cache.d.ts +6 -0
  18. package/dist/service/aws/factory/sim-aws-account-service-cache.js +5 -0
  19. package/dist/service/aws/factory/sim-aws-service-factory.d.ts +5 -0
  20. package/dist/service/aws/factory/sim-aws-service-factory.js +6 -0
  21. package/dist/service/aws/sim-aws-properties.d.ts +15 -0
  22. package/dist/service/aws/sim-aws.js +2 -1
  23. package/dist/service/cloudformation/resource/resolve/property/sim-cfn-resource-property-resolver.d.ts +11 -4
  24. package/dist/service/cloudformation/resource/resolve/property/sim-cfn-resource-property-resolver.js +21 -8
  25. package/dist/service/cloudformation/resource/sim-cfn-resource.type.d.ts +9 -0
  26. package/dist/service/cloudformation/template/dynamic/make-sim-cfn-dynamic-references.d.ts +2 -0
  27. package/dist/service/cloudformation/template/dynamic/make-sim-cfn-dynamic-references.js +2 -2
  28. package/dist/service/cloudformation/template/dynamic/sim-cfn-dynamic-reference-resolvers.d.ts +10 -0
  29. package/dist/service/cloudformation/template/dynamic/sim-cfn-dynamic-reference-resolvers.js +4 -3
  30. package/dist/service/cloudformation/template/dynamic/sim-cfn-dynamic-reference-scan.d.ts +11 -0
  31. package/dist/service/cloudformation/template/dynamic/sim-cfn-dynamic-reference-scan.js +21 -0
  32. package/dist/service/cloudformation/template/dynamic/sim-cfn-dynamic-references.d.ts +24 -2
  33. package/dist/service/cloudformation/template/dynamic/sim-cfn-dynamic-references.js +50 -11
  34. package/dist/service/cloudformation/template/dynamic/sim-cfn-prefetched-dynamic-references.d.ts +42 -0
  35. package/dist/service/cloudformation/template/dynamic/sim-cfn-prefetched-dynamic-references.js +80 -0
  36. package/dist/service/eventbridge/delivery/sim-event-bridge-delivery-failures.d.ts +16 -0
  37. package/dist/service/eventbridge/delivery/sim-event-bridge-delivery-failures.js +19 -0
  38. package/dist/service/iam/authorize/context/sim-iam-auth-z-caller-context-builder.d.ts +30 -7
  39. package/dist/service/iam/authorize/context/sim-iam-auth-z-caller-context-builder.js +20 -11
  40. package/dist/service/iam/authorize/context/sim-iam-auth-z-context-builder.d.ts +2 -1
  41. package/dist/service/iam/authorize/context/sim-iam-auth-z-context-builder.js +5 -1
  42. package/dist/service/iam/authorize/match/sim-iam-policy-principal-matcher.d.ts +10 -0
  43. package/dist/service/iam/authorize/match/sim-iam-policy-principal-matcher.js +15 -3
  44. package/dist/service/iam/authorize/sim-iam-account-auth-z.d.ts +11 -4
  45. package/dist/service/iam/authorize/sim-iam-account-auth-z.js +5 -3
  46. package/dist/service/iam/authorize/sim-iam-authorizer.d.ts +2 -1
  47. package/dist/service/iam/authorize/sim-iam-authorizer.js +1 -0
  48. package/dist/service/iam/command/policy/create-policy/create-policy-input-resolver.js +4 -1
  49. package/dist/service/iam/command/policy/put-role-policy/put-role-policy.handler.js +5 -1
  50. package/dist/service/iam/command/policy/put-user-policy/put-user-policy.handler.js +5 -1
  51. package/dist/service/iam/policy/parse/sim-iam-document-parser.d.ts +4 -1
  52. package/dist/service/iam/policy/parse/sim-iam-document-parser.js +13 -15
  53. package/dist/service/iam/policy/sim-iam-statement-label.d.ts +28 -0
  54. package/dist/service/iam/policy/sim-iam-statement-label.js +17 -0
  55. package/dist/service/iam/policy/sim-iam-statement-strings.d.ts +11 -0
  56. package/dist/service/iam/policy/sim-iam-statement-strings.js +27 -0
  57. package/dist/service/iam/role/sim-iam-role-with-policy.factory.d.ts +9 -0
  58. package/dist/service/iam/role/sim-iam-role-with-policy.factory.js +4 -2
  59. package/dist/service/iam/sim-iam-account-parts.d.ts +8 -0
  60. package/dist/service/iam/sim-iam-account-parts.js +1 -0
  61. package/dist/service/iam/validate/sim-iam-policy-document-validator.d.ts +9 -2
  62. package/dist/service/iam/validate/sim-iam-policy-document-validator.js +37 -19
  63. package/dist/service/lambda/function/code/sim-lambda-vm-zip-function.factory.d.ts +7 -0
  64. package/dist/service/lambda/function/code/sim-lambda-vm-zip-function.factory.js +1 -0
  65. package/dist/service/lambda/function/code/vm/sdk/sim-lambda-provided-sdk-module.d.ts +19 -0
  66. package/dist/service/lambda/function/code/vm/sdk/sim-lambda-provided-sdk-module.js +78 -0
  67. package/dist/service/lambda/function/code/vm/sdk/sim-lambda-sdk-packages-not-installed.error.d.ts +15 -0
  68. package/dist/service/lambda/function/code/vm/sdk/sim-lambda-sdk-packages-not-installed.error.js +27 -0
  69. package/dist/service/lambda/function/code/vm/sdk/sim-lambda-vm-sdk-module-provider.d.ts +9 -0
  70. package/dist/service/lambda/function/code/vm/sdk/sim-sdk-lambda-vm-module-provider.d.ts +10 -0
  71. package/dist/service/lambda/function/code/vm/sdk/sim-sdk-lambda-vm-module-provider.js +28 -34
  72. package/dist/service/lambda/function/code/vm/sim-lambda-host-modules.d.ts +15 -0
  73. package/dist/service/lambda/function/code/vm/sim-lambda-host-modules.js +31 -0
  74. package/dist/service/lambda/function/code/vm/sim-lambda-vm-module-resolver.d.ts +7 -0
  75. package/dist/service/lambda/function/code/vm/sim-lambda-vm-module-resolver.js +15 -0
  76. package/dist/service/lambda/function/code/vm/sim-lambda-vm-modules.js +5 -4
  77. package/dist/service/s3/command/put-bucket-policy/put-bucket-policy.handler.js +4 -1
  78. package/dist/service/scheduler/delivery/sim-scheduler-delivery-failures.d.ts +16 -0
  79. package/dist/service/scheduler/delivery/sim-scheduler-delivery-failures.js +19 -0
  80. package/dist/service/scheduler/index.d.ts +1 -1
  81. package/dist/service/scheduler/index.js +1 -1
  82. package/dist/service/secretsmanager/cfn/dynamic/sim-cfn-secrets-manager-dynamic-reference-resolver.d.ts +13 -15
  83. package/dist/service/secretsmanager/cfn/dynamic/sim-cfn-secrets-manager-dynamic-reference-resolver.js +22 -43
  84. package/dist/service/secretsmanager/cfn/dynamic/sim-cfn-secrets-manager-reference-stand-in.d.ts +9 -0
  85. package/dist/service/secretsmanager/cfn/dynamic/sim-cfn-secrets-manager-reference-stand-in.js +14 -0
  86. package/dist/service/secretsmanager/cfn/dynamic/sim-cfn-secrets-manager-reference-value.d.ts +12 -0
  87. package/dist/service/secretsmanager/cfn/dynamic/sim-cfn-secrets-manager-reference-value.js +29 -0
  88. package/dist/service/ssm/cfn/dynamic/sim-cfn-ssm-dynamic-reference-resolver.d.ts +22 -6
  89. package/dist/service/ssm/cfn/dynamic/sim-cfn-ssm-dynamic-reference-resolver.js +39 -24
  90. package/dist/service/ssm/cfn/dynamic/sim-cfn-ssm-secure-dynamic-reference-resolver.d.ts +10 -5
  91. package/dist/service/ssm/cfn/dynamic/sim-cfn-ssm-secure-dynamic-reference-resolver.js +10 -8
  92. package/dist/service/ssm/sim-ssm.d.ts +10 -2
  93. package/dist/service/ssm/sim-ssm.js +12 -4
  94. package/dist/service/sts/auth-z/assume-role-auth-z-coordinator.d.ts +9 -2
  95. package/dist/service/sts/auth-z/assume-role-auth-z-coordinator.js +2 -2
  96. package/dist/service/sts/auth-z/assume-role-source-account-auth-z.d.ts +10 -4
  97. package/dist/service/sts/auth-z/assume-role-source-account-auth-z.js +8 -7
  98. package/dist/service/sts/auth-z/assume-role-target-auth-z.d.ts +2 -2
  99. package/dist/service/sts/auth-z/assume-role-trust-policy-authorizer.d.ts +6 -2
  100. package/dist/service/sts/auth-z/assume-role-trust-policy-authorizer.js +14 -11
  101. package/dist/service/sts/command/assume-role/assume-role.handler.d.ts +5 -1
  102. package/dist/service/sts/command/assume-role/assume-role.handler.js +5 -2
  103. package/dist/service/sts/command/get-caller-identity/get-caller-identity.handler.d.ts +5 -1
  104. package/dist/service/sts/command/get-caller-identity/get-caller-identity.handler.js +4 -1
  105. package/dist/service/sts/service-role/sim-service-role.js +5 -1
  106. package/dist/service/sts/sim-sts.d.ts +9 -1
  107. package/dist/service/sts/sim-sts.js +4 -0
  108. package/docs/README.md +54 -0
  109. package/docs/ai-skill/README.md +69 -0
  110. package/docs/cli/README.md +284 -0
  111. package/docs/factories/README.md +140 -0
  112. package/docs/lint/README.md +176 -0
  113. package/docs/non-aws-dependencies/README.md +261 -0
  114. package/docs/sdk/README.md +272 -0
  115. package/docs/serve/README.md +1126 -0
  116. package/docs/services/acm/README.md +823 -0
  117. package/docs/services/apigateway/README.md +1919 -0
  118. package/docs/services/apigatewayv2/README.md +2678 -0
  119. package/docs/services/athena/README.md +1044 -0
  120. package/docs/services/bedrock/README.md +350 -0
  121. package/docs/services/cloudformation/README.md +3440 -0
  122. package/docs/services/cloudfront/README.md +2874 -0
  123. package/docs/services/cloudwatch/README.md +420 -0
  124. package/docs/services/cognito/README.md +4923 -0
  125. package/docs/services/dynamodb/README.md +3784 -0
  126. package/docs/services/ecr/README.md +302 -0
  127. package/docs/services/ecs/README.md +2010 -0
  128. package/docs/services/elbv2/README.md +1897 -0
  129. package/docs/services/eventbridge/README.md +1110 -0
  130. package/docs/services/firehose/README.md +805 -0
  131. package/docs/services/glue/README.md +501 -0
  132. package/docs/services/iam/README.md +1283 -0
  133. package/docs/services/kinesis/README.md +454 -0
  134. package/docs/services/kms/README.md +659 -0
  135. package/docs/services/lambda/README.md +3645 -0
  136. package/docs/services/logs/README.md +868 -0
  137. package/docs/services/organizations/README.md +574 -0
  138. package/docs/services/personalize/README.md +835 -0
  139. package/docs/services/rekognition/README.md +1070 -0
  140. package/docs/services/route53/README.md +1783 -0
  141. package/docs/services/s3/README.md +3024 -0
  142. package/docs/services/scheduler/README.md +806 -0
  143. package/docs/services/secretsmanager/README.md +667 -0
  144. package/docs/services/ses/README.md +1011 -0
  145. package/docs/services/sns/README.md +1904 -0
  146. package/docs/services/sqs/README.md +1185 -0
  147. package/docs/services/ssm/README.md +1072 -0
  148. package/docs/services/stepfunctions/README.md +1332 -0
  149. package/docs/services/sts/README.md +245 -0
  150. package/docs/services/wafv2/README.md +1378 -0
  151. package/docs/terraform/README.md +248 -0
  152. package/docs/time/README.md +284 -0
  153. package/llms.txt +59 -0
  154. package/package.json +9 -5
@@ -0,0 +1,420 @@
1
+ # Simulated CloudWatch Metrics
2
+
3
+ Yulin includes a simulated Amazon CloudWatch for tests and local development. It holds custom
4
+ metrics. Those are the datapoints `PutMetricData` publishes, and the statistics
5
+ `GetMetricStatistics` and `GetMetricData` read back from them, without an AWS account. It also holds alarms over those
6
+ metrics, which evaluate on the simulation's clock and notify an SNS topic when they change state.
7
+
8
+ Code that publishes a business metric is code teams already have, and until now the only way to test
9
+ it was to assert that the SDK client had been called. That proves the call was made. What it
10
+ measured goes untested.
11
+
12
+ Only custom metrics live here. This simulation publishes into no `AWS/` namespace. A query for
13
+ `AWS/Lambda` `Invocations` comes back empty, in place of a number that was never measured.
14
+
15
+ CloudWatch specific types are imported from the `@kensio/yulin/cloudwatch` subpath.
16
+
17
+ ## Publishing and reading back a metric
18
+
19
+ A metric is identified by its namespace, its name and its dimensions together. Publishing a value
20
+ and asking for it back at a period is the whole loop:
21
+
22
+ ```typescript sim-cloudwatch-publish-and-read
23
+ /**
24
+ * Publishing a custom metric and reading it back as statistics.
25
+ */
26
+
27
+ import {
28
+ GetMetricStatisticsCommand,
29
+ PutMetricDataCommand,
30
+ } from "@aws-sdk/client-cloudwatch";
31
+
32
+ import { SimAws } from "@kensio/yulin";
33
+
34
+ const simAws = new SimAws();
35
+ const metrics = simAws.cloudWatch();
36
+
37
+ await metrics.putMetricData(
38
+ new PutMetricDataCommand({
39
+ Namespace: "Orders",
40
+ MetricData: [
41
+ {
42
+ MetricName: "Failed",
43
+ Value: 1,
44
+ Unit: "Count",
45
+ Timestamp: new Date("2026-08-16T09:00:10.000Z"),
46
+ Dimensions: [{ Name: "Channel", Value: "web" }],
47
+ },
48
+ ],
49
+ }),
50
+ );
51
+
52
+ const read = await metrics.getMetricStatistics(
53
+ new GetMetricStatisticsCommand({
54
+ Namespace: "Orders",
55
+ MetricName: "Failed",
56
+ Dimensions: [{ Name: "Channel", Value: "web" }],
57
+ StartTime: new Date("2026-08-16T09:00:00.000Z"),
58
+ EndTime: new Date("2026-08-16T09:05:00.000Z"),
59
+ Period: 60,
60
+ Statistics: ["Sum", "SampleCount"],
61
+ }),
62
+ );
63
+
64
+ // One datapoint, stamped with the start of the minute the value fell in.
65
+ console.log(read.Datapoints?.at(0)?.Sum);
66
+ ```
67
+
68
+ A datum may state its values as a plain `Value`, as a `StatisticValues` summary, or as `Values` with
69
+ matching `Counts`. All three answer the same statistics, and a metric published one way reads back
70
+ like a metric published another.
71
+
72
+ Values are checked the way real CloudWatch checks them. A value sits within -2^360 to 2^360, is
73
+ never `NaN` or an infinity, runs to at most 150 unique values in one datum, and carries `Counts`
74
+ only alongside the `Values` it counts. `Unit` is the closed `StandardUnit` set on the way in and on
75
+ the way out. A query naming a unit CloudWatch lacks fails here as it would in an account.
76
+
77
+ ## Metrics are identified by their dimensions
78
+
79
+ Real CloudWatch leaves a custom metric unrolled across its dimensions, and so does this. The same
80
+ metric name published under two channels is two metrics. A read naming no dimensions reaches only the
81
+ metric that was published with none, and never aggregates across the others.
82
+
83
+ That is the behaviour teams most often get wrong, and it is worth a test of its own. A dashboard
84
+ query written against a metric name alone finds nothing at all if every publish carried a dimension.
85
+
86
+ ## Metrics and simulated time
87
+
88
+ A datum carrying no `Timestamp` is stamped from the simulation's clock. A test with
89
+ a frozen clock therefore gets timestamps it can assert on exactly, and one that moves time on gets
90
+ datapoints in the period it moved to:
91
+
92
+ ```typescript sim-cloudwatch-simulated-time
93
+ /**
94
+ * Publishing metrics across simulated minutes, and reading a value per minute.
95
+ */
96
+
97
+ import {
98
+ GetMetricDataCommand,
99
+ PutMetricDataCommand,
100
+ } from "@aws-sdk/client-cloudwatch";
101
+
102
+ import { SimAws } from "@kensio/yulin";
103
+
104
+ const simAws = new SimAws();
105
+ const metrics = simAws.cloudWatch();
106
+ const startedAt = new Date("2026-08-16T09:00:00.000Z");
107
+
108
+ await simAws.clock().setTo(startedAt);
109
+
110
+ // Three failures, one a minute, without waiting three real minutes.
111
+ for (let minute = 0; minute < 3; minute++) {
112
+ await metrics.putMetricData(
113
+ new PutMetricDataCommand({
114
+ Namespace: "Orders",
115
+ MetricData: [{ MetricName: "Failed", Value: 1 }],
116
+ }),
117
+ );
118
+ await simAws.clock().advanceBy({ minutes: 1 });
119
+ }
120
+
121
+ const read = await metrics.getMetricData(
122
+ new GetMetricDataCommand({
123
+ MetricDataQueries: [
124
+ {
125
+ Id: "failed",
126
+ MetricStat: {
127
+ Metric: { Namespace: "Orders", MetricName: "Failed" },
128
+ Period: 60,
129
+ Stat: "Sum",
130
+ },
131
+ },
132
+ ],
133
+ StartTime: startedAt,
134
+ EndTime: new Date("2026-08-16T09:03:00.000Z"),
135
+ ScanBy: "TimestampAscending",
136
+ }),
137
+ );
138
+
139
+ // [1, 1, 1]: one failure in each of the three simulated minutes.
140
+ console.log(read.MetricDataResults?.at(0)?.Values);
141
+ ```
142
+
143
+ `ListMetrics` reads `RecentlyActive: "PT3H"` against the same clock, so advancing time past the
144
+ window drops a metric out of the listing without anything having to expire it.
145
+
146
+ ## Alarms
147
+
148
+ An alarm watches one metric and changes state on the simulation's clock, with no real timer behind
149
+ it. Each evaluation is scheduled at the next period boundary. A frozen clock evaluates nothing, and
150
+ advancing time by twenty minutes walks twenty one-minute evaluations and settles before the next
151
+ line of the test runs.
152
+
153
+ ```typescript sim-cloudwatch-alarm
154
+ /**
155
+ * An alarm that fires into an SNS topic once two of three minutes breach.
156
+ */
157
+
158
+ import {
159
+ DescribeAlarmsCommand,
160
+ PutMetricAlarmCommand,
161
+ PutMetricDataCommand,
162
+ } from "@aws-sdk/client-cloudwatch";
163
+ import { CreateTopicCommand } from "@aws-sdk/client-sns";
164
+
165
+ import { SimAws } from "@kensio/yulin";
166
+
167
+ const simAws = new SimAws();
168
+ const metrics = simAws.cloudWatch();
169
+
170
+ await simAws.clock().setTo(new Date("2026-08-16T09:00:00.000Z"));
171
+
172
+ const topic = await simAws
173
+ .sns()
174
+ .createTopic(new CreateTopicCommand({ Name: "orders-alerts" }));
175
+
176
+ await metrics.putMetricAlarm(
177
+ new PutMetricAlarmCommand({
178
+ AlarmName: "OrdersFailing",
179
+ Namespace: "Orders",
180
+ MetricName: "Failed",
181
+ Statistic: "Sum",
182
+ Period: 60,
183
+ EvaluationPeriods: 3,
184
+ DatapointsToAlarm: 2,
185
+ Threshold: 5,
186
+ ComparisonOperator: "GreaterThanThreshold",
187
+ AlarmActions: [String(topic.TopicArn)],
188
+ }),
189
+ );
190
+
191
+ // Two breaching minutes, without waiting two real minutes.
192
+ for (let minute = 0; minute < 2; minute++) {
193
+ await metrics.putMetricData(
194
+ new PutMetricDataCommand({
195
+ Namespace: "Orders",
196
+ MetricData: [{ MetricName: "Failed", Value: 10 }],
197
+ }),
198
+ );
199
+ await simAws.clock().advanceBy({ minutes: 1 });
200
+ }
201
+
202
+ const described = await metrics.describeAlarms(
203
+ new DescribeAlarmsCommand({ AlarmNames: ["OrdersFailing"] }),
204
+ );
205
+
206
+ // "ALARM", and anything subscribed to the topic has the notification.
207
+ console.log(described.MetricAlarms?.at(0)?.StateValue);
208
+ ```
209
+
210
+ A new alarm is in `INSUFFICIENT_DATA` until it has evaluated a period, as on real CloudWatch. The
211
+ window it looks back over reaches behind the moment the alarm was created. An alarm over a metric
212
+ nothing publishes into, with `TreatMissingData: "breaching"`, therefore fires on its first
213
+ evaluation, without waiting for the periods to accumulate. That is what an account does too.
214
+
215
+ ### Reaching a subscriber
216
+
217
+ An alarm notifies through the ordinary `Publish` path. A notification fans out to the topic's
218
+ subscriptions exactly as an SDK caller's message would, with the JSON body real CloudWatch sends:
219
+ `AlarmName`, `NewStateValue`, `OldStateValue`, `NewStateReason`, `StateChangeTime` and `Trigger`.
220
+ Only a change fires anything, and an alarm that stays in `ALARM` across ten periods notifies once.
221
+
222
+ `SetAlarmState` forces a transition and fires its actions. That is how a test exercises a subscriber
223
+ without arranging for a metric to breach at all.
224
+
225
+ The topic has to be in the same account and region as the alarm, as real CloudWatch requires. An
226
+ action that lands nowhere is recorded, never passed over quietly:
227
+
228
+ ```typescript sim-cloudwatch-alarm-failures
229
+ /**
230
+ * Finding out that an alarm action reached nothing.
231
+ */
232
+
233
+ import { SimAws } from "@kensio/yulin";
234
+
235
+ const simAws = new SimAws();
236
+
237
+ // ...after an alarm with a bad action ARN has fired:
238
+ for (const failure of simAws.cloudWatch().alarmActionFailures) {
239
+ console.log(failure.alarmName, failure.actionArn, failure.reason);
240
+ }
241
+ ```
242
+
243
+ Real CloudWatch tells nobody when an alarm action fails, and this tells nobody either. The alarm
244
+ changes state regardless. Keeping the failure is what stops a subscriber's queue being mysteriously
245
+ empty.
246
+
247
+ ### What an alarm can watch and do
248
+
249
+ - The four threshold comparison operators, `DatapointsToAlarm` for M-of-N evaluation, and all four
250
+ `TreatMissingData` treatments including `ignore`, which leaves the alarm where it is.
251
+ - `ActionsEnabled: false` still evaluates and records state. It just publishes no notification.
252
+ - `DescribeAlarmHistory` reports the state changes with the simulated time each happened at.
253
+ - An SNS topic ARN is the only action target. Auto Scaling, EC2, Systems Manager and Lambda actions
254
+ are refused, never stored and ignored, because an alarm that fired into nowhere would let a test
255
+ pass while the thing the alarm exists to do never happened.
256
+ - Composite alarms, anomaly detection and metric math alarms are all refused.
257
+
258
+ ## Declaring an alarm in a template
259
+
260
+ Alarms are nearly always declared in infrastructure rather than created through the SDK, so
261
+ `AWS::CloudWatch::Alarm` is deployed by simulated CloudFormation. The alarm a stack creates is the
262
+ same thing `PutMetricAlarm` creates. It evaluates on the clock, fires on a transition, and refuses
263
+ what the command refuses.
264
+
265
+ ```yaml
266
+ OrdersFailing:
267
+ Type: AWS::CloudWatch::Alarm
268
+ Properties:
269
+ AlarmName: OrdersFailing
270
+ Namespace: Orders
271
+ MetricName: Failed
272
+ Statistic: Sum
273
+ Period: 60
274
+ EvaluationPeriods: 3
275
+ DatapointsToAlarm: 2
276
+ Threshold: 5
277
+ ComparisonOperator: GreaterThanThreshold
278
+ AlarmActions:
279
+ - !Ref Alerts
280
+ ```
281
+
282
+ `Ref` resolves to the alarm name and `Fn::GetAtt Arn` to the alarm ARN. An `AlarmActions` entry
283
+ holding a `Ref` to an `AWS::SNS::Topic` in the same stack resolves to that topic's ARN. A test can
284
+ deploy the stack, publish a breaching datapoint, advance the clock and read the notification off
285
+ whatever is subscribed. Deleting the stack deletes the alarm and takes its scheduled evaluation back
286
+ off the clock with it.
287
+
288
+ `AlarmName` may be left out, and the alarm is then named after the stack and the logical ID. A test
289
+ still has a name to pass to `DescribeAlarms`. Real CloudFormation generates a physical ID of the
290
+ same shape with a random tail on the end. The tail is left off here, so the name is one a test can
291
+ predict.
292
+
293
+ These are the properties acted on: `AlarmName`, `AlarmDescription`, `ActionsEnabled`,
294
+ `AlarmActions`, `OKActions`, `InsufficientDataActions`, `Namespace`, `MetricName`, `Dimensions`,
295
+ `Statistic`, `Unit`, `Period`, `EvaluationPeriods`, `DatapointsToAlarm`, `Threshold`,
296
+ `ComparisonOperator` and `TreatMissingData`.
297
+
298
+ `Metrics`, `ThresholdMetricId`, `ExtendedStatistic` and `EvaluateLowSampleCountPercentile` are
299
+ refused, in the same words `PutMetricAlarm` refuses them with. Each of them changes what the alarm
300
+ watches or how it decides. An alarm deployed with one ignored would sit in a test looking configured
301
+ and evaluating something else.
302
+
303
+ `Tags` is the one difference from the command, which refuses it outright. Real CloudFormation tags
304
+ the alarm it creates, and this leaves the alarm untagged. A template's tags are usually the whole
305
+ stack's rather than the alarm's, and they are recorded as an ignored property, leaving the deploy
306
+ standing. Nothing reads them back either. An alarm deployed with tags behaves as though the template
307
+ had never named them.
308
+
309
+ `AWS::CloudWatch::CompositeAlarm`, `AWS::CloudWatch::Dashboard` and
310
+ `AWS::CloudWatch::AnomalyDetector` are left undeployed, and recorded as gaps in the stack.
311
+
312
+ ## Permissions
313
+
314
+ CloudWatch metrics have no ARN, leaving a policy nothing to name. Every metric action here is
315
+ granted on `*`. A policy written against something like
316
+ `arn:aws:cloudwatch:eu-west-2:111111111111:metric/Orders/Failed` reaches nothing, here and in an
317
+ account.
318
+
319
+ Alarms are the exception, and do have an ARN. `PutMetricAlarm`, `DeleteAlarms` and `SetAlarmState`
320
+ authorize against `arn:aws:cloudwatch:<region>:<account>:alarm:<name>`, while `DescribeAlarms` and
321
+ `DescribeAlarmHistory` take no resource-level permission at all, exactly as on real CloudWatch.
322
+
323
+ The one way to narrow publishing is the `cloudwatch:namespace` condition key:
324
+
325
+ ```typescript sim-cloudwatch-permissions
326
+ /**
327
+ * A simulated IAM policy allowing a Role to publish into one namespace only.
328
+ */
329
+
330
+ import { PutMetricDataCommand } from "@aws-sdk/client-cloudwatch";
331
+ import { CreateRoleCommand, PutRolePolicyCommand } from "@aws-sdk/client-iam";
332
+
333
+ import { SimAws } from "@kensio/yulin";
334
+
335
+ const simAws = new SimAws();
336
+
337
+ const role = await simAws.iam().createRole(
338
+ new CreateRoleCommand({
339
+ RoleName: "OrdersFunctionRole",
340
+ AssumeRolePolicyDocument: JSON.stringify({
341
+ Version: "2012-10-17",
342
+ Statement: {
343
+ Effect: "Allow",
344
+ Principal: { Service: "lambda.amazonaws.com" },
345
+ Action: "sts:AssumeRole",
346
+ },
347
+ }),
348
+ }),
349
+ );
350
+
351
+ await simAws.iam().putRolePolicy(
352
+ new PutRolePolicyCommand({
353
+ RoleName: "OrdersFunctionRole",
354
+ PolicyName: "PublishOrdersMetrics",
355
+ PolicyDocument: JSON.stringify({
356
+ Version: "2012-10-17",
357
+ Statement: {
358
+ Effect: "Allow",
359
+ Action: "cloudwatch:PutMetricData",
360
+ // Metrics have no ARN, so the namespace condition is what scopes this.
361
+ Resource: "*",
362
+ Condition: { StringEquals: { "cloudwatch:namespace": "Orders" } },
363
+ },
364
+ }),
365
+ }),
366
+ );
367
+
368
+ const asRole = { caller: { kind: "arn", arn: role.Role.Arn } } as const;
369
+
370
+ await simAws.cloudWatch().putMetricData(
371
+ new PutMetricDataCommand({
372
+ Namespace: "Orders",
373
+ MetricData: [{ MetricName: "Failed", Value: 1 }],
374
+ }),
375
+ asRole,
376
+ );
377
+
378
+ // Publishing into any other namespace as this Role is denied.
379
+ ```
380
+
381
+ ## What is simulated
382
+
383
+ - `PutMetricData`, with `Value`, `StatisticValues` and `Values`/`Counts`.
384
+ - `ListMetrics`, filtered by namespace, metric name and dimensions, with `RecentlyActive` and
385
+ `NextToken` paging.
386
+ - `GetMetricStatistics`, with `SampleCount`, `Average`, `Sum`, `Minimum` and `Maximum` over periods
387
+ of a whole number of minutes, filtered by `Unit`.
388
+ - `GetMetricData`, with `MetricStat` queries, `ScanBy` and `ReturnData`.
389
+ - `PutMetricAlarm`, `DescribeAlarms`, `DeleteAlarms`, `SetAlarmState` and `DescribeAlarmHistory`,
390
+ with evaluation on the simulation's clock and SNS notifications on a state change.
391
+ - IAM authorization on each action, including the `cloudwatch:namespace` condition key and
392
+ alarm-ARN resources.
393
+ - `AWS::CloudWatch::Alarm` in simulated CloudFormation, deployed through `PutMetricAlarm` and taken
394
+ down with the stack.
395
+
396
+ ## What is refused, and how it says so
397
+
398
+ Anything real CloudWatch would accept and this leaves undone is refused with a message saying so,
399
+ rather than accepted and ignored. A silently dropped filter is worse than a failure, because the
400
+ test still passes and no longer means what it says.
401
+
402
+ - **Composite and anomaly detection alarms.** `Metrics` and `ThresholdMetricId` on `PutMetricAlarm`
403
+ are refused. There is no trained model here for an anomaly band to come from.
404
+ - **Metric math.** A `GetMetricData` query carrying an `Expression` is refused.
405
+ - **Percentiles and other extended statistics.** They need the individual values behind a period,
406
+ which a `StatisticValues` datum never carries, so CloudWatch itself cannot report one for a metric
407
+ published that way.
408
+ - **High-resolution metrics.** `StorageResolution: 1` is refused. Every period here is a whole
409
+ number of minutes.
410
+ - **`MaxDatapoints`.** Real CloudWatch answers it by widening the period, and every result here
411
+ comes back at the period its query asked for.
412
+ - **Cross-account metrics.** `IncludeLinkedAccounts` and `OwningAccount` are refused. There is no
413
+ monitoring account.
414
+ - **Metrics AWS publishes.** No simulated service writes its own `AWS/` metrics.
415
+
416
+ Two divergences are deliberate, and not refusals. Real CloudWatch rejects a datapoint more than two
417
+ weeks old or more than two hours in the future, and this accepts any timestamp, letting a test seed
418
+ a window without arranging the clock around it. And datapoints come back earliest first, which real
419
+ CloudWatch's contract permits without promising, because a test reading the third period of five
420
+ needs an order it can rely on.