@intentius/chant-lexicon-aws 0.45.0 → 0.49.0
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.
- package/dist/api/read-client.d.ts +10 -2
- package/dist/api/read-client.d.ts.map +1 -1
- package/dist/components/cloud-executor.d.ts.map +1 -1
- package/dist/composites/agentcore-agent.d.ts +26 -1
- package/dist/composites/agentcore-agent.d.ts.map +1 -1
- package/dist/composites/bucket-deployment.d.ts +82 -0
- package/dist/composites/bucket-deployment.d.ts.map +1 -0
- package/dist/composites/dynamodb-table.d.ts +45 -0
- package/dist/composites/dynamodb-table.d.ts.map +1 -0
- package/dist/composites/ec2-instance-bundle.d.ts +61 -0
- package/dist/composites/ec2-instance-bundle.d.ts.map +1 -0
- package/dist/composites/ecr-repository.d.ts +49 -0
- package/dist/composites/ecr-repository.d.ts.map +1 -0
- package/dist/composites/fargate-service.d.ts +16 -0
- package/dist/composites/fargate-service.d.ts.map +1 -1
- package/dist/composites/index.d.ts +16 -2
- package/dist/composites/index.d.ts.map +1 -1
- package/dist/composites/lambda-function.d.ts +4 -4
- package/dist/composites/lambda-function.d.ts.map +1 -1
- package/dist/composites/monitoring-stack.d.ts +99 -0
- package/dist/composites/monitoring-stack.d.ts.map +1 -0
- package/dist/composites/nlb-service.d.ts +48 -0
- package/dist/composites/nlb-service.d.ts.map +1 -0
- package/dist/composites/step-functions-workflow.d.ts +36 -0
- package/dist/composites/step-functions-workflow.d.ts.map +1 -0
- package/dist/deep-observe.d.ts +45 -0
- package/dist/deep-observe.d.ts.map +1 -1
- package/dist/deep-topology.d.ts +104 -0
- package/dist/deep-topology.d.ts.map +1 -0
- package/dist/effect-receipt-row.d.ts +92 -0
- package/dist/effect-receipt-row.d.ts.map +1 -0
- package/dist/index.d.ts +6 -2
- package/dist/index.d.ts.map +1 -1
- package/dist/integrity.json +9 -4
- package/dist/lint/audit-catalog.d.ts.map +1 -1
- package/dist/lint/post-synth/cf-refs.d.ts +24 -0
- package/dist/lint/post-synth/cf-refs.d.ts.map +1 -1
- package/dist/lint/post-synth/index.d.ts.map +1 -1
- package/dist/lint/post-synth/waw059.d.ts +36 -0
- package/dist/lint/post-synth/waw059.d.ts.map +1 -0
- package/dist/lint/post-synth/waw060.d.ts +16 -0
- package/dist/lint/post-synth/waw060.d.ts.map +1 -0
- package/dist/lint/post-synth/waw061.d.ts +19 -0
- package/dist/lint/post-synth/waw061.d.ts.map +1 -0
- package/dist/lint/post-synth/waw062.d.ts +19 -0
- package/dist/lint/post-synth/waw062.d.ts.map +1 -0
- package/dist/lint/post-synth/waw063.d.ts +35 -0
- package/dist/lint/post-synth/waw063.d.ts.map +1 -0
- package/dist/manifest.json +1 -1
- package/dist/okf/index.md +5 -0
- package/dist/okf/rules/WAW059.md +25 -0
- package/dist/okf/rules/WAW060.md +17 -0
- package/dist/okf/rules/WAW061.md +17 -0
- package/dist/okf/rules/WAW062.md +27 -0
- package/dist/okf/rules/WAW063.md +19 -0
- package/dist/okf/types/Action.md +2 -0
- package/dist/okf/types/AutoScalingGroup.md +1 -0
- package/dist/okf/types/Bucket.md +2 -0
- package/dist/okf/types/ECRRepository.md +1 -0
- package/dist/okf/types/Function.md +1 -0
- package/dist/okf/types/GlobalTable.md +4 -0
- package/dist/okf/types/IamPolicy.md +3 -0
- package/dist/okf/types/InstanceProfile.md +4 -0
- package/dist/okf/types/LaunchTemplate.md +1 -0
- package/dist/okf/types/LoadBalancer.md +1 -0
- package/dist/okf/types/ManagedPolicy.md +4 -0
- package/dist/okf/types/Map.md +2 -0
- package/dist/okf/types/Queue.md +1 -0
- package/dist/okf/types/Role.md +3 -0
- package/dist/okf/types/SecurityGroup.md +1 -0
- package/dist/okf/types/StateMachine.md +4 -0
- package/dist/okf/types/Subnet.md +4 -0
- package/dist/okf/types/Table.md +2 -0
- package/dist/okf/types/Type.md +5 -0
- package/dist/okf/types/Vpc.md +1 -0
- package/dist/op/activities/aws-apply.d.ts +48 -5
- package/dist/op/activities/aws-apply.d.ts.map +1 -1
- package/dist/op/activities/index.d.ts +11 -4
- package/dist/op/activities/index.d.ts.map +1 -1
- package/dist/ownership.d.ts +18 -0
- package/dist/ownership.d.ts.map +1 -1
- package/dist/plugin.d.ts.map +1 -1
- package/dist/receipt-store.d.ts +109 -0
- package/dist/receipt-store.d.ts.map +1 -0
- package/dist/rules/cf-refs.ts +62 -0
- package/dist/rules/waw059.ts +353 -0
- package/dist/rules/waw060.ts +91 -0
- package/dist/rules/waw061.ts +67 -0
- package/dist/rules/waw062.ts +0 -0
- package/dist/rules/waw063.ts +0 -0
- package/dist/serializer.d.ts.map +1 -1
- package/dist/skills/chant-aws-carve-terraform.md +26 -23
- package/dist/teardown.d.ts +85 -0
- package/dist/teardown.d.ts.map +1 -0
- package/package.json +3 -3
- package/src/api/read-client.test.ts +9 -0
- package/src/api/read-client.ts +15 -2
- package/src/components/cloud-executor.ts +10 -1
- package/src/composites/agentcore-agent.ts +35 -13
- package/src/composites/bucket-deployment.test.ts +84 -0
- package/src/composites/bucket-deployment.ts +153 -0
- package/src/composites/composites.test.ts +325 -0
- package/src/composites/dynamodb-table.test.ts +154 -0
- package/src/composites/dynamodb-table.ts +124 -0
- package/src/composites/ec2-instance-bundle.ts +134 -0
- package/src/composites/ecr-repository.test.ts +118 -0
- package/src/composites/ecr-repository.ts +117 -0
- package/src/composites/fargate-service.ts +19 -3
- package/src/composites/index.ts +21 -2
- package/src/composites/monitoring-stack.test.ts +130 -0
- package/src/composites/monitoring-stack.ts +164 -0
- package/src/composites/nlb-service.ts +117 -0
- package/src/composites/step-functions-workflow.ts +127 -0
- package/src/deep-observe.test.ts +201 -1
- package/src/deep-observe.ts +204 -0
- package/src/deep-topology.test.ts +392 -0
- package/src/deep-topology.ts +237 -0
- package/src/effect-receipt-row.test.ts +164 -0
- package/src/effect-receipt-row.ts +147 -0
- package/src/index.ts +28 -7
- package/src/lifecycle-integration.test.ts +4 -0
- package/src/lint/audit-catalog.ts +11 -0
- package/src/lint/post-synth/cf-refs.ts +62 -0
- package/src/lint/post-synth/index.ts +10 -0
- package/src/lint/post-synth/waw059.test.ts +309 -0
- package/src/lint/post-synth/waw059.ts +353 -0
- package/src/lint/post-synth/waw060.test.ts +131 -0
- package/src/lint/post-synth/waw060.ts +91 -0
- package/src/lint/post-synth/waw061.test.ts +99 -0
- package/src/lint/post-synth/waw061.ts +67 -0
- package/src/lint/post-synth/waw062.test.ts +109 -0
- package/src/lint/post-synth/waw062.ts +0 -0
- package/src/lint/post-synth/waw063.test.ts +220 -0
- package/src/lint/post-synth/waw063.ts +0 -0
- package/src/op/activities/aws-apply.test.ts +111 -0
- package/src/op/activities/aws-apply.ts +100 -6
- package/src/op/activities/index.ts +20 -3
- package/src/ownership.test.ts +24 -1
- package/src/ownership.ts +37 -0
- package/src/plugin.ts +59 -10
- package/src/receipt-store.test.ts +204 -0
- package/src/receipt-store.ts +315 -0
- package/src/serializer-ownership.test.ts +18 -0
- package/src/serializer.ts +114 -3
- package/src/skills/chant-aws-carve-terraform.md +26 -23
- package/src/teardown.test.ts +258 -0
- package/src/teardown.ts +276 -0
|
@@ -0,0 +1,130 @@
|
|
|
1
|
+
import { describe, test, expect } from "vitest";
|
|
2
|
+
import { expandComposite } from "@intentius/chant";
|
|
3
|
+
import { MonitoringStack, type MonitoringMetricSpec } from "./monitoring-stack";
|
|
4
|
+
|
|
5
|
+
const errors: MonitoringMetricSpec = {
|
|
6
|
+
title: "OrdersFn Errors",
|
|
7
|
+
namespace: "AWS/Lambda",
|
|
8
|
+
metricName: "Errors",
|
|
9
|
+
dimension: { name: "FunctionName", value: "orders-fn" },
|
|
10
|
+
alarm: { threshold: 5 },
|
|
11
|
+
};
|
|
12
|
+
|
|
13
|
+
const duration: MonitoringMetricSpec = {
|
|
14
|
+
title: "OrdersFn Duration p99",
|
|
15
|
+
namespace: "AWS/Lambda",
|
|
16
|
+
metricName: "Duration",
|
|
17
|
+
dimension: { name: "FunctionName", value: "orders-fn" },
|
|
18
|
+
statistic: "p99",
|
|
19
|
+
period: 60,
|
|
20
|
+
};
|
|
21
|
+
|
|
22
|
+
// The composite's return is a union (which alarmN slots are present), and the
|
|
23
|
+
// repo-wide typecheck rightly refuses to narrow it structurally — tests go
|
|
24
|
+
// through a member map, the same access the expander itself uses.
|
|
25
|
+
type Members = Record<string, { props?: Record<string, any> } | undefined>;
|
|
26
|
+
|
|
27
|
+
describe("MonitoringStack", () => {
|
|
28
|
+
test("dashboard-only metric: no alarm slot, widget carries namespace/metric/dimension", () => {
|
|
29
|
+
const m = MonitoringStack({ metric1: duration }) as unknown as Members;
|
|
30
|
+
expect(m.dashboard).toBeDefined();
|
|
31
|
+
expect(m.alarm1).toBeUndefined();
|
|
32
|
+
const body = JSON.parse(m.dashboard!.props!.DashboardBody);
|
|
33
|
+
expect(body.widgets).toHaveLength(1);
|
|
34
|
+
expect(body.widgets[0].properties.metrics).toEqual([["AWS/Lambda", "Duration", "FunctionName", "orders-fn"]]);
|
|
35
|
+
expect(body.widgets[0].properties.stat).toBe("p99");
|
|
36
|
+
expect(body.widgets[0].properties.period).toBe(60);
|
|
37
|
+
});
|
|
38
|
+
|
|
39
|
+
test("alarmed metric: alarm1 wired from the same spec, defaults filled in", () => {
|
|
40
|
+
const m = MonitoringStack({ metric1: errors }) as unknown as Members;
|
|
41
|
+
expect(m.alarm1).toBeDefined();
|
|
42
|
+
const p = m.alarm1!.props!;
|
|
43
|
+
expect(p.AlarmName).toBe("OrdersFn Errors");
|
|
44
|
+
expect(p.Namespace).toBe("AWS/Lambda");
|
|
45
|
+
expect(p.MetricName).toBe("Errors");
|
|
46
|
+
expect(p.Dimensions).toEqual([{ Name: "FunctionName", Value: "orders-fn" }]);
|
|
47
|
+
expect(p.Statistic).toBe("Sum");
|
|
48
|
+
expect(p.Period).toBe(300);
|
|
49
|
+
expect(p.Threshold).toBe(5);
|
|
50
|
+
expect(p.ComparisonOperator).toBe("GreaterThanThreshold");
|
|
51
|
+
expect(p.EvaluationPeriods).toBe(1);
|
|
52
|
+
});
|
|
53
|
+
|
|
54
|
+
test("three metrics: two alarmed (1, 3), one dashboard-only (2) — slots line up positionally", () => {
|
|
55
|
+
const m = MonitoringStack({
|
|
56
|
+
metric1: errors,
|
|
57
|
+
metric2: duration,
|
|
58
|
+
metric3: { ...errors, title: "OrdersFn Throttles", metricName: "Throttles", alarm: { threshold: 1 } },
|
|
59
|
+
}) as unknown as Members;
|
|
60
|
+
expect(m.alarm1).toBeDefined();
|
|
61
|
+
expect(m.alarm2).toBeUndefined();
|
|
62
|
+
expect(m.alarm3).toBeDefined();
|
|
63
|
+
expect(m.alarm3!.props!.MetricName).toBe("Throttles");
|
|
64
|
+
const body = JSON.parse(m.dashboard!.props!.DashboardBody);
|
|
65
|
+
expect(body.widgets).toHaveLength(3);
|
|
66
|
+
// stacked full-width, one below the last
|
|
67
|
+
expect(body.widgets.map((w: any) => w.y)).toEqual([0, 6, 12]);
|
|
68
|
+
});
|
|
69
|
+
|
|
70
|
+
test("alarm.dimensionValue overrides the dashboard's literal dimension value (Ref/GetAtt escape hatch)", () => {
|
|
71
|
+
const m = MonitoringStack({
|
|
72
|
+
metric1: { ...errors, alarm: { threshold: 5, dimensionValue: { Ref: "OrdersFn" } as any } },
|
|
73
|
+
}) as unknown as Members;
|
|
74
|
+
expect(m.alarm1!.props!.Dimensions).toEqual([{ Name: "FunctionName", Value: { Ref: "OrdersFn" } }]);
|
|
75
|
+
const body = JSON.parse(m.dashboard!.props!.DashboardBody);
|
|
76
|
+
expect(body.widgets[0].properties.metrics[0]).toEqual(["AWS/Lambda", "Errors", "FunctionName", "orders-fn"]);
|
|
77
|
+
});
|
|
78
|
+
|
|
79
|
+
test("no dimension: metric tuple is just [namespace, metricName], alarm Dimensions is undefined", () => {
|
|
80
|
+
const m = MonitoringStack({
|
|
81
|
+
metric1: { title: "5xx", namespace: "AWS/ApiGateway", metricName: "5XXError", alarm: { threshold: 1 } },
|
|
82
|
+
}) as unknown as Members;
|
|
83
|
+
const body = JSON.parse(m.dashboard!.props!.DashboardBody);
|
|
84
|
+
expect(body.widgets[0].properties.metrics).toEqual([["AWS/ApiGateway", "5XXError"]]);
|
|
85
|
+
expect(m.alarm1!.props!.Dimensions).toBeUndefined();
|
|
86
|
+
});
|
|
87
|
+
|
|
88
|
+
test("alarmActions reach AlarmActions, comparisonOperator/evaluationPeriods/treatMissingData are honored", () => {
|
|
89
|
+
const m = MonitoringStack({
|
|
90
|
+
metric1: {
|
|
91
|
+
...errors,
|
|
92
|
+
alarm: {
|
|
93
|
+
threshold: 100,
|
|
94
|
+
comparisonOperator: "LessThanThreshold",
|
|
95
|
+
evaluationPeriods: 3,
|
|
96
|
+
treatMissingData: "notBreaching",
|
|
97
|
+
alarmActions: ["arn:aws:sns:us-east-1:123456789012:pages"],
|
|
98
|
+
},
|
|
99
|
+
},
|
|
100
|
+
}) as unknown as Members;
|
|
101
|
+
const p = m.alarm1!.props!;
|
|
102
|
+
expect(p.ComparisonOperator).toBe("LessThanThreshold");
|
|
103
|
+
expect(p.EvaluationPeriods).toBe(3);
|
|
104
|
+
expect(p.TreatMissingData).toBe("notBreaching");
|
|
105
|
+
expect(p.AlarmActions).toEqual(["arn:aws:sns:us-east-1:123456789012:pages"]);
|
|
106
|
+
});
|
|
107
|
+
|
|
108
|
+
test("defaults escape hatch reaches the dashboard and a named alarm slot", () => {
|
|
109
|
+
const m = MonitoringStack({
|
|
110
|
+
metric1: errors,
|
|
111
|
+
dashboardName: "orders-dash",
|
|
112
|
+
defaults: {
|
|
113
|
+
dashboard: { Tags: [{ Key: "team", Value: "orders" }] },
|
|
114
|
+
alarm1: { DatapointsToAlarm: 2 },
|
|
115
|
+
},
|
|
116
|
+
}) as unknown as Members;
|
|
117
|
+
expect(m.dashboard!.props!.DashboardName).toBe("orders-dash");
|
|
118
|
+
expect(m.dashboard!.props!.Tags).toEqual([{ Key: "team", Value: "orders" }]);
|
|
119
|
+
expect(m.alarm1!.props!.DatapointsToAlarm).toBe(2);
|
|
120
|
+
});
|
|
121
|
+
|
|
122
|
+
test("expandComposite produces stable logical names for dashboard + alarm slots", () => {
|
|
123
|
+
const instance = MonitoringStack({ metric1: errors, metric2: duration });
|
|
124
|
+
const expanded = expandComposite("orders", instance);
|
|
125
|
+
expect(expanded.has("ordersDashboard")).toBe(true);
|
|
126
|
+
expect(expanded.has("ordersAlarm1")).toBe(true);
|
|
127
|
+
expect(expanded.has("ordersAlarm2")).toBe(false);
|
|
128
|
+
expect(expanded.size).toBe(2);
|
|
129
|
+
});
|
|
130
|
+
});
|
|
@@ -0,0 +1,164 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* MonitoringStack — a CloudWatch dashboard wired to up to three named
|
|
3
|
+
* metrics, with an optional alarm per metric.
|
|
4
|
+
*
|
|
5
|
+
* The recurring hand-rolled shape the aws-bench corpus surfaced: a team
|
|
6
|
+
* stands up a `cloudwatch.Dashboard`, a `cloudwatch.Metric` per resource it
|
|
7
|
+
* cares about, and a near-identical `cloudwatch.Alarm` next to each one — 44
|
|
8
|
+
* combined dashboard+alarm+metric instantiations across the corpus' 8 real
|
|
9
|
+
* CDK apps, nothing equivalent shipped in chant (epic #1139).
|
|
10
|
+
*
|
|
11
|
+
* Fixed `metric1..metric3` slots, not a `metrics: MetricSpec[]` array — a
|
|
12
|
+
* loop building `Alarm` resources from an array is exactly the control-flow-
|
|
13
|
+
* over-resources shape EVL002/EVL003 rule out for reference composites (see
|
|
14
|
+
* `EksCluster`'s `addon1..addon3`, this directory). A fourth metric is a bare
|
|
15
|
+
* `Alarm` + a hand-edited `CwDashboard.DashboardBody` the estate declares
|
|
16
|
+
* directly.
|
|
17
|
+
*
|
|
18
|
+
* What is deliberately NOT here:
|
|
19
|
+
* - Notification wiring. `alarm.alarmActions` on each metric takes ARNs the
|
|
20
|
+
* caller already has (an SNS topic from `LambdaSns`, or hand-declared);
|
|
21
|
+
* this composite does not create a `Topic`/`Subscription` itself — who
|
|
22
|
+
* gets paged and on what channel is a real per-estate decision, not a
|
|
23
|
+
* default worth guessing. `defaults.alarm1..3` is the escape hatch for
|
|
24
|
+
* anything else on the alarm (e.g. `OKActions`, `DatapointsToAlarm`).
|
|
25
|
+
* - Math-expression / composite / anomaly-detection alarms (`Alarm.Metrics`,
|
|
26
|
+
* `AlarmModel`) — single-metric threshold alarms only.
|
|
27
|
+
* - Dynamic (`Ref`/`GetAtt`/`Sub`) values inside the dashboard body.
|
|
28
|
+
* `CwDashboard.DashboardBody` is one flat JSON string CloudFormation does
|
|
29
|
+
* not template, so each metric's `namespace`/`metricName`/`dimension.value`
|
|
30
|
+
* must be a literal the caller already knows — the physical name they set
|
|
31
|
+
* on the monitored resource, not one CloudFormation generates. The `Alarm`
|
|
32
|
+
* resources carry no such limit: `Dimensions` is a real structured
|
|
33
|
+
* property, so `alarm.dimensionValue` accepts `Value<string>` and can
|
|
34
|
+
* reference a sibling resource by `Ref`/`GetAtt`.
|
|
35
|
+
*/
|
|
36
|
+
import { Composite, mergeDefaults, type Value } from "@intentius/chant";
|
|
37
|
+
import { Alarm, CwDashboard } from "../generated";
|
|
38
|
+
|
|
39
|
+
export interface MonitoringMetricSpec {
|
|
40
|
+
/** Dashboard widget title, and the alarm's default `AlarmName`. */
|
|
41
|
+
title: string;
|
|
42
|
+
/** CloudWatch namespace, e.g. `"AWS/Lambda"`, `"AWS/DynamoDB"`. */
|
|
43
|
+
namespace: string;
|
|
44
|
+
/** Metric name within the namespace, e.g. `"Errors"`, `"ConsumedReadCapacityUnits"`. */
|
|
45
|
+
metricName: string;
|
|
46
|
+
/**
|
|
47
|
+
* Dimension identifying which resource emits the metric, e.g.
|
|
48
|
+
* `{ name: "FunctionName", value: "my-fn" }`. Omit for an account/region-
|
|
49
|
+
* wide metric. `value` must be a literal — see the dashboard-body note above.
|
|
50
|
+
*/
|
|
51
|
+
dimension?: { name: string; value: string };
|
|
52
|
+
/** Statistic for both the widget and (if alarmed) the alarm. Default `"Sum"`. */
|
|
53
|
+
statistic?: string;
|
|
54
|
+
/** Period in seconds, for both the widget and the alarm. Default `300`. */
|
|
55
|
+
period?: number;
|
|
56
|
+
/** Alarm this metric when set; omit for a dashboard-only metric. */
|
|
57
|
+
alarm?: {
|
|
58
|
+
threshold: number;
|
|
59
|
+
/** Default `"GreaterThanThreshold"`. */
|
|
60
|
+
comparisonOperator?: string;
|
|
61
|
+
/** Default `1`. */
|
|
62
|
+
evaluationPeriods?: number;
|
|
63
|
+
treatMissingData?: "breaching" | "ignore" | "missing" | "notBreaching";
|
|
64
|
+
/** ARNs to notify on ALARM — an existing SNS topic, typically. */
|
|
65
|
+
alarmActions?: Value<string>[];
|
|
66
|
+
/**
|
|
67
|
+
* Dimension value for the alarm. Defaults to `dimension.value`; set this
|
|
68
|
+
* instead when the alarm needs to track a `Ref`/`GetAtt` the dashboard
|
|
69
|
+
* widget cannot (see the dashboard-body note above).
|
|
70
|
+
*/
|
|
71
|
+
dimensionValue?: Value<string>;
|
|
72
|
+
};
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
export interface MonitoringStackProps {
|
|
76
|
+
/** `Value<string>`: a name is routinely built with `Sub`/`Ref` (#1366). */
|
|
77
|
+
dashboardName?: Value<string>;
|
|
78
|
+
metric1: MonitoringMetricSpec;
|
|
79
|
+
metric2?: MonitoringMetricSpec;
|
|
80
|
+
metric3?: MonitoringMetricSpec;
|
|
81
|
+
tags?: Array<{ Key: string; Value: string }>;
|
|
82
|
+
defaults?: {
|
|
83
|
+
dashboard?: Partial<ConstructorParameters<typeof CwDashboard>[0]>;
|
|
84
|
+
alarm1?: Partial<ConstructorParameters<typeof Alarm>[0]>;
|
|
85
|
+
alarm2?: Partial<ConstructorParameters<typeof Alarm>[0]>;
|
|
86
|
+
alarm3?: Partial<ConstructorParameters<typeof Alarm>[0]>;
|
|
87
|
+
};
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
/** A CloudWatch dashboard "metric" widget, stacked full-width below the last. */
|
|
91
|
+
function widget(spec: MonitoringMetricSpec, index: number) {
|
|
92
|
+
// Two literal-array branches, not a spread — EVL004 only traces a spread
|
|
93
|
+
// source to a *module-level* const (packages/core/src/lint/rules/
|
|
94
|
+
// evl004-spread-non-const.ts's isConstIdentifier walks sourceFile.statements
|
|
95
|
+
// only), so a same-function `const dims = [...]` here reads as non-const
|
|
96
|
+
// even though it is one. A ternary between two literals sidesteps the gap
|
|
97
|
+
// instead of fighting it.
|
|
98
|
+
const metricTuple = spec.dimension
|
|
99
|
+
? [spec.namespace, spec.metricName, spec.dimension.name, spec.dimension.value]
|
|
100
|
+
: [spec.namespace, spec.metricName];
|
|
101
|
+
return {
|
|
102
|
+
type: "metric",
|
|
103
|
+
x: 0,
|
|
104
|
+
y: index * 6,
|
|
105
|
+
width: 24,
|
|
106
|
+
height: 6,
|
|
107
|
+
properties: {
|
|
108
|
+
title: spec.title,
|
|
109
|
+
metrics: [metricTuple],
|
|
110
|
+
period: spec.period ?? 300,
|
|
111
|
+
stat: spec.statistic ?? "Sum",
|
|
112
|
+
view: "timeSeries",
|
|
113
|
+
},
|
|
114
|
+
};
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
function alarmProps(spec: MonitoringMetricSpec) {
|
|
118
|
+
const a = spec.alarm!;
|
|
119
|
+
return {
|
|
120
|
+
AlarmName: spec.title,
|
|
121
|
+
AlarmDescription: `${spec.title} breached its threshold`,
|
|
122
|
+
Namespace: spec.namespace,
|
|
123
|
+
MetricName: spec.metricName,
|
|
124
|
+
Dimensions: spec.dimension
|
|
125
|
+
? [{ Name: spec.dimension.name, Value: a.dimensionValue ?? spec.dimension.value }]
|
|
126
|
+
: undefined,
|
|
127
|
+
Statistic: spec.statistic ?? "Sum",
|
|
128
|
+
Period: spec.period ?? 300,
|
|
129
|
+
Threshold: a.threshold,
|
|
130
|
+
ComparisonOperator: a.comparisonOperator ?? "GreaterThanThreshold",
|
|
131
|
+
EvaluationPeriods: a.evaluationPeriods ?? 1,
|
|
132
|
+
TreatMissingData: a.treatMissingData,
|
|
133
|
+
AlarmActions: a.alarmActions,
|
|
134
|
+
};
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
export const MonitoringStack = Composite((props: MonitoringStackProps) => {
|
|
138
|
+
const { defaults } = props;
|
|
139
|
+
const specs = [props.metric1, props.metric2, props.metric3].filter(
|
|
140
|
+
(s): s is MonitoringMetricSpec => s !== undefined,
|
|
141
|
+
);
|
|
142
|
+
|
|
143
|
+
const dashboard = new CwDashboard(mergeDefaults({
|
|
144
|
+
DashboardName: props.dashboardName,
|
|
145
|
+
DashboardBody: JSON.stringify({ widgets: specs.map(widget) }),
|
|
146
|
+
}, defaults?.dashboard));
|
|
147
|
+
|
|
148
|
+
const alarm1 = props.metric1.alarm
|
|
149
|
+
? new Alarm(mergeDefaults(alarmProps(props.metric1), defaults?.alarm1))
|
|
150
|
+
: undefined;
|
|
151
|
+
const alarm2 = props.metric2?.alarm
|
|
152
|
+
? new Alarm(mergeDefaults(alarmProps(props.metric2), defaults?.alarm2))
|
|
153
|
+
: undefined;
|
|
154
|
+
const alarm3 = props.metric3?.alarm
|
|
155
|
+
? new Alarm(mergeDefaults(alarmProps(props.metric3), defaults?.alarm3))
|
|
156
|
+
: undefined;
|
|
157
|
+
|
|
158
|
+
return {
|
|
159
|
+
dashboard,
|
|
160
|
+
...(alarm1 ? { alarm1 } : {}),
|
|
161
|
+
...(alarm2 ? { alarm2 } : {}),
|
|
162
|
+
...(alarm3 ? { alarm3 } : {}),
|
|
163
|
+
};
|
|
164
|
+
}, "MonitoringStack");
|
|
@@ -0,0 +1,117 @@
|
|
|
1
|
+
import { Composite, mergeDefaults } from "@intentius/chant";
|
|
2
|
+
import {
|
|
3
|
+
LoadBalancer,
|
|
4
|
+
TargetGroup,
|
|
5
|
+
TargetGroup_TargetDescription,
|
|
6
|
+
Listener,
|
|
7
|
+
Listener_Action,
|
|
8
|
+
Listener_Certificate,
|
|
9
|
+
} from "../generated";
|
|
10
|
+
|
|
11
|
+
export interface NlbServiceProps {
|
|
12
|
+
vpcId: string;
|
|
13
|
+
subnetIds: string[];
|
|
14
|
+
listenerPort?: number;
|
|
15
|
+
/** Port the target group forwards traffic to. Defaults to `listenerPort`. */
|
|
16
|
+
targetPort?: number;
|
|
17
|
+
/**
|
|
18
|
+
* Listener + target group protocol. `TLS` terminates TLS at the NLB and
|
|
19
|
+
* requires `certificateArn`; the other protocols pass traffic through
|
|
20
|
+
* unmodified, which is the common case for a Network Load Balancer.
|
|
21
|
+
*/
|
|
22
|
+
protocol?: "TCP" | "UDP" | "TLS" | "TCP_UDP";
|
|
23
|
+
scheme?: "internet-facing" | "internal";
|
|
24
|
+
/**
|
|
25
|
+
* Target group target type. `ip` (the default) covers Fargate awsvpc tasks
|
|
26
|
+
* and any routable IP; use `instance` to register EC2 instance IDs directly,
|
|
27
|
+
* or `alb` to chain an NLB in front of an existing Application Load Balancer.
|
|
28
|
+
*/
|
|
29
|
+
targetType?: "instance" | "ip" | "alb";
|
|
30
|
+
/**
|
|
31
|
+
* Static targets to register at creation time (instance IDs, IPs, or an ALB
|
|
32
|
+
* ARN depending on `targetType`). Omit when targets self-register instead —
|
|
33
|
+
* an ECS service, for instance, that references `targetGroup.TargetGroupArn`.
|
|
34
|
+
*/
|
|
35
|
+
targets?: Array<{ id: string; port?: number; availabilityZone?: string }>;
|
|
36
|
+
/** ACM certificate ARN. Required when `protocol` is `TLS`. */
|
|
37
|
+
certificateArn?: string;
|
|
38
|
+
healthCheckProtocol?: "TCP" | "HTTP" | "HTTPS";
|
|
39
|
+
healthCheckPort?: string;
|
|
40
|
+
/** Only meaningful when `healthCheckProtocol` is `HTTP` or `HTTPS`. */
|
|
41
|
+
healthCheckPath?: string;
|
|
42
|
+
defaults?: {
|
|
43
|
+
nlb?: Partial<ConstructorParameters<typeof LoadBalancer>[0]>;
|
|
44
|
+
targetGroup?: Partial<ConstructorParameters<typeof TargetGroup>[0]>;
|
|
45
|
+
listener?: Partial<ConstructorParameters<typeof Listener>[0]>;
|
|
46
|
+
};
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
export const NlbService = Composite((props: NlbServiceProps) => {
|
|
50
|
+
if (props.protocol === "TLS" && !props.certificateArn) {
|
|
51
|
+
throw new Error("NlbService requires certificateArn when protocol is TLS");
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
const listenerPort = props.listenerPort ?? 80;
|
|
55
|
+
const targetPort = props.targetPort ?? listenerPort;
|
|
56
|
+
const protocol = props.protocol ?? "TCP";
|
|
57
|
+
const scheme = props.scheme ?? "internet-facing";
|
|
58
|
+
const targetType = props.targetType ?? "ip";
|
|
59
|
+
const { defaults: defs } = props;
|
|
60
|
+
|
|
61
|
+
// Network Load Balancer. Unlike the ALB composites, no security group is
|
|
62
|
+
// created here — NLBs pass through the client's source IP by default, so
|
|
63
|
+
// access control belongs on the target side (or via an explicit SG passed
|
|
64
|
+
// through `defaults.nlb`), not on the load balancer itself.
|
|
65
|
+
const nlb = new LoadBalancer(mergeDefaults({
|
|
66
|
+
Type: "network",
|
|
67
|
+
Scheme: scheme,
|
|
68
|
+
Subnets: props.subnetIds,
|
|
69
|
+
}, defs?.nlb));
|
|
70
|
+
|
|
71
|
+
// Target group. Static targets are optional — a `.map` keeps the `new`s
|
|
72
|
+
// out of a conditional (EVL002).
|
|
73
|
+
const targets = props.targets?.map((t) =>
|
|
74
|
+
new TargetGroup_TargetDescription({
|
|
75
|
+
Id: t.id,
|
|
76
|
+
Port: t.port,
|
|
77
|
+
AvailabilityZone: t.availabilityZone,
|
|
78
|
+
}),
|
|
79
|
+
);
|
|
80
|
+
|
|
81
|
+
const targetGroup = new TargetGroup(mergeDefaults({
|
|
82
|
+
TargetType: targetType,
|
|
83
|
+
Protocol: protocol,
|
|
84
|
+
Port: targetPort,
|
|
85
|
+
VpcId: props.vpcId,
|
|
86
|
+
HealthCheckProtocol: props.healthCheckProtocol ?? "TCP",
|
|
87
|
+
HealthCheckPort: props.healthCheckPort,
|
|
88
|
+
HealthCheckPath: props.healthCheckPath,
|
|
89
|
+
Targets: targets,
|
|
90
|
+
}, defs?.targetGroup));
|
|
91
|
+
|
|
92
|
+
// Listener — forwards to the target group. TLS carries the cert; a
|
|
93
|
+
// ternary keeps the `new` out of the `if` (EVL002).
|
|
94
|
+
const forwardAction = new Listener_Action({
|
|
95
|
+
Type: "forward",
|
|
96
|
+
TargetGroupArn: targetGroup.TargetGroupArn,
|
|
97
|
+
});
|
|
98
|
+
|
|
99
|
+
const listenerProps: Record<string, unknown> = {
|
|
100
|
+
LoadBalancerArn: nlb.LoadBalancerArn,
|
|
101
|
+
Port: listenerPort,
|
|
102
|
+
Protocol: protocol,
|
|
103
|
+
DefaultActions: [forwardAction],
|
|
104
|
+
Certificates:
|
|
105
|
+
protocol === "TLS" && props.certificateArn
|
|
106
|
+
? [new Listener_Certificate({ CertificateArn: props.certificateArn })]
|
|
107
|
+
: undefined,
|
|
108
|
+
};
|
|
109
|
+
|
|
110
|
+
const listener = new Listener(mergeDefaults(listenerProps, defs?.listener));
|
|
111
|
+
|
|
112
|
+
return {
|
|
113
|
+
nlb,
|
|
114
|
+
targetGroup,
|
|
115
|
+
listener,
|
|
116
|
+
};
|
|
117
|
+
}, "NlbService");
|
|
@@ -0,0 +1,127 @@
|
|
|
1
|
+
import { Composite, mergeDefaults, type Value } from "@intentius/chant";
|
|
2
|
+
import {
|
|
3
|
+
Role,
|
|
4
|
+
Role_Policy,
|
|
5
|
+
LogGroup,
|
|
6
|
+
StateMachine,
|
|
7
|
+
StateMachine_LoggingConfiguration,
|
|
8
|
+
StateMachine_LogDestination,
|
|
9
|
+
StateMachine_CloudWatchLogsLogGroup,
|
|
10
|
+
} from "../generated";
|
|
11
|
+
import { LambdaActions } from "../actions/lambda";
|
|
12
|
+
|
|
13
|
+
const stepFunctionsTrustPolicy = {
|
|
14
|
+
Version: "2012-10-17" as const,
|
|
15
|
+
Statement: [
|
|
16
|
+
{
|
|
17
|
+
Effect: "Allow" as const,
|
|
18
|
+
Principal: { Service: "states.amazonaws.com" },
|
|
19
|
+
Action: "sts:AssumeRole",
|
|
20
|
+
},
|
|
21
|
+
],
|
|
22
|
+
};
|
|
23
|
+
|
|
24
|
+
/**
|
|
25
|
+
* CloudWatch Logs delivery actions a state machine's role needs to write to a
|
|
26
|
+
* log group via `LoggingConfiguration`. These act on the account's log
|
|
27
|
+
* delivery configuration rather than a specific ARN, so AWS documents them
|
|
28
|
+
* granted with `Resource: "*"` — see
|
|
29
|
+
* https://docs.aws.amazon.com/step-functions/latest/dg/cw-logs.html#cloudwatch-iam-policy
|
|
30
|
+
*/
|
|
31
|
+
const LOG_DELIVERY_ACTIONS = [
|
|
32
|
+
"logs:CreateLogDelivery",
|
|
33
|
+
"logs:GetLogDelivery",
|
|
34
|
+
"logs:UpdateLogDelivery",
|
|
35
|
+
"logs:DeleteLogDelivery",
|
|
36
|
+
"logs:ListLogDeliveries",
|
|
37
|
+
"logs:PutResourcePolicy",
|
|
38
|
+
"logs:DescribeResourcePolicies",
|
|
39
|
+
"logs:DescribeLogGroups",
|
|
40
|
+
];
|
|
41
|
+
|
|
42
|
+
const logDeliveryPolicyDocument = {
|
|
43
|
+
Version: "2012-10-17" as const,
|
|
44
|
+
// Log-delivery actions operate on the account's delivery configuration
|
|
45
|
+
// rather than a specific ARN — AWS documents them with Resource: "*".
|
|
46
|
+
Statement: [{ Effect: "Allow" as const, Action: LOG_DELIVERY_ACTIONS, Resource: "*" }],
|
|
47
|
+
};
|
|
48
|
+
|
|
49
|
+
export interface StepFunctionsWorkflowProps {
|
|
50
|
+
/** `Value<string>`: a name is routinely built with `Sub`/`Ref` (#1366). */
|
|
51
|
+
name?: Value<string>;
|
|
52
|
+
/**
|
|
53
|
+
* ASL state machine definition, authored as a plain object. Passed to
|
|
54
|
+
* CloudFormation's `Definition` property rather than `DefinitionString`:
|
|
55
|
+
* CFN accepts the definition as a native JSON value (verified against
|
|
56
|
+
* Floci — both forms round-trip), so a task's `Resource` can carry a
|
|
57
|
+
* `Ref`/`Fn::GetAtt` intrinsic — e.g. `Resource: someFunc.Arn` — directly,
|
|
58
|
+
* with no `Fn::Sub` string templating needed to splice ARNs into a JSON
|
|
59
|
+
* string. Same `Value<T>`-over-string-building bias as #1366, one level up.
|
|
60
|
+
*/
|
|
61
|
+
definition: Record<string, unknown>;
|
|
62
|
+
/** ARNs of the Lambda functions this workflow's tasks invoke; grants `lambda:InvokeFunction` on each. */
|
|
63
|
+
lambdaFunctionArns?: Value<string>[];
|
|
64
|
+
stateMachineType?: "STANDARD" | "EXPRESS";
|
|
65
|
+
/** CloudWatch Logs retention for the auto-created log group. Default 14. */
|
|
66
|
+
logRetentionDays?: number;
|
|
67
|
+
/** `LoggingConfiguration` level. Default `"ALL"` — set `"OFF"` to skip execution logging. */
|
|
68
|
+
loggingLevel?: "ALL" | "ERROR" | "FATAL" | "OFF";
|
|
69
|
+
ManagedPolicyArns?: string[];
|
|
70
|
+
Policies?: InstanceType<typeof Role_Policy>[];
|
|
71
|
+
defaults?: {
|
|
72
|
+
role?: Partial<ConstructorParameters<typeof Role>[0]>;
|
|
73
|
+
logGroup?: Partial<ConstructorParameters<typeof LogGroup>[0]>;
|
|
74
|
+
stateMachine?: Partial<ConstructorParameters<typeof StateMachine>[0]>;
|
|
75
|
+
};
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
export const StepFunctionsWorkflow = Composite((props: StepFunctionsWorkflowProps) => {
|
|
79
|
+
const { defaults } = props;
|
|
80
|
+
|
|
81
|
+
const logGroup = new LogGroup(mergeDefaults({
|
|
82
|
+
RetentionInDays: props.logRetentionDays ?? 14,
|
|
83
|
+
}, defaults?.logGroup));
|
|
84
|
+
|
|
85
|
+
const logDeliveryPolicy = new Role_Policy({
|
|
86
|
+
PolicyName: "StepFunctionsLogDelivery",
|
|
87
|
+
PolicyDocument: logDeliveryPolicyDocument,
|
|
88
|
+
});
|
|
89
|
+
|
|
90
|
+
const invokePolicyDocument = {
|
|
91
|
+
Version: "2012-10-17" as const,
|
|
92
|
+
Statement: [{ Effect: "Allow" as const, Action: LambdaActions.Invoke, Resource: props.lambdaFunctionArns }],
|
|
93
|
+
};
|
|
94
|
+
|
|
95
|
+
// Conditional entry via spread keeps the `new` out of the `if` (EVL002).
|
|
96
|
+
const invokePolicies = props.lambdaFunctionArns && props.lambdaFunctionArns.length > 0
|
|
97
|
+
? [new Role_Policy({ PolicyName: "InvokeLambdaTasks", PolicyDocument: invokePolicyDocument })]
|
|
98
|
+
: [];
|
|
99
|
+
|
|
100
|
+
const policies = [logDeliveryPolicy, ...invokePolicies, ...(props.Policies ?? [])];
|
|
101
|
+
|
|
102
|
+
const role = new Role(mergeDefaults({
|
|
103
|
+
AssumeRolePolicyDocument: stepFunctionsTrustPolicy,
|
|
104
|
+
ManagedPolicyArns: props.ManagedPolicyArns,
|
|
105
|
+
Policies: policies,
|
|
106
|
+
}, defaults?.role));
|
|
107
|
+
|
|
108
|
+
const loggingConfiguration = new StateMachine_LoggingConfiguration({
|
|
109
|
+
Destinations: [
|
|
110
|
+
new StateMachine_LogDestination({
|
|
111
|
+
CloudWatchLogsLogGroup: new StateMachine_CloudWatchLogsLogGroup({ LogGroupArn: logGroup.Arn }),
|
|
112
|
+
}),
|
|
113
|
+
],
|
|
114
|
+
IncludeExecutionData: true,
|
|
115
|
+
Level: props.loggingLevel ?? "ALL",
|
|
116
|
+
});
|
|
117
|
+
|
|
118
|
+
const stateMachine = new StateMachine(mergeDefaults({
|
|
119
|
+
StateMachineName: props.name,
|
|
120
|
+
RoleArn: role.Arn,
|
|
121
|
+
Definition: props.definition,
|
|
122
|
+
StateMachineType: props.stateMachineType ?? "STANDARD",
|
|
123
|
+
LoggingConfiguration: loggingConfiguration,
|
|
124
|
+
}, defaults?.stateMachine));
|
|
125
|
+
|
|
126
|
+
return { role, logGroup, stateMachine };
|
|
127
|
+
}, "StepFunctionsWorkflow");
|