@intentius/chant-lexicon-aws 0.46.0 → 0.50.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/codegen/generate-lexicon.d.ts +5 -0
- package/dist/codegen/generate-lexicon.d.ts.map +1 -1
- package/dist/components/host-delivery.d.ts +4 -3
- package/dist/components/host-delivery.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/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 +7 -2
- package/dist/index.d.ts.map +1 -1
- package/dist/integrity.json +14 -6
- package/dist/lint/audit-catalog.d.ts.map +1 -1
- package/dist/lint/post-synth/cf-refs.d.ts +17 -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/waw016.d.ts +10 -2
- package/dist/lint/post-synth/waw016.d.ts.map +1 -1
- 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/lint/post-synth/waw064.d.ts +17 -0
- package/dist/lint/post-synth/waw064.d.ts.map +1 -0
- package/dist/lint/post-synth/waw065.d.ts +23 -0
- package/dist/lint/post-synth/waw065.d.ts.map +1 -0
- package/dist/lint/post-synth/waw066.d.ts +25 -0
- package/dist/lint/post-synth/waw066.d.ts.map +1 -0
- package/dist/lint/post-synth/waw067.d.ts +24 -0
- package/dist/lint/post-synth/waw067.d.ts.map +1 -0
- package/dist/lint/post-synth/waw068.d.ts +20 -0
- package/dist/lint/post-synth/waw068.d.ts.map +1 -0
- package/dist/manifest.json +1 -1
- package/dist/meta.json +34 -0
- package/dist/okf/index.md +8 -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/rules/WAW064.md +18 -0
- package/dist/okf/rules/WAW065.md +25 -0
- package/dist/okf/rules/WAW066.md +22 -0
- package/dist/okf/rules/WAW067.md +20 -0
- package/dist/okf/rules/WAW068.md +19 -0
- package/dist/okf/types/Action.md +1 -0
- package/dist/okf/types/AutoScalingGroup.md +1 -0
- package/dist/okf/types/Bucket.md +1 -0
- package/dist/okf/types/EC2Route.md +2 -0
- package/dist/okf/types/ECRRepository.md +1 -0
- package/dist/okf/types/Function.md +1 -0
- package/dist/okf/types/IamPolicy.md +1 -0
- package/dist/okf/types/InternetGateway.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 +2 -0
- package/dist/okf/types/Map.md +5 -0
- package/dist/okf/types/NatGateway.md +4 -0
- package/dist/okf/types/Role.md +2 -0
- package/dist/okf/types/RouteTable.md +4 -0
- package/dist/okf/types/SecurityGroup.md +1 -0
- package/dist/okf/types/StateMachine.md +4 -0
- package/dist/okf/types/Subnet.md +6 -0
- package/dist/okf/types/SubnetRouteTableAssociation.md +5 -0
- package/dist/okf/types/Table.md +4 -0
- package/dist/okf/types/TransitGateway.md +6 -0
- package/dist/okf/types/TransitGatewayAttachment.md +4 -0
- package/dist/okf/types/TransitGatewayPeeringAttachment.md +4 -0
- package/dist/okf/types/TransitGatewayRoute.md +5 -0
- package/dist/okf/types/TransitGatewayRouteTable.md +4 -0
- package/dist/okf/types/TransitGatewayRouteTableAssociation.md +4 -0
- package/dist/okf/types/TransitGatewayRouteTablePropagation.md +4 -0
- package/dist/okf/types/TransitGatewayVpcAttachment.md +4 -0
- package/dist/okf/types/Type.md +8 -0
- package/dist/okf/types/VPNConnection.md +4 -0
- package/dist/okf/types/VPNGateway.md +4 -0
- package/dist/okf/types/Vpc.md +1 -0
- package/dist/op/activities/index.d.ts +6 -0
- package/dist/op/activities/index.d.ts.map +1 -1
- package/dist/op/builders.d.ts +47 -0
- package/dist/op/builders.d.ts.map +1 -0
- 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 +40 -0
- package/dist/rules/waw016.ts +37 -16
- package/dist/rules/waw061.ts +67 -0
- package/dist/rules/waw062.ts +0 -0
- package/dist/rules/waw063.ts +0 -0
- package/dist/rules/waw064.ts +52 -0
- package/dist/rules/waw065.ts +110 -0
- package/dist/rules/waw066.ts +155 -0
- package/dist/rules/waw067.ts +114 -0
- package/dist/rules/waw068.ts +78 -0
- package/dist/serializer.d.ts.map +1 -1
- package/dist/skills/chant-aws-carve-terraform.md +26 -23
- package/dist/spec/parse.d.ts +7 -0
- package/dist/spec/parse.d.ts.map +1 -1
- package/package.json +3 -3
- package/src/api/read-client.test.ts +9 -0
- package/src/api/read-client.ts +15 -2
- package/src/codegen/fallback.ts +1 -0
- package/src/codegen/generate-lexicon.ts +6 -0
- package/src/codegen/sam.ts +9 -0
- package/src/components/host-delivery.ts +4 -3
- 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/generated/lexicon-aws.json +34 -0
- package/src/index.ts +36 -7
- package/src/lint/audit-catalog.ts +16 -0
- package/src/lint/post-synth/cf-refs.ts +40 -0
- package/src/lint/post-synth/index.ts +16 -0
- package/src/lint/post-synth/waw016.test.ts +61 -4
- package/src/lint/post-synth/waw016.ts +37 -16
- 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/lint/post-synth/waw064.test.ts +79 -0
- package/src/lint/post-synth/waw064.ts +52 -0
- package/src/lint/post-synth/waw065.test.ts +90 -0
- package/src/lint/post-synth/waw065.ts +110 -0
- package/src/lint/post-synth/waw066.test.ts +108 -0
- package/src/lint/post-synth/waw066.ts +155 -0
- package/src/lint/post-synth/waw067.test.ts +79 -0
- package/src/lint/post-synth/waw067.ts +114 -0
- package/src/lint/post-synth/waw068.test.ts +74 -0
- package/src/lint/post-synth/waw068.ts +78 -0
- package/src/lsp/hover.ts +17 -2
- package/src/op/activities/index.ts +15 -0
- package/src/op/builders.test.ts +61 -0
- package/src/op/builders.ts +72 -0
- package/src/plugin.ts +41 -10
- package/src/receipt-store.test.ts +204 -0
- package/src/receipt-store.ts +315 -0
- package/src/serializer.ts +105 -2
- package/src/skills/chant-aws-carve-terraform.md +26 -23
- package/src/spec/parse.test.ts +74 -0
- package/src/spec/parse.ts +26 -6
|
@@ -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");
|
package/src/deep-observe.test.ts
CHANGED
|
@@ -25,9 +25,10 @@ const {
|
|
|
25
25
|
awsDeepNormalizationHooks,
|
|
26
26
|
hasOwnershipMarker,
|
|
27
27
|
schemaReadOnlyPatterns,
|
|
28
|
+
outOfBandChildName,
|
|
28
29
|
} = await import("./deep-observe");
|
|
29
30
|
const { parseResourceDescription } = await import("./api/read-client");
|
|
30
|
-
const { deepDiffForLexicon } = await import("@intentius/chant/lifecycle/deep-observe");
|
|
31
|
+
const { deepDiffForLexicon, diffDeepObservation } = await import("@intentius/chant/lifecycle/deep-observe");
|
|
31
32
|
const { normalizeDeepObservation, normalizeDeepProperties, flattenDeepProperties } = await import("@intentius/chant/deep-observation");
|
|
32
33
|
|
|
33
34
|
const ok = (text: string) => ({ status: 200, text });
|
|
@@ -598,6 +599,205 @@ describe("observeResourcesDeepAws", () => {
|
|
|
598
599
|
});
|
|
599
600
|
});
|
|
600
601
|
|
|
602
|
+
/**
|
|
603
|
+
* The out-of-band pass (#1015): child resources that exist live under a
|
|
604
|
+
* declared parent and correspond to nothing in the stack, in the estate, or
|
|
605
|
+
* in source — the console-added SNS subscription and inline role policy the
|
|
606
|
+
* issue names. Cloud Control's `ListResources` is scoped to one parent by
|
|
607
|
+
* `ResourceModel`, and everything the listing returns is sorted into
|
|
608
|
+
* declared or out-of-band.
|
|
609
|
+
*/
|
|
610
|
+
describe("out-of-band children (#1015)", () => {
|
|
611
|
+
/** A Cloud Control `ListResources` body — each model a JSON string, like `GetResource`. */
|
|
612
|
+
const list = (descriptions: Array<[string, Record<string, unknown>]>) =>
|
|
613
|
+
ok(
|
|
614
|
+
JSON.stringify({
|
|
615
|
+
ResourceDescriptions: descriptions.map(([id, props]) => ({
|
|
616
|
+
Identifier: id,
|
|
617
|
+
Properties: JSON.stringify(props),
|
|
618
|
+
})),
|
|
619
|
+
}),
|
|
620
|
+
);
|
|
621
|
+
|
|
622
|
+
test("a console-added inline policy surfaces as an entity nobody declared", async () => {
|
|
623
|
+
const fake = httpFake((_identifier, call) => {
|
|
624
|
+
if (!call.target) {
|
|
625
|
+
return stackResources([
|
|
626
|
+
["AppRole", "AWS::IAM::Role", "app-role"],
|
|
627
|
+
// The stack's own inline policy, deployed under its Cloud Control
|
|
628
|
+
// identifier (`PolicyName|RoleName`, the schema's own order).
|
|
629
|
+
["Inline", "AWS::IAM::RolePolicy", "declared-pol|app-role"],
|
|
630
|
+
]);
|
|
631
|
+
}
|
|
632
|
+
if (call.target === "CloudApiService.ListResources") {
|
|
633
|
+
return list([
|
|
634
|
+
["declared-pol|app-role", { RoleName: "app-role", PolicyName: "declared-pol" }],
|
|
635
|
+
[
|
|
636
|
+
"debug-access|app-role",
|
|
637
|
+
{
|
|
638
|
+
RoleName: "app-role",
|
|
639
|
+
PolicyName: "debug-access",
|
|
640
|
+
PolicyDocument: { Statement: [{ Sid: "Debug", Effect: "Allow", Action: ["s3:*"], Resource: "*" }] },
|
|
641
|
+
},
|
|
642
|
+
],
|
|
643
|
+
]);
|
|
644
|
+
}
|
|
645
|
+
return cloudControl("app-role", { RoleName: "app-role" });
|
|
646
|
+
});
|
|
647
|
+
const result = normalizeDeepObservation(
|
|
648
|
+
await observeResourcesDeepAws({ environment: "prod", entityNames: ["AppRole", "Inline"], http: fake.http }),
|
|
649
|
+
);
|
|
650
|
+
|
|
651
|
+
const name = outOfBandChildName("AWS::IAM::RolePolicy", "debug-access|app-role");
|
|
652
|
+
expect(result.resources[name]).toMatchObject({
|
|
653
|
+
type: "AWS::IAM::RolePolicy",
|
|
654
|
+
physicalId: "debug-access|app-role",
|
|
655
|
+
});
|
|
656
|
+
expect(result.resources[name].properties.PolicyName).toBe("debug-access");
|
|
657
|
+
// The stack's own inline policy is declared, not a finding.
|
|
658
|
+
expect(Object.keys(result.resources)).not.toContain(
|
|
659
|
+
outOfBandChildName("AWS::IAM::RolePolicy", "declared-pol|app-role"),
|
|
660
|
+
);
|
|
661
|
+
// The listing was scoped to the parent the stack read resolved.
|
|
662
|
+
const listing = fake.calls.find((c) => c.target === "CloudApiService.ListResources");
|
|
663
|
+
expect(JSON.parse(listing?.body ?? "{}")).toEqual({
|
|
664
|
+
TypeName: "AWS::IAM::RolePolicy",
|
|
665
|
+
ResourceModel: JSON.stringify({ RoleName: "app-role" }),
|
|
666
|
+
});
|
|
667
|
+
});
|
|
668
|
+
|
|
669
|
+
test("a console-added SNS subscription surfaces even though the topic has no deep reader", async () => {
|
|
670
|
+
const topicArn = "arn:aws:sns:us-east-1:111122223333:alerts";
|
|
671
|
+
const fake = httpFake((_identifier, call) => {
|
|
672
|
+
if (!call.target) {
|
|
673
|
+
return stackResources([
|
|
674
|
+
["Alerts", "AWS::SNS::Topic", topicArn],
|
|
675
|
+
["Mail", "AWS::SNS::Subscription", `${topicArn}:5d5a2c73`],
|
|
676
|
+
]);
|
|
677
|
+
}
|
|
678
|
+
if (call.target === "CloudApiService.ListResources") {
|
|
679
|
+
return list([
|
|
680
|
+
[`${topicArn}:5d5a2c73`, { TopicArn: topicArn, Protocol: "email" }],
|
|
681
|
+
[`${topicArn}:rogue`, { TopicArn: topicArn, Protocol: "https", Endpoint: "https://n.example" }],
|
|
682
|
+
]);
|
|
683
|
+
}
|
|
684
|
+
return apiError("ValidationException", "nothing else should be read");
|
|
685
|
+
});
|
|
686
|
+
const result = normalizeDeepObservation(
|
|
687
|
+
await observeResourcesDeepAws({ environment: "prod", entityNames: ["Alerts", "Mail"], http: fake.http }),
|
|
688
|
+
);
|
|
689
|
+
|
|
690
|
+
// The topic itself stays a hole — no deep reader — but the listing under
|
|
691
|
+
// it needs only the physical id the stack already resolved.
|
|
692
|
+
expect(result.unobserved.Alerts?.reason).toBe("unsupported-kind");
|
|
693
|
+
expect(Object.keys(result.resources)).toEqual([
|
|
694
|
+
outOfBandChildName("AWS::SNS::Subscription", `${topicArn}:rogue`),
|
|
695
|
+
]);
|
|
696
|
+
});
|
|
697
|
+
|
|
698
|
+
test("a legacy AWS::IAM::Policy attachment is declared, not out-of-band", async () => {
|
|
699
|
+
const fake = httpFake((_identifier, call) => {
|
|
700
|
+
if (!call.target) {
|
|
701
|
+
return stackResources([
|
|
702
|
+
["AppRole", "AWS::IAM::Role", "app-role"],
|
|
703
|
+
// The legacy inline form: its physical id is the policy name, which
|
|
704
|
+
// is the first part of the RolePolicy identifier the listing returns.
|
|
705
|
+
["LegacyPol", "AWS::IAM::Policy", "stack-legacypol-1ABC"],
|
|
706
|
+
]);
|
|
707
|
+
}
|
|
708
|
+
if (call.target === "CloudApiService.ListResources") {
|
|
709
|
+
return list([["stack-legacypol-1ABC|app-role", { RoleName: "app-role", PolicyName: "stack-legacypol-1ABC" }]]);
|
|
710
|
+
}
|
|
711
|
+
return cloudControl("app-role", { RoleName: "app-role" });
|
|
712
|
+
});
|
|
713
|
+
const result = normalizeDeepObservation(
|
|
714
|
+
await observeResourcesDeepAws({ environment: "prod", entityNames: ["AppRole", "LegacyPol"], http: fake.http }),
|
|
715
|
+
);
|
|
716
|
+
expect(Object.keys(result.resources)).toEqual(["AppRole"]);
|
|
717
|
+
});
|
|
718
|
+
|
|
719
|
+
test("an identity spelled in source counts as declared before it reaches any stack", async () => {
|
|
720
|
+
// The carve case: the policy is declared, its identity is spelled in
|
|
721
|
+
// props, and no stack carries it yet. Listing it is not a finding.
|
|
722
|
+
const declared = entities({
|
|
723
|
+
AppRole: { entityType: "AWS::IAM::Role", props: { RoleName: "app-role" } },
|
|
724
|
+
Carved: { entityType: "AWS::IAM::RolePolicy", props: { RoleName: "app-role", PolicyName: "carved" } },
|
|
725
|
+
});
|
|
726
|
+
const fake = httpFake((_identifier, call) => {
|
|
727
|
+
if (!call.target) return stackResources([["AppRole", "AWS::IAM::Role", "app-role"]]);
|
|
728
|
+
if (call.target === "CloudApiService.ListResources") {
|
|
729
|
+
return list([["carved|app-role", { RoleName: "app-role", PolicyName: "carved" }]]);
|
|
730
|
+
}
|
|
731
|
+
return cloudControl("app-role", { RoleName: "app-role" });
|
|
732
|
+
});
|
|
733
|
+
const result = normalizeDeepObservation(
|
|
734
|
+
await observeResourcesDeepAws({
|
|
735
|
+
environment: "prod",
|
|
736
|
+
entityNames: ["AppRole", "Carved"],
|
|
737
|
+
entities: declared,
|
|
738
|
+
http: fake.http,
|
|
739
|
+
}),
|
|
740
|
+
);
|
|
741
|
+
expect(Object.keys(result.resources)).toEqual(["AppRole"]);
|
|
742
|
+
});
|
|
743
|
+
|
|
744
|
+
test("a failed child listing is silence about children, never a fabricated finding", async () => {
|
|
745
|
+
const fake = httpFake((_identifier, call) => {
|
|
746
|
+
if (!call.target) return stackResources([["AppRole", "AWS::IAM::Role", "app-role"]]);
|
|
747
|
+
if (call.target === "CloudApiService.ListResources") {
|
|
748
|
+
// Floci's answer for an operation it does not serve.
|
|
749
|
+
return apiError("UnsupportedOperation", "ListResources is not supported for AWS::IAM::RolePolicy");
|
|
750
|
+
}
|
|
751
|
+
return cloudControl("app-role", { RoleName: "app-role" });
|
|
752
|
+
});
|
|
753
|
+
const result = normalizeDeepObservation(
|
|
754
|
+
await observeResourcesDeepAws({ environment: "prod", entityNames: ["AppRole"], http: fake.http }),
|
|
755
|
+
);
|
|
756
|
+
expect(Object.keys(result.resources)).toEqual(["AppRole"]);
|
|
757
|
+
expect(result.unobserved).toEqual({});
|
|
758
|
+
});
|
|
759
|
+
|
|
760
|
+
test("--owned: a withheld parent keeps its children out of the report too", async () => {
|
|
761
|
+
const fake = httpFake((_identifier, call) => {
|
|
762
|
+
if (!call.target) return stackResources([["AppRole", "AWS::IAM::Role", "app-role"]]);
|
|
763
|
+
if (call.target === "CloudApiService.ListResources") {
|
|
764
|
+
return list([["debug-access|app-role", { RoleName: "app-role", PolicyName: "debug-access" }]]);
|
|
765
|
+
}
|
|
766
|
+
// No ownership tag: the parent is not chant's.
|
|
767
|
+
return cloudControl("app-role", { RoleName: "app-role" });
|
|
768
|
+
});
|
|
769
|
+
const result = normalizeDeepObservation(
|
|
770
|
+
await observeResourcesDeepAws({ environment: "prod", entityNames: ["AppRole"], owned: true, http: fake.http }),
|
|
771
|
+
);
|
|
772
|
+
expect(result.unobserved.AppRole?.reason).toBe("filtered");
|
|
773
|
+
expect(Object.keys(result.resources)).toEqual([]);
|
|
774
|
+
expect(fake.calls.some((c) => c.target === "CloudApiService.ListResources")).toBe(false);
|
|
775
|
+
});
|
|
776
|
+
|
|
777
|
+
test("the deep diff reports the child as an undeclared entity, and the declared estate stays clean", async () => {
|
|
778
|
+
const declared = entities({ AppRole: { entityType: "AWS::IAM::Role", props: { RoleName: "app-role" } } });
|
|
779
|
+
const fake = httpFake((_identifier, call) => {
|
|
780
|
+
if (!call.target) return stackResources([["AppRole", "AWS::IAM::Role", "app-role"]]);
|
|
781
|
+
if (call.target === "CloudApiService.ListResources") {
|
|
782
|
+
return list([["debug-access|app-role", { RoleName: "app-role", PolicyName: "debug-access" }]]);
|
|
783
|
+
}
|
|
784
|
+
return cloudControl("app-role", { RoleName: "app-role" });
|
|
785
|
+
});
|
|
786
|
+
const live = normalizeDeepObservation(
|
|
787
|
+
await observeResourcesDeepAws({
|
|
788
|
+
environment: "prod",
|
|
789
|
+
entityNames: ["AppRole"],
|
|
790
|
+
entities: declared,
|
|
791
|
+
http: fake.http,
|
|
792
|
+
}),
|
|
793
|
+
);
|
|
794
|
+
const result = diffDeepObservation(declared, live, awsDeepNormalizationHooks);
|
|
795
|
+
expect(result.undeclaredEntities).toEqual([outOfBandChildName("AWS::IAM::RolePolicy", "debug-access|app-role")]);
|
|
796
|
+
expect(result.unchanged).toEqual(["AppRole"]);
|
|
797
|
+
expect(result.drifted).toEqual([]);
|
|
798
|
+
});
|
|
799
|
+
});
|
|
800
|
+
|
|
601
801
|
/**
|
|
602
802
|
* The acceptance test for #1015: the real plugin, a mutated live tree, a
|
|
603
803
|
* baseline, and exactly the genuine drift.
|