@intentius/chant-lexicon-aws 0.49.0 → 0.51.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/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/disruption.d.ts +26 -0
- package/dist/disruption.d.ts.map +1 -0
- package/dist/index.d.ts +1 -0
- 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/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/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 +5 -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/EC2Route.md +2 -0
- package/dist/okf/types/InternetGateway.md +4 -0
- package/dist/okf/types/Map.md +4 -0
- package/dist/okf/types/NatGateway.md +4 -0
- package/dist/okf/types/RouteTable.md +4 -0
- package/dist/okf/types/Subnet.md +2 -0
- package/dist/okf/types/SubnetRouteTableAssociation.md +5 -0
- package/dist/okf/types/Table.md +3 -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 +5 -0
- package/dist/okf/types/VPNConnection.md +4 -0
- package/dist/okf/types/VPNGateway.md +4 -0
- 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/rules/waw016.ts +37 -16
- 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/spec/parse.d.ts +7 -0
- package/dist/spec/parse.d.ts.map +1 -1
- package/package.json +2 -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/disruption.test.ts +198 -0
- package/src/disruption.ts +154 -0
- package/src/generated/lexicon-aws.json +34 -0
- package/src/index.ts +8 -0
- package/src/lint/audit-catalog.ts +10 -0
- package/src/lint/post-synth/index.ts +10 -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/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 +14 -2
- package/src/op/builders.test.ts +61 -0
- package/src/op/builders.ts +72 -0
- package/src/plugin.ts +16 -1
- package/src/spec/parse.test.ts +74 -0
- package/src/spec/parse.ts +26 -6
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
import { describe, test, expect } from "vitest";
|
|
2
|
+
import { createPostSynthContext } from "@intentius/chant-test-utils";
|
|
3
|
+
import { waw068, checkVpnGatewayRedundancy } from "./waw068";
|
|
4
|
+
|
|
5
|
+
function makeCtx(template: object) {
|
|
6
|
+
return createPostSynthContext({ aws: template });
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
describe("WAW068: VPN Gateway Has A Single VPN Connection", () => {
|
|
10
|
+
test("check metadata", () => {
|
|
11
|
+
expect(waw068.id).toBe("WAW068");
|
|
12
|
+
expect(waw068.description).toContain("VPN Connection");
|
|
13
|
+
});
|
|
14
|
+
|
|
15
|
+
test("VPN Gateway with a single VPN connection → warning", () => {
|
|
16
|
+
const ctx = makeCtx({
|
|
17
|
+
Resources: {
|
|
18
|
+
Vgw: { Type: "AWS::EC2::VPNGateway", Properties: { Type: "ipsec.1" } },
|
|
19
|
+
CustomerGw: { Type: "AWS::EC2::CustomerGateway", Properties: { Type: "ipsec.1", BgpAsn: 65000, IpAddress: "203.0.113.1" } },
|
|
20
|
+
VpnConnection: {
|
|
21
|
+
Type: "AWS::EC2::VPNConnection",
|
|
22
|
+
Properties: { Type: "ipsec.1", CustomerGatewayId: { Ref: "CustomerGw" }, VpnGatewayId: { Ref: "Vgw" } },
|
|
23
|
+
},
|
|
24
|
+
},
|
|
25
|
+
});
|
|
26
|
+
const diags = checkVpnGatewayRedundancy(ctx);
|
|
27
|
+
expect(diags).toHaveLength(1);
|
|
28
|
+
expect(diags[0].checkId).toBe("WAW068");
|
|
29
|
+
expect(diags[0].severity).toBe("warning");
|
|
30
|
+
expect(diags[0].entity).toBe("Vgw");
|
|
31
|
+
expect(diags[0].message).toContain("VpnConnection");
|
|
32
|
+
});
|
|
33
|
+
|
|
34
|
+
test("Transit Gateway with a single VPN connection → warning", () => {
|
|
35
|
+
const ctx = makeCtx({
|
|
36
|
+
Resources: {
|
|
37
|
+
Tgw: { Type: "AWS::EC2::TransitGateway", Properties: {} },
|
|
38
|
+
CustomerGw: { Type: "AWS::EC2::CustomerGateway", Properties: { Type: "ipsec.1", BgpAsn: 65000, IpAddress: "203.0.113.1" } },
|
|
39
|
+
VpnConnection: {
|
|
40
|
+
Type: "AWS::EC2::VPNConnection",
|
|
41
|
+
Properties: { Type: "ipsec.1", CustomerGatewayId: { Ref: "CustomerGw" }, TransitGatewayId: { Ref: "Tgw" } },
|
|
42
|
+
},
|
|
43
|
+
},
|
|
44
|
+
});
|
|
45
|
+
const diags = checkVpnGatewayRedundancy(ctx);
|
|
46
|
+
expect(diags).toHaveLength(1);
|
|
47
|
+
expect(diags[0].entity).toBe("Tgw");
|
|
48
|
+
expect(diags[0].message).toContain("Transit Gateway");
|
|
49
|
+
});
|
|
50
|
+
|
|
51
|
+
test("VPN Gateway with two VPN connections attached → no diagnostic", () => {
|
|
52
|
+
const ctx = makeCtx({
|
|
53
|
+
Resources: {
|
|
54
|
+
Vgw: { Type: "AWS::EC2::VPNGateway", Properties: { Type: "ipsec.1" } },
|
|
55
|
+
CustomerGwA: { Type: "AWS::EC2::CustomerGateway", Properties: { Type: "ipsec.1", BgpAsn: 65000, IpAddress: "203.0.113.1" } },
|
|
56
|
+
CustomerGwB: { Type: "AWS::EC2::CustomerGateway", Properties: { Type: "ipsec.1", BgpAsn: 65000, IpAddress: "203.0.113.2" } },
|
|
57
|
+
VpnConnectionA: {
|
|
58
|
+
Type: "AWS::EC2::VPNConnection",
|
|
59
|
+
Properties: { Type: "ipsec.1", CustomerGatewayId: { Ref: "CustomerGwA" }, VpnGatewayId: { Ref: "Vgw" } },
|
|
60
|
+
},
|
|
61
|
+
VpnConnectionB: {
|
|
62
|
+
Type: "AWS::EC2::VPNConnection",
|
|
63
|
+
Properties: { Type: "ipsec.1", CustomerGatewayId: { Ref: "CustomerGwB" }, VpnGatewayId: { Ref: "Vgw" } },
|
|
64
|
+
},
|
|
65
|
+
},
|
|
66
|
+
});
|
|
67
|
+
expect(checkVpnGatewayRedundancy(ctx)).toHaveLength(0);
|
|
68
|
+
});
|
|
69
|
+
|
|
70
|
+
test("no VPN connections at all → no diagnostic", () => {
|
|
71
|
+
const ctx = makeCtx({ Resources: { Vgw: { Type: "AWS::EC2::VPNGateway", Properties: { Type: "ipsec.1" } } } });
|
|
72
|
+
expect(checkVpnGatewayRedundancy(ctx)).toHaveLength(0);
|
|
73
|
+
});
|
|
74
|
+
});
|
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* WAW068: VPN Gateway Has A Single VPN Connection
|
|
3
|
+
*
|
|
4
|
+
* An `AWS::EC2::VPNConnection` always gets two tunnels from AWS, but that
|
|
5
|
+
* redundancy only covers the tunnel pair inside one connection — it does
|
|
6
|
+
* nothing if the connection's Customer Gateway, on-prem device, or the
|
|
7
|
+
* single site it terminates at goes down. The reliability boundary that
|
|
8
|
+
* matters is the connection itself: a `AWS::EC2::VPNGateway` (or a Transit
|
|
9
|
+
* Gateway used for VPN attachment) with only one `VPNConnection` attached
|
|
10
|
+
* has no fallback path for hybrid connectivity if that connection's
|
|
11
|
+
* Customer Gateway or on-prem side fails.
|
|
12
|
+
*
|
|
13
|
+
* Cross-resource join: group every declared `VPNConnection` by the gateway
|
|
14
|
+
* (`VpnGatewayId` or `TransitGatewayId`) it attaches to, and flag any
|
|
15
|
+
* gateway left with exactly one.
|
|
16
|
+
*/
|
|
17
|
+
|
|
18
|
+
import type { PostSynthCheck, PostSynthContext, PostSynthDiagnostic } from "@intentius/chant/lint/post-synth";
|
|
19
|
+
import { parseCFTemplate, findResourceRefs, type CFTemplate } from "./cf-refs";
|
|
20
|
+
|
|
21
|
+
export function checkVpnGatewayRedundancy(ctx: PostSynthContext): PostSynthDiagnostic[] {
|
|
22
|
+
const diagnostics: PostSynthDiagnostic[] = [];
|
|
23
|
+
|
|
24
|
+
for (const [_lexicon, output] of ctx.outputs) {
|
|
25
|
+
const template: CFTemplate | null = parseCFTemplate(output);
|
|
26
|
+
if (!template?.Resources) continue;
|
|
27
|
+
const resources = template.Resources;
|
|
28
|
+
|
|
29
|
+
// gateway logical id -> the VPNConnection logical ids attached to it.
|
|
30
|
+
const connectionsByGateway = new Map<string, string[]>();
|
|
31
|
+
|
|
32
|
+
for (const [logicalId, resource] of Object.entries(resources)) {
|
|
33
|
+
if (resource.Type !== "AWS::EC2::VPNConnection") continue;
|
|
34
|
+
const props = resource.Properties ?? {};
|
|
35
|
+
|
|
36
|
+
const gatewayProp = props.VpnGatewayId !== undefined ? props.VpnGatewayId : props.TransitGatewayId;
|
|
37
|
+
if (gatewayProp === undefined) continue; // no declared gateway target — can't group, stay quiet
|
|
38
|
+
|
|
39
|
+
const gwRefs = findResourceRefs(gatewayProp);
|
|
40
|
+
if (gwRefs.size !== 1) continue;
|
|
41
|
+
const [gwId] = gwRefs;
|
|
42
|
+
|
|
43
|
+
const gw = resources[gwId];
|
|
44
|
+
if (!gw || (gw.Type !== "AWS::EC2::VPNGateway" && gw.Type !== "AWS::EC2::TransitGateway")) continue;
|
|
45
|
+
|
|
46
|
+
let list = connectionsByGateway.get(gwId);
|
|
47
|
+
if (!list) {
|
|
48
|
+
list = [];
|
|
49
|
+
connectionsByGateway.set(gwId, list);
|
|
50
|
+
}
|
|
51
|
+
list.push(logicalId);
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
for (const [gwId, connectionIds] of connectionsByGateway) {
|
|
55
|
+
if (connectionIds.length !== 1) continue;
|
|
56
|
+
|
|
57
|
+
const kind = resources[gwId].Type === "AWS::EC2::VPNGateway" ? "VPN Gateway" : "Transit Gateway";
|
|
58
|
+
diagnostics.push({
|
|
59
|
+
checkId: "WAW068",
|
|
60
|
+
severity: "warning",
|
|
61
|
+
message: `${kind} "${gwId}" has only one VPN connection ("${connectionIds[0]}") attached — no redundant path if this connection, its tunnels, or its Customer Gateway fail; attach a second VPNConnection (ideally to a separate Customer Gateway) for hybrid-connectivity redundancy`,
|
|
62
|
+
entity: gwId,
|
|
63
|
+
lexicon: "aws",
|
|
64
|
+
});
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
return diagnostics;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
export const waw068: PostSynthCheck = {
|
|
72
|
+
id: "WAW068",
|
|
73
|
+
description: "VPN Gateway or Transit Gateway has only one attached VPN Connection — no redundant path for hybrid connectivity",
|
|
74
|
+
|
|
75
|
+
check(ctx: PostSynthContext): PostSynthDiagnostic[] {
|
|
76
|
+
return checkVpnGatewayRedundancy(ctx);
|
|
77
|
+
},
|
|
78
|
+
};
|
package/src/lsp/hover.ts
CHANGED
|
@@ -61,9 +61,21 @@ function resourceHover(className: string, entry: LexiconEntry): HoverInfo | unde
|
|
|
61
61
|
lines.push(`**Conditionally immutable:** ${entry.conditionalCreateOnly.map((p) => `\`${p}\``).join(", ")}`);
|
|
62
62
|
}
|
|
63
63
|
|
|
64
|
-
|
|
64
|
+
// Declared and inferred deprecations rest on different evidence, so they
|
|
65
|
+
// read as separate lines rather than one merged list.
|
|
66
|
+
const inferred = new Set(entry.inferredDeprecations ?? []);
|
|
67
|
+
const declared = (entry.deprecatedProperties ?? []).filter((p) => !inferred.has(p));
|
|
68
|
+
|
|
69
|
+
if (declared.length > 0) {
|
|
70
|
+
lines.push("");
|
|
71
|
+
lines.push(`**Deprecated properties:** ${declared.map((p) => `\`${p}\``).join(", ")}`);
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
if (inferred.size > 0) {
|
|
65
75
|
lines.push("");
|
|
66
|
-
lines.push(
|
|
76
|
+
lines.push(
|
|
77
|
+
`**Possibly deprecated** (description text only, not declared by the Registry): ${[...inferred].map((p) => `\`${p}\``).join(", ")}`,
|
|
78
|
+
);
|
|
67
79
|
}
|
|
68
80
|
|
|
69
81
|
return { contents: lines.join("\n") };
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Typed step-builder wrappers (chant #1288 Stage 2) — see
|
|
3
|
+
* `lexicons/k8s/src/op/builders.test.ts`'s module doc for what's asserted
|
|
4
|
+
* and why.
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
import { describe, test, expect } from "vitest";
|
|
8
|
+
import { awsApply as awsApplyOld, awsDelete as awsDeleteOld, flociUp as flociUpOld, flociDown as flociDownOld, stepOutput, type StepOutputRef } from "@intentius/chant/op";
|
|
9
|
+
import { awsApply, awsDelete, flociUp, flociDown } from "./builders";
|
|
10
|
+
|
|
11
|
+
describe("aws typed step builders (#1288 Stage 2)", () => {
|
|
12
|
+
test("awsApply: identical ActivityStep to core's original", () => {
|
|
13
|
+
const opts = { stackName: "my-stack" };
|
|
14
|
+
expect(awsApply("dist/template.json", opts)).toEqual(awsApplyOld("dist/template.json", opts));
|
|
15
|
+
});
|
|
16
|
+
|
|
17
|
+
test("awsApply: identical ActivityStep with more opts", () => {
|
|
18
|
+
const opts = { stackName: "my-stack", region: "us-west-2", capabilities: ["CAPABILITY_NAMED_IAM"], profile: "fastIdempotent" as const };
|
|
19
|
+
expect(awsApply("dist/template.json", opts)).toEqual(awsApplyOld("dist/template.json", opts));
|
|
20
|
+
});
|
|
21
|
+
|
|
22
|
+
test("awsDelete: identical ActivityStep to core's original", () => {
|
|
23
|
+
const opts = { stackName: "my-stack" };
|
|
24
|
+
expect(awsDelete("dist/template.json", opts)).toEqual(awsDeleteOld("dist/template.json", opts));
|
|
25
|
+
});
|
|
26
|
+
|
|
27
|
+
test("flociUp/flociDown: identical ActivityStep to core's original", () => {
|
|
28
|
+
expect(flociUp()).toEqual(flociUpOld());
|
|
29
|
+
expect(flociUp({ port: 4567 })).toEqual(flociUpOld({ port: 4567 }));
|
|
30
|
+
expect(flociDown()).toEqual(flociDownOld());
|
|
31
|
+
});
|
|
32
|
+
|
|
33
|
+
test("awsApply: accepts a StepOutputRef in a typed slot", () => {
|
|
34
|
+
const ref = stepOutput("resolve-region", "region");
|
|
35
|
+
const step = awsApply("dist/template.json", { stackName: "my-stack", region: ref });
|
|
36
|
+
expect(step.args?.region).toBe(ref);
|
|
37
|
+
});
|
|
38
|
+
|
|
39
|
+
test("awsApply: .out is reachable when an id is given", () => {
|
|
40
|
+
const step = awsApply("dist/template.json", { stackName: "my-stack", id: "apply" });
|
|
41
|
+
const ref: StepOutputRef = step.out.releaseName;
|
|
42
|
+
expect(ref.step).toBe("apply");
|
|
43
|
+
});
|
|
44
|
+
});
|
|
45
|
+
|
|
46
|
+
// ── Compile-time-only: authoring-time type errors (never executed) ──────────
|
|
47
|
+
function _typeChecksOnly(): void {
|
|
48
|
+
// @ts-expect-error — stackName is required (the activity fails without it);
|
|
49
|
+
// omitting opts entirely is no longer a way around that.
|
|
50
|
+
awsApply("dist/template.json");
|
|
51
|
+
|
|
52
|
+
// @ts-expect-error — "stackname" (wrong case) is not a key of AwsApplyArgs.
|
|
53
|
+
awsApply("dist/template.json", { stackname: "my-stack" });
|
|
54
|
+
|
|
55
|
+
// @ts-expect-error — capabilities must be string[], not a single string.
|
|
56
|
+
awsApply("dist/template.json", { stackName: "my-stack", capabilities: "CAPABILITY_NAMED_IAM" });
|
|
57
|
+
|
|
58
|
+
// @ts-expect-error — port must be a number.
|
|
59
|
+
flociUp({ port: "4566" });
|
|
60
|
+
}
|
|
61
|
+
void _typeChecksOnly;
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Typed step-builder wrappers for this lexicon's activities (chant #1288
|
|
3
|
+
* Stage 2). See `lexicons/k8s/src/op/builders.ts`'s module doc for why these
|
|
4
|
+
* live beside their `*Args` interfaces rather than in core or the temporal
|
|
5
|
+
* barrel: core cannot import a lexicon's types (lexicons depend on core, not
|
|
6
|
+
* the reverse), and wiring them into the cloud-agnostic temporal barrel
|
|
7
|
+
* would make it depend on every cloud lexicon at runtime. `opts`'s type in
|
|
8
|
+
* each wrapper below IS the activity's own `*Args` interface (via
|
|
9
|
+
* `Omit`/`WithStepRefs`) — never restated. `core`'s own
|
|
10
|
+
* `awsApply`/`awsDelete`/`flociUp`/`flociDown` are unchanged and produce
|
|
11
|
+
* byte-identical `ActivityStep` output; these are purely additive.
|
|
12
|
+
*/
|
|
13
|
+
|
|
14
|
+
import {
|
|
15
|
+
activity,
|
|
16
|
+
takeProfileAndId,
|
|
17
|
+
type ActivityStep,
|
|
18
|
+
type NamedActivityStep,
|
|
19
|
+
type WithStepRefs,
|
|
20
|
+
} from "@intentius/chant/op";
|
|
21
|
+
import type { AwsApplyArgs, AwsDeleteArgs } from "./activities/aws-apply";
|
|
22
|
+
import type { FlociUpArgs, FlociDownArgs } from "./activities/floci";
|
|
23
|
+
|
|
24
|
+
/** Extra opts every wrapper below accepts alongside its activity's own fields. */
|
|
25
|
+
type StepOpts = { profile?: ActivityStep["profile"]; id?: string };
|
|
26
|
+
|
|
27
|
+
/**
|
|
28
|
+
* Deploy a built CloudFormation template directly against the CFN API — the
|
|
29
|
+
* fully typed twin of core's `awsApply`. `opts` is {@link AwsApplyArgs}
|
|
30
|
+
* itself, minus the positional `templatePath`. Defaults to the `longInfra`
|
|
31
|
+
* profile.
|
|
32
|
+
*/
|
|
33
|
+
export const awsApply = (
|
|
34
|
+
templatePath: string,
|
|
35
|
+
opts: WithStepRefs<Omit<AwsApplyArgs, "templatePath">> & StepOpts,
|
|
36
|
+
): NamedActivityStep => {
|
|
37
|
+
const { args, profile, id } = takeProfileAndId(opts as Record<string, unknown> | undefined);
|
|
38
|
+
return activity("awsApply", { templatePath, ...args }, { profile: profile ?? "longInfra", ...(id ? { id } : {}) });
|
|
39
|
+
};
|
|
40
|
+
|
|
41
|
+
/**
|
|
42
|
+
* Delete a CloudFormation stack — the inverse of {@link awsApply}, fully
|
|
43
|
+
* typed twin of core's `awsDelete`. `opts` is {@link AwsDeleteArgs} itself,
|
|
44
|
+
* minus the positional `templatePath`. Defaults to the `longInfra` profile.
|
|
45
|
+
*/
|
|
46
|
+
export const awsDelete = (
|
|
47
|
+
templatePath: string,
|
|
48
|
+
opts: WithStepRefs<Omit<AwsDeleteArgs, "templatePath">> & StepOpts,
|
|
49
|
+
): NamedActivityStep => {
|
|
50
|
+
const { args, profile, id } = takeProfileAndId(opts as Record<string, unknown> | undefined);
|
|
51
|
+
return activity("awsDelete", { templatePath, ...args }, { profile: profile ?? "longInfra", ...(id ? { id } : {}) });
|
|
52
|
+
};
|
|
53
|
+
|
|
54
|
+
/**
|
|
55
|
+
* Boot the local Floci AWS emulator — the fully typed twin of core's
|
|
56
|
+
* `flociUp`. `opts` is {@link FlociUpArgs} itself. Defaults to the
|
|
57
|
+
* `longInfra` profile.
|
|
58
|
+
*/
|
|
59
|
+
export const flociUp = (opts?: WithStepRefs<FlociUpArgs> & StepOpts): NamedActivityStep => {
|
|
60
|
+
const { args, profile, id } = takeProfileAndId(opts as Record<string, unknown> | undefined);
|
|
61
|
+
return activity("flociUp", args, { profile: profile ?? "longInfra", ...(id ? { id } : {}) });
|
|
62
|
+
};
|
|
63
|
+
|
|
64
|
+
/**
|
|
65
|
+
* Stop and remove the local Floci emulator container — the fully typed twin
|
|
66
|
+
* of core's `flociDown`. `opts` is {@link FlociDownArgs} itself. Defaults to
|
|
67
|
+
* the `fastIdempotent` profile.
|
|
68
|
+
*/
|
|
69
|
+
export const flociDown = (opts?: WithStepRefs<FlociDownArgs> & StepOpts): NamedActivityStep => {
|
|
70
|
+
const { args, profile, id } = takeProfileAndId(opts as Record<string, unknown> | undefined);
|
|
71
|
+
return activity("flociDown", args, { profile: profile ?? "fastIdempotent", ...(id ? { id } : {}) });
|
|
72
|
+
};
|
package/src/plugin.ts
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { createRequire } from "module";
|
|
2
2
|
import { detectTemplate } from "./detect";
|
|
3
|
-
import type { LexiconPlugin, IntrinsicDef, ObservationResult, DeepObservationResult, DependencyObservation, ResourceMetadata, ExportedTemplate, ResourceSelector, InitTemplateSet, StackStatusObservation } from "@intentius/chant/lexicon";
|
|
3
|
+
import type { LexiconPlugin, IntrinsicDef, ObservationResult, DeepObservationResult, DependencyObservation, DisruptionQuery, DisruptionVerdict, ResourceMetadata, ExportedTemplate, ResourceSelector, InitTemplateSet, StackStatusObservation } from "@intentius/chant/lexicon";
|
|
4
4
|
const require = createRequire(import.meta.url);
|
|
5
5
|
import type { LintRule } from "@intentius/chant/lint/rule";
|
|
6
6
|
import type { TemplateParser } from "@intentius/chant/import/parser";
|
|
@@ -25,6 +25,7 @@ import {
|
|
|
25
25
|
type StackResource,
|
|
26
26
|
} from "./api/read-client";
|
|
27
27
|
import { awsDeepNormalizationHooks, observeResourcesDeepAws } from "./deep-observe";
|
|
28
|
+
import { awsDisruption } from "./disruption";
|
|
28
29
|
import { awsReferenceCatalog } from "./reference-catalog";
|
|
29
30
|
import { AMBIENT_KINDS } from "./ambient";
|
|
30
31
|
import { canDescribe, describeOwnProperties, stampRegion } from "./properties";
|
|
@@ -765,6 +766,20 @@ aws cloudformation wait stack-update-complete --stack-name my-app-prod`,
|
|
|
765
766
|
return observeAwsDependencies({ observed: options.observed, region: options.region });
|
|
766
767
|
},
|
|
767
768
|
|
|
769
|
+
/**
|
|
770
|
+
* What a pending update costs (#1665), read off the Registry schema the
|
|
771
|
+
* codegen already compiled: a changed `createOnlyProperties` entry is a
|
|
772
|
+
* replacement, and `replacementStrategy: delete_then_create` makes it a
|
|
773
|
+
* destroy. A conditionally-create-only property is `unknown` on purpose —
|
|
774
|
+
* the schema says "depends on the value", which is not an answer.
|
|
775
|
+
*/
|
|
776
|
+
classifyDisruption(options: {
|
|
777
|
+
environment: string;
|
|
778
|
+
changes: DisruptionQuery[];
|
|
779
|
+
}): Record<string, DisruptionVerdict> {
|
|
780
|
+
return awsDisruption(options);
|
|
781
|
+
},
|
|
782
|
+
|
|
768
783
|
/**
|
|
769
784
|
* Resources of a managed kind that exist without being declared or
|
|
770
785
|
* referenced (#1278) — the account's default security groups, an unattached
|
package/src/spec/parse.test.ts
CHANGED
|
@@ -187,6 +187,80 @@ describe("parseCFNSchema", () => {
|
|
|
187
187
|
});
|
|
188
188
|
const result = parseCFNSchema(schema);
|
|
189
189
|
expect(result.resource.deprecatedProperties).toEqual([]);
|
|
190
|
+
expect(result.resource.inferredDeprecations).toEqual([]);
|
|
191
|
+
});
|
|
192
|
+
|
|
193
|
+
// --- Deprecation basis (#1701) ---
|
|
194
|
+
|
|
195
|
+
test("a declared deprecation is not recorded as inferred", () => {
|
|
196
|
+
const schema = JSON.stringify({
|
|
197
|
+
typeName: "AWS::Test::Declared",
|
|
198
|
+
properties: {
|
|
199
|
+
OldProp: { type: "string", description: "An old property." },
|
|
200
|
+
},
|
|
201
|
+
deprecatedProperties: ["/properties/OldProp"],
|
|
202
|
+
additionalProperties: false,
|
|
203
|
+
});
|
|
204
|
+
const result = parseCFNSchema(schema);
|
|
205
|
+
expect(result.resource.deprecatedProperties).toEqual(["OldProp"]);
|
|
206
|
+
expect(result.resource.inferredDeprecations).toEqual([]);
|
|
207
|
+
});
|
|
208
|
+
|
|
209
|
+
test("a description-mined deprecation is recorded as inferred", () => {
|
|
210
|
+
const schema = JSON.stringify({
|
|
211
|
+
typeName: "AWS::Test::Mined",
|
|
212
|
+
properties: {
|
|
213
|
+
OldProp: { type: "string", description: "This property is deprecated. Use NewProp instead." },
|
|
214
|
+
NewProp: { type: "string", description: "The replacement property" },
|
|
215
|
+
},
|
|
216
|
+
additionalProperties: false,
|
|
217
|
+
});
|
|
218
|
+
const result = parseCFNSchema(schema);
|
|
219
|
+
expect(result.resource.deprecatedProperties).toEqual(["OldProp"]);
|
|
220
|
+
expect(result.resource.inferredDeprecations).toEqual(["OldProp"]);
|
|
221
|
+
});
|
|
222
|
+
|
|
223
|
+
test("a property both declared and matched by the regex counts as declared", () => {
|
|
224
|
+
// sampleBucketSchema declares AccessControl and its description says "legacy".
|
|
225
|
+
const result = parseCFNSchema(sampleBucketSchema);
|
|
226
|
+
expect(result.resource.deprecatedProperties).toContain("AccessControl");
|
|
227
|
+
expect(result.resource.inferredDeprecations).not.toContain("AccessControl");
|
|
228
|
+
});
|
|
229
|
+
|
|
230
|
+
test("inferredDeprecations is a subset of deprecatedProperties", () => {
|
|
231
|
+
const schema = JSON.stringify({
|
|
232
|
+
typeName: "AWS::Test::Mixed",
|
|
233
|
+
properties: {
|
|
234
|
+
DeclaredProp: { type: "string", description: "Plain prose." },
|
|
235
|
+
MinedProp: { type: "string", description: "This is a legacy property." },
|
|
236
|
+
},
|
|
237
|
+
deprecatedProperties: ["/properties/DeclaredProp"],
|
|
238
|
+
additionalProperties: false,
|
|
239
|
+
});
|
|
240
|
+
const result = parseCFNSchema(schema);
|
|
241
|
+
const all = new Set(result.resource.deprecatedProperties);
|
|
242
|
+
for (const p of result.resource.inferredDeprecations) {
|
|
243
|
+
expect(all.has(p)).toBe(true);
|
|
244
|
+
}
|
|
245
|
+
expect(result.resource.inferredDeprecations).toEqual(["MinedProp"]);
|
|
246
|
+
});
|
|
247
|
+
|
|
248
|
+
test("a description naming a sibling's deprecation lands in inferred, not declared", () => {
|
|
249
|
+
// Shape of AWS::Neptune::DBCluster.DBPort, where the description announces
|
|
250
|
+
// that the sibling `Port` is going away and DBPort is the replacement.
|
|
251
|
+
const schema = JSON.stringify({
|
|
252
|
+
typeName: "AWS::Test::Sibling",
|
|
253
|
+
properties: {
|
|
254
|
+
DBPort: {
|
|
255
|
+
type: "integer",
|
|
256
|
+
description: "The port. Note: `Port` will soon be deprecated. Rename it to `DBPort`.",
|
|
257
|
+
},
|
|
258
|
+
},
|
|
259
|
+
additionalProperties: false,
|
|
260
|
+
});
|
|
261
|
+
const result = parseCFNSchema(schema);
|
|
262
|
+
expect(result.resource.inferredDeprecations).toEqual(["DBPort"]);
|
|
263
|
+
expect(result.resource.deprecatedProperties).toEqual(["DBPort"]);
|
|
190
264
|
});
|
|
191
265
|
|
|
192
266
|
// --- Tagging metadata ---
|
package/src/spec/parse.ts
CHANGED
|
@@ -30,6 +30,13 @@ export type { PropertyConstraints } from "@intentius/chant/codegen/json-schema";
|
|
|
30
30
|
*/
|
|
31
31
|
const MAX_PROPERTY_TYPE_DEPTH = 3;
|
|
32
32
|
|
|
33
|
+
/**
|
|
34
|
+
* Prose signals that a property description is describing a deprecation.
|
|
35
|
+
* A hit is a guess, not a reading — see `ParsedResource.inferredDeprecations`.
|
|
36
|
+
*/
|
|
37
|
+
const DEPRECATION_RE =
|
|
38
|
+
/\bdeprecated\b|\blegacy\b|no longer (available|recommended|used|supported)|is not recommended|has been discontinued/i;
|
|
39
|
+
|
|
33
40
|
export interface ParsedProperty {
|
|
34
41
|
name: string;
|
|
35
42
|
tsType: string;
|
|
@@ -63,6 +70,13 @@ export interface ParsedResource {
|
|
|
63
70
|
writeOnly: string[];
|
|
64
71
|
primaryIdentifier: string[];
|
|
65
72
|
deprecatedProperties: string[];
|
|
73
|
+
/**
|
|
74
|
+
* The subset of `deprecatedProperties` that no upstream declaration backs.
|
|
75
|
+
* These names come from {@link DEPRECATION_RE} matching the property
|
|
76
|
+
* description, so they are an inference rather than a reading. Kept apart so
|
|
77
|
+
* a consumer can tell the two apart and calibrate what it says (#1701).
|
|
78
|
+
*/
|
|
79
|
+
inferredDeprecations: string[];
|
|
66
80
|
conditionalCreateOnly: string[];
|
|
67
81
|
replacementStrategy?: "delete_then_create" | "create_then_delete";
|
|
68
82
|
tagging?: { taggable: boolean; tagOnCreate: boolean; tagUpdatable: boolean };
|
|
@@ -160,19 +174,24 @@ export function parseCFNSchema(data: string | Buffer): SchemaParseResult {
|
|
|
160
174
|
}
|
|
161
175
|
}
|
|
162
176
|
|
|
163
|
-
// --- Deprecated properties:
|
|
177
|
+
// --- Deprecated properties: declared upstream, plus description-mined ---
|
|
178
|
+
// The declared half is what the Registry schema states. The mined half is a
|
|
179
|
+
// regex over English prose and is recorded separately (#1701): a description
|
|
180
|
+
// can mention the deprecation of a sibling property, of an enum value, or of
|
|
181
|
+
// something the property merely configures.
|
|
164
182
|
const deprecatedSet = new Set<string>(
|
|
165
183
|
stripPointerPaths(schema.deprecatedProperties ?? []),
|
|
166
184
|
);
|
|
167
|
-
|
|
168
|
-
const DEPRECATION_RE = /\bdeprecated\b|\blegacy\b|no longer (available|recommended|used|supported)|is not recommended|has been discontinued/i;
|
|
185
|
+
const inferredDeprecations: string[] = [];
|
|
169
186
|
|
|
170
187
|
// Mine top-level property descriptions
|
|
171
188
|
if (schema.properties) {
|
|
172
189
|
for (const [name, prop] of Object.entries(schema.properties)) {
|
|
173
|
-
if (prop.description
|
|
174
|
-
|
|
175
|
-
|
|
190
|
+
if (!prop.description || !DEPRECATION_RE.test(prop.description)) continue;
|
|
191
|
+
// A hit upstream already declares adds nothing, and is not an inference.
|
|
192
|
+
if (deprecatedSet.has(name)) continue;
|
|
193
|
+
deprecatedSet.add(name);
|
|
194
|
+
inferredDeprecations.push(name);
|
|
176
195
|
}
|
|
177
196
|
}
|
|
178
197
|
|
|
@@ -211,6 +230,7 @@ export function parseCFNSchema(data: string | Buffer): SchemaParseResult {
|
|
|
211
230
|
writeOnly: stripPointerPaths(schema.writeOnlyProperties ?? []),
|
|
212
231
|
primaryIdentifier: stripPointerPaths(schema.primaryIdentifier ?? []),
|
|
213
232
|
deprecatedProperties: [...deprecatedSet],
|
|
233
|
+
inferredDeprecations,
|
|
214
234
|
conditionalCreateOnly: stripPointerPaths(schema.conditionalCreateOnlyProperties ?? []),
|
|
215
235
|
...(replacementStrategy && { replacementStrategy }),
|
|
216
236
|
...(tagging && { tagging }),
|