@intentius/chant-lexicon-aws 0.50.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/disruption.d.ts +26 -0
- package/dist/disruption.d.ts.map +1 -0
- package/dist/integrity.json +2 -2
- package/dist/manifest.json +1 -1
- package/dist/plugin.d.ts.map +1 -1
- package/package.json +2 -2
- package/src/disruption.test.ts +198 -0
- package/src/disruption.ts +154 -0
- package/src/lsp/hover.ts +2 -5
- package/src/plugin.ts +16 -1
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
import type { DisruptionQuery, DisruptionVerdict } from "@intentius/chant/lexicon";
|
|
2
|
+
/** The slice of a `lexicon-aws.json` entry that carries replacement semantics. */
|
|
3
|
+
export interface DisruptionSpec {
|
|
4
|
+
resourceType: string;
|
|
5
|
+
kind: string;
|
|
6
|
+
/** Read-only attributes (TS key → CF attr name). An output changing is a symptom, never a cause. */
|
|
7
|
+
attrs?: Record<string, string>;
|
|
8
|
+
createOnly?: string[];
|
|
9
|
+
conditionalCreateOnly?: string[];
|
|
10
|
+
replacementStrategy?: "delete_then_create" | "create_then_delete";
|
|
11
|
+
}
|
|
12
|
+
/**
|
|
13
|
+
* Classify one pending update against the type's compiled schema. Pure — the
|
|
14
|
+
* spec table is passed in, so this is testable without generated artifacts.
|
|
15
|
+
*/
|
|
16
|
+
export declare function classifyAwsChange(query: DisruptionQuery, specs: Map<string, DisruptionSpec>): DisruptionVerdict;
|
|
17
|
+
/**
|
|
18
|
+
* `LexiconPlugin.classifyDisruption` for aws. Loads the compiled registry once
|
|
19
|
+
* and answers every query from it. A registry that cannot be read leaves every
|
|
20
|
+
* change `unknown` — the honest answer, and the one the contract requires.
|
|
21
|
+
*/
|
|
22
|
+
export declare function awsDisruption(options: {
|
|
23
|
+
environment: string;
|
|
24
|
+
changes: DisruptionQuery[];
|
|
25
|
+
}): Record<string, DisruptionVerdict>;
|
|
26
|
+
//# sourceMappingURL=disruption.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"disruption.d.ts","sourceRoot":"","sources":["../src/disruption.ts"],"names":[],"mappings":"AAoBA,OAAO,KAAK,EAAE,eAAe,EAAE,iBAAiB,EAAE,MAAM,0BAA0B,CAAC;AAInF,kFAAkF;AAClF,MAAM,WAAW,cAAc;IAC7B,YAAY,EAAE,MAAM,CAAC;IACrB,IAAI,EAAE,MAAM,CAAC;IACb,oGAAoG;IACpG,KAAK,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;IAC/B,UAAU,CAAC,EAAE,MAAM,EAAE,CAAC;IACtB,qBAAqB,CAAC,EAAE,MAAM,EAAE,CAAC;IACjC,mBAAmB,CAAC,EAAE,oBAAoB,GAAG,oBAAoB,CAAC;CACnE;AAED;;;GAGG;AACH,wBAAgB,iBAAiB,CAC/B,KAAK,EAAE,eAAe,EACtB,KAAK,EAAE,GAAG,CAAC,MAAM,EAAE,cAAc,CAAC,GACjC,iBAAiB,CAuDnB;AAED;;;;GAIG;AACH,wBAAgB,aAAa,CAAC,OAAO,EAAE;IACrC,WAAW,EAAE,MAAM,CAAC;IACpB,OAAO,EAAE,eAAe,EAAE,CAAC;CAC5B,GAAG,MAAM,CAAC,MAAM,EAAE,iBAAiB,CAAC,CAOpC"}
|
package/dist/integrity.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"algorithm": "sha256",
|
|
3
3
|
"artifacts": {
|
|
4
|
-
"manifest.json": "
|
|
4
|
+
"manifest.json": "0c488f36f907e56798a03315c3dec265068b2db4233aa839f03db166e2c17b54",
|
|
5
5
|
"meta.json": "e7c751a09a871dc5a16e9aeea0b0a149513a7a2a051c923e9a5acf193dc2aef8",
|
|
6
6
|
"types/index.d.ts": "6d6b37bd14a9b40fb3ff5c66e1cb49be1ae1032de47a4e167cf5f411c943b7f5",
|
|
7
7
|
"rules/hardcoded-region.ts": "5a0eaf7ab391231fe6cd51426ece29539cb4b36f31c8dd060956638fed55722a",
|
|
@@ -72,5 +72,5 @@
|
|
|
72
72
|
"skills/chant-aws-eks.md": "8789255709ff004ad0a875fd5999edcdc66fc6e33d710db058d9f42703bcfdfe",
|
|
73
73
|
"skills/chant-aws-carve-terraform.md": "0ae4376590db47979be144d3ac6cdcfd56c9640d0536421aacf7330459d38e52"
|
|
74
74
|
},
|
|
75
|
-
"composite": "
|
|
75
|
+
"composite": "e03cda0c71c249a0fad0b4c921913fe533bf0f3ddcfc9c4758a0f8d66922fb2d"
|
|
76
76
|
}
|
package/dist/manifest.json
CHANGED
package/dist/plugin.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"plugin.d.ts","sourceRoot":"","sources":["../src/plugin.ts"],"names":[],"mappings":"AAEA,OAAO,KAAK,EAAE,aAAa,
|
|
1
|
+
{"version":3,"file":"plugin.d.ts","sourceRoot":"","sources":["../src/plugin.ts"],"names":[],"mappings":"AAEA,OAAO,KAAK,EAAE,aAAa,EAAoN,MAAM,0BAA0B,CAAC;AAsChR;yEACyE;AACzE,OAAO,EAAE,iBAAiB,EAAE,MAAM,gBAAgB,CAAC;AAiBnD,eAAO,MAAM,SAAS,EAAE,aAg+BvB,CAAC"}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@intentius/chant-lexicon-aws",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.51.0",
|
|
4
4
|
"description": "AWS CloudFormation lexicon for chant — declarative IaC in TypeScript",
|
|
5
5
|
"license": "Apache-2.0",
|
|
6
6
|
"homepage": "https://intentius.io/chant",
|
|
@@ -80,7 +80,7 @@
|
|
|
80
80
|
"typescript": "^5.9.3"
|
|
81
81
|
},
|
|
82
82
|
"peerDependencies": {
|
|
83
|
-
"@intentius/chant": "^0.
|
|
83
|
+
"@intentius/chant": "^0.51.0",
|
|
84
84
|
"typescript": "^5.9.3"
|
|
85
85
|
}
|
|
86
86
|
}
|
|
@@ -0,0 +1,198 @@
|
|
|
1
|
+
import { describe, test, expect } from "vitest";
|
|
2
|
+
import { awsDisruption, classifyAwsChange, type DisruptionSpec } from "./disruption";
|
|
3
|
+
import type { DisruptionQuery } from "@intentius/chant/lexicon";
|
|
4
|
+
|
|
5
|
+
const specs = new Map<string, DisruptionSpec>([
|
|
6
|
+
[
|
|
7
|
+
"AWS::EC2::Instance",
|
|
8
|
+
{
|
|
9
|
+
resourceType: "AWS::EC2::Instance",
|
|
10
|
+
kind: "resource",
|
|
11
|
+
attrs: { InstanceId: "InstanceId", PublicIp: "PublicIp" },
|
|
12
|
+
createOnly: ["ImageId", "SubnetId", "KeyName"],
|
|
13
|
+
conditionalCreateOnly: ["InstanceType"],
|
|
14
|
+
},
|
|
15
|
+
],
|
|
16
|
+
[
|
|
17
|
+
"AWS::RDS::DBInstance",
|
|
18
|
+
{
|
|
19
|
+
resourceType: "AWS::RDS::DBInstance",
|
|
20
|
+
kind: "resource",
|
|
21
|
+
createOnly: ["DBInstanceIdentifier"],
|
|
22
|
+
replacementStrategy: "delete_then_create",
|
|
23
|
+
},
|
|
24
|
+
],
|
|
25
|
+
[
|
|
26
|
+
"AWS::S3::BucketPolicy",
|
|
27
|
+
{ resourceType: "AWS::S3::BucketPolicy", kind: "resource" },
|
|
28
|
+
],
|
|
29
|
+
[
|
|
30
|
+
"AWS::EC2::Instance.BlockDeviceMapping",
|
|
31
|
+
{ resourceType: "AWS::EC2::Instance.BlockDeviceMapping", kind: "property" },
|
|
32
|
+
],
|
|
33
|
+
]);
|
|
34
|
+
|
|
35
|
+
function query(type: string | undefined, paths: string[]): DisruptionQuery {
|
|
36
|
+
return {
|
|
37
|
+
name: "thing",
|
|
38
|
+
...(type ? { type } : {}),
|
|
39
|
+
deltas: paths.map((path) => ({ path, oldValue: "a", newValue: "b" })),
|
|
40
|
+
};
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
describe("classifyAwsChange (#1665)", () => {
|
|
44
|
+
test("a create-only property is a replace, and says which one", () => {
|
|
45
|
+
const v = classifyAwsChange(query("AWS::EC2::Instance", ["attributes.ImageId"]), specs);
|
|
46
|
+
expect(v.disruption).toBe("replace");
|
|
47
|
+
expect(v.because).toEqual(["attributes.ImageId"]);
|
|
48
|
+
expect(v.detail).toContain("ImageId is create-only");
|
|
49
|
+
});
|
|
50
|
+
|
|
51
|
+
// `delete_then_create` is the destructive form: the old resource is gone
|
|
52
|
+
// before the new one exists.
|
|
53
|
+
test("delete_then_create replacement is a destroy", () => {
|
|
54
|
+
const v = classifyAwsChange(
|
|
55
|
+
query("AWS::RDS::DBInstance", ["attributes.DBInstanceIdentifier"]),
|
|
56
|
+
specs,
|
|
57
|
+
);
|
|
58
|
+
expect(v.disruption).toBe("destroy");
|
|
59
|
+
expect(v.detail).toContain("deleting first");
|
|
60
|
+
});
|
|
61
|
+
|
|
62
|
+
test("a mutable property is in-place", () => {
|
|
63
|
+
const v = classifyAwsChange(query("AWS::EC2::Instance", ["attributes.Monitoring"]), specs);
|
|
64
|
+
expect(v.disruption).toBe("in-place");
|
|
65
|
+
expect(v.detail).toContain("no create-only property of AWS::EC2::Instance changed");
|
|
66
|
+
});
|
|
67
|
+
|
|
68
|
+
test("replacement wins over an in-place property changing alongside it", () => {
|
|
69
|
+
const v = classifyAwsChange(
|
|
70
|
+
query("AWS::EC2::Instance", ["attributes.Monitoring", "attributes.SubnetId"]),
|
|
71
|
+
specs,
|
|
72
|
+
);
|
|
73
|
+
expect(v.disruption).toBe("replace");
|
|
74
|
+
expect(v.because).toEqual(["attributes.SubnetId"]);
|
|
75
|
+
});
|
|
76
|
+
|
|
77
|
+
// The schema says "depends on the value", which is not an answer. Reporting
|
|
78
|
+
// that as in-place would be the exact failure the `unknown` level exists for.
|
|
79
|
+
test("a conditionally create-only property is unknown, not in-place", () => {
|
|
80
|
+
const v = classifyAwsChange(
|
|
81
|
+
query("AWS::EC2::Instance", ["attributes.InstanceType"]),
|
|
82
|
+
specs,
|
|
83
|
+
);
|
|
84
|
+
expect(v.disruption).toBe("unknown");
|
|
85
|
+
expect(v.because).toEqual(["attributes.InstanceType"]);
|
|
86
|
+
expect(v.detail).toContain("conditionally create-only");
|
|
87
|
+
});
|
|
88
|
+
|
|
89
|
+
test("a type with no schema on record is unknown", () => {
|
|
90
|
+
const v = classifyAwsChange(query("AWS::Nowhere::Thing", ["attributes.Anything"]), specs);
|
|
91
|
+
expect(v.disruption).toBe("unknown");
|
|
92
|
+
expect(v.detail).toContain("no CloudFormation registry schema on record");
|
|
93
|
+
});
|
|
94
|
+
|
|
95
|
+
test("a property-type entry is not a resource, so it is unknown", () => {
|
|
96
|
+
const v = classifyAwsChange(
|
|
97
|
+
query("AWS::EC2::Instance.BlockDeviceMapping", ["attributes.Ebs"]),
|
|
98
|
+
specs,
|
|
99
|
+
);
|
|
100
|
+
expect(v.disruption).toBe("unknown");
|
|
101
|
+
});
|
|
102
|
+
|
|
103
|
+
test("an entry with no observed type is unknown", () => {
|
|
104
|
+
const v = classifyAwsChange(query(undefined, ["attributes.ImageId"]), specs);
|
|
105
|
+
expect(v.disruption).toBe("unknown");
|
|
106
|
+
expect(v.detail).toContain("no resource type");
|
|
107
|
+
});
|
|
108
|
+
|
|
109
|
+
// A read-only attribute changing is a symptom of something else, never the
|
|
110
|
+
// cause of a replacement — a re-created instance reports a new PublicIp.
|
|
111
|
+
test("read-only attributes are not evidence of anything", () => {
|
|
112
|
+
const v = classifyAwsChange(query("AWS::EC2::Instance", ["attributes.PublicIp"]), specs);
|
|
113
|
+
expect(v.disruption).toBe("in-place");
|
|
114
|
+
expect(v.detail).toContain("only status, identity, or read-only attributes changed");
|
|
115
|
+
});
|
|
116
|
+
|
|
117
|
+
test("core's own observation fields are not properties", () => {
|
|
118
|
+
const v = classifyAwsChange(
|
|
119
|
+
query("AWS::EC2::Instance", ["status", "lastUpdated", "physicalId"]),
|
|
120
|
+
specs,
|
|
121
|
+
);
|
|
122
|
+
expect(v.disruption).toBe("in-place");
|
|
123
|
+
expect(v.detail).toContain("only status, identity, or read-only attributes changed");
|
|
124
|
+
});
|
|
125
|
+
|
|
126
|
+
test("a type whose schema declares no create-only property is in-place", () => {
|
|
127
|
+
const v = classifyAwsChange(query("AWS::S3::BucketPolicy", ["attributes.PolicyDocument"]), specs);
|
|
128
|
+
expect(v.disruption).toBe("in-place");
|
|
129
|
+
});
|
|
130
|
+
|
|
131
|
+
// createOnlyProperties can point at a nested pointer path (`Config/Engine`);
|
|
132
|
+
// the observation reports the top-level property.
|
|
133
|
+
test("a nested create-only pointer matches on its head segment", () => {
|
|
134
|
+
const nested = new Map<string, DisruptionSpec>([
|
|
135
|
+
[
|
|
136
|
+
"AWS::Fake::Thing",
|
|
137
|
+
{ resourceType: "AWS::Fake::Thing", kind: "resource", createOnly: ["Config/Engine"] },
|
|
138
|
+
],
|
|
139
|
+
]);
|
|
140
|
+
const v = classifyAwsChange(query("AWS::Fake::Thing", ["attributes.Config"]), nested);
|
|
141
|
+
expect(v.disruption).toBe("replace");
|
|
142
|
+
});
|
|
143
|
+
|
|
144
|
+
test("duplicate hits name the property once but keep every path", () => {
|
|
145
|
+
const v = classifyAwsChange(
|
|
146
|
+
query("AWS::EC2::Instance", ["attributes.ImageId", "attributes.KeyName"]),
|
|
147
|
+
specs,
|
|
148
|
+
);
|
|
149
|
+
expect(v.because).toEqual(["attributes.ImageId", "attributes.KeyName"]);
|
|
150
|
+
expect(v.detail).toContain("ImageId, KeyName");
|
|
151
|
+
});
|
|
152
|
+
});
|
|
153
|
+
|
|
154
|
+
// The plugin method against the registry the codegen actually compiled — the
|
|
155
|
+
// evidence that the verdicts come from CloudFormation's own schema and not
|
|
156
|
+
// from a table in this file.
|
|
157
|
+
describe("awsDisruption over the generated registry", () => {
|
|
158
|
+
const change = (name: string, type: string, paths: string[]): DisruptionQuery => ({
|
|
159
|
+
name,
|
|
160
|
+
type,
|
|
161
|
+
deltas: paths.map((path) => ({ path, oldValue: "a", newValue: "b" })),
|
|
162
|
+
});
|
|
163
|
+
|
|
164
|
+
test("classifies a whole batch off the compiled schema", () => {
|
|
165
|
+
const out = awsDisruption({
|
|
166
|
+
environment: "prod",
|
|
167
|
+
changes: [
|
|
168
|
+
// createOnlyProperties
|
|
169
|
+
change("web", "AWS::EC2::Instance", ["attributes.ImageId"]),
|
|
170
|
+
// conditionalCreateOnlyProperties — the schema says "it depends"
|
|
171
|
+
change("sized", "AWS::EC2::Instance", ["attributes.InstanceType"]),
|
|
172
|
+
// not create-only for this type
|
|
173
|
+
change("mon", "AWS::EC2::Instance", ["attributes.Monitoring"]),
|
|
174
|
+
// no schema on record
|
|
175
|
+
change("nope", "AWS::Nowhere::Thing", ["attributes.Whatever"]),
|
|
176
|
+
],
|
|
177
|
+
});
|
|
178
|
+
|
|
179
|
+
expect(out.web.disruption).toBe("replace");
|
|
180
|
+
expect(out.web.because).toEqual(["attributes.ImageId"]);
|
|
181
|
+
expect(out.sized.disruption).toBe("unknown");
|
|
182
|
+
expect(out.sized.detail).toContain("conditionally create-only");
|
|
183
|
+
expect(out.mon.disruption).toBe("in-place");
|
|
184
|
+
expect(out.nope.disruption).toBe("unknown");
|
|
185
|
+
});
|
|
186
|
+
|
|
187
|
+
test("every query gets a verdict, so nothing silently drops out of the plan", () => {
|
|
188
|
+
const changes = [
|
|
189
|
+
change("a", "AWS::S3::Bucket", ["attributes.BucketName"]),
|
|
190
|
+
change("b", "AWS::IAM::Role", ["attributes.RoleName"]),
|
|
191
|
+
];
|
|
192
|
+
const out = awsDisruption({ environment: "prod", changes });
|
|
193
|
+
expect(Object.keys(out).sort()).toEqual(["a", "b"]);
|
|
194
|
+
// BucketName and RoleName are both create-only in the Registry schema.
|
|
195
|
+
expect(out.a.disruption).toBe("replace");
|
|
196
|
+
expect(out.b.disruption).toBe("replace");
|
|
197
|
+
});
|
|
198
|
+
});
|
|
@@ -0,0 +1,154 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Replacement semantics for a pending update (#1665).
|
|
3
|
+
*
|
|
4
|
+
* CloudFormation already knows this and says so in the Registry schema:
|
|
5
|
+
* `createOnlyProperties` is the type's complete list of properties that cannot
|
|
6
|
+
* be updated without building a new resource, and `replacementStrategy` says
|
|
7
|
+
* whether the new one is created before the old is removed or after. The
|
|
8
|
+
* codegen compiles both into `lexicon-aws.json` alongside the types, which is
|
|
9
|
+
* why this is a lookup rather than a rule anyone maintains by hand.
|
|
10
|
+
*
|
|
11
|
+
* `conditionalCreateOnlyProperties` is the interesting case: the schema is
|
|
12
|
+
* saying "depends on the value", which is not an answer, so it degrades to
|
|
13
|
+
* `unknown` with the property named. That is the whole point of the contract
|
|
14
|
+
* having an `unknown` — a maybe reported as `in-place` is worse than no
|
|
15
|
+
* report.
|
|
16
|
+
*
|
|
17
|
+
* `rolling` is not produced here. Nothing in the Registry schema expresses a
|
|
18
|
+
* workload roll; that verdict belongs to a lexicon whose spec does.
|
|
19
|
+
*/
|
|
20
|
+
import { createRequire } from "module";
|
|
21
|
+
import type { DisruptionQuery, DisruptionVerdict } from "@intentius/chant/lexicon";
|
|
22
|
+
|
|
23
|
+
const require = createRequire(import.meta.url);
|
|
24
|
+
|
|
25
|
+
/** The slice of a `lexicon-aws.json` entry that carries replacement semantics. */
|
|
26
|
+
export interface DisruptionSpec {
|
|
27
|
+
resourceType: string;
|
|
28
|
+
kind: string;
|
|
29
|
+
/** Read-only attributes (TS key → CF attr name). An output changing is a symptom, never a cause. */
|
|
30
|
+
attrs?: Record<string, string>;
|
|
31
|
+
createOnly?: string[];
|
|
32
|
+
conditionalCreateOnly?: string[];
|
|
33
|
+
replacementStrategy?: "delete_then_create" | "create_then_delete";
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
/**
|
|
37
|
+
* Classify one pending update against the type's compiled schema. Pure — the
|
|
38
|
+
* spec table is passed in, so this is testable without generated artifacts.
|
|
39
|
+
*/
|
|
40
|
+
export function classifyAwsChange(
|
|
41
|
+
query: DisruptionQuery,
|
|
42
|
+
specs: Map<string, DisruptionSpec>,
|
|
43
|
+
): DisruptionVerdict {
|
|
44
|
+
const spec = query.type ? specs.get(query.type) : undefined;
|
|
45
|
+
if (!spec || spec.kind !== "resource") {
|
|
46
|
+
return {
|
|
47
|
+
disruption: "unknown",
|
|
48
|
+
detail: query.type
|
|
49
|
+
? `no CloudFormation registry schema on record for ${query.type}`
|
|
50
|
+
: "the observation reported no resource type",
|
|
51
|
+
};
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
const readOnly = readOnlyNames(spec);
|
|
55
|
+
const considered: Array<{ path: string; property: string }> = [];
|
|
56
|
+
for (const delta of query.deltas) {
|
|
57
|
+
const property = propertyName(delta.path);
|
|
58
|
+
if (!property || readOnly.has(property)) continue;
|
|
59
|
+
considered.push({ path: delta.path, property });
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
const createOnly = new Set(spec.createOnly?.map(headSegment));
|
|
63
|
+
const forced = considered.filter((c) => createOnly.has(c.property));
|
|
64
|
+
if (forced.length > 0) {
|
|
65
|
+
const names = [...new Set(forced.map((c) => c.property))];
|
|
66
|
+
const destroys = spec.replacementStrategy === "delete_then_create";
|
|
67
|
+
return {
|
|
68
|
+
disruption: destroys ? "destroy" : "replace",
|
|
69
|
+
because: forced.map((c) => c.path),
|
|
70
|
+
detail: destroys
|
|
71
|
+
? `${names.join(", ")} is create-only and ${spec.resourceType} replaces by deleting first — the resource is gone before the new one exists`
|
|
72
|
+
: `${names.join(", ")} is create-only — CloudFormation builds a new ${spec.resourceType} and removes the old one`,
|
|
73
|
+
};
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
const conditional = new Set(spec.conditionalCreateOnly?.map(headSegment));
|
|
77
|
+
const maybe = considered.filter((c) => conditional.has(c.property));
|
|
78
|
+
if (maybe.length > 0) {
|
|
79
|
+
const names = [...new Set(maybe.map((c) => c.property))];
|
|
80
|
+
return {
|
|
81
|
+
disruption: "unknown",
|
|
82
|
+
because: maybe.map((c) => c.path),
|
|
83
|
+
detail: `the schema marks ${names.join(", ")} conditionally create-only — whether this replaces depends on the value, and the schema does not say which way`,
|
|
84
|
+
};
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
if (considered.length === 0) {
|
|
88
|
+
return {
|
|
89
|
+
disruption: "in-place",
|
|
90
|
+
detail: "no configured property differs — only status, identity, or read-only attributes changed",
|
|
91
|
+
};
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
return {
|
|
95
|
+
disruption: "in-place",
|
|
96
|
+
detail: `no create-only property of ${spec.resourceType} changed`,
|
|
97
|
+
};
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
/**
|
|
101
|
+
* `LexiconPlugin.classifyDisruption` for aws. Loads the compiled registry once
|
|
102
|
+
* and answers every query from it. A registry that cannot be read leaves every
|
|
103
|
+
* change `unknown` — the honest answer, and the one the contract requires.
|
|
104
|
+
*/
|
|
105
|
+
export function awsDisruption(options: {
|
|
106
|
+
environment: string;
|
|
107
|
+
changes: DisruptionQuery[];
|
|
108
|
+
}): Record<string, DisruptionVerdict> {
|
|
109
|
+
const specs = registryByType();
|
|
110
|
+
const out: Record<string, DisruptionVerdict> = {};
|
|
111
|
+
for (const change of options.changes) {
|
|
112
|
+
out[change.name] = classifyAwsChange(change, specs);
|
|
113
|
+
}
|
|
114
|
+
return out;
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
let cached: Map<string, DisruptionSpec> | undefined;
|
|
118
|
+
|
|
119
|
+
function registryByType(): Map<string, DisruptionSpec> {
|
|
120
|
+
if (cached) return cached;
|
|
121
|
+
try {
|
|
122
|
+
const manifest = require("./generated/lexicon-aws.json") as Record<string, DisruptionSpec>;
|
|
123
|
+
cached = new Map(Object.values(manifest).map((e) => [e.resourceType, e]));
|
|
124
|
+
} catch {
|
|
125
|
+
cached = new Map();
|
|
126
|
+
}
|
|
127
|
+
return cached;
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
/** Every read-only attribute name the schema declares, top segment only. */
|
|
131
|
+
function readOnlyNames(spec: DisruptionSpec): Set<string> {
|
|
132
|
+
const names = new Set<string>();
|
|
133
|
+
for (const attr of Object.values(spec.attrs ?? {})) names.add(attr.split(".")[0]);
|
|
134
|
+
return names;
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
/**
|
|
138
|
+
* The property a core delta path names, or undefined when the path is not
|
|
139
|
+
* about a property at all. Core's own observation fields (`status`,
|
|
140
|
+
* `physicalId`, `lastUpdated`) sit at the root of the change and say nothing
|
|
141
|
+
* about configuration; only what is under `attributes.` was read off the
|
|
142
|
+
* resource.
|
|
143
|
+
*/
|
|
144
|
+
function propertyName(path: string): string | undefined {
|
|
145
|
+
const prefix = "attributes.";
|
|
146
|
+
if (!path.startsWith(prefix)) return undefined;
|
|
147
|
+
const rest = path.slice(prefix.length);
|
|
148
|
+
return rest ? headSegment(rest) : undefined;
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
/** The first segment of a JSON-pointer-ish property path (`Config/Engine` → `Config`). */
|
|
152
|
+
function headSegment(path: string): string {
|
|
153
|
+
return path.split(/[/.]/)[0];
|
|
154
|
+
}
|
package/src/lsp/hover.ts
CHANGED
|
@@ -62,11 +62,8 @@ function resourceHover(className: string, entry: LexiconEntry): HoverInfo | unde
|
|
|
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
|
-
|
|
67
|
-
// lexicon builds against the published core, so widen locally until then.
|
|
68
|
-
const withBasis = entry as LexiconEntry & { inferredDeprecations?: string[] };
|
|
69
|
-
const inferred = new Set(withBasis.inferredDeprecations ?? []);
|
|
65
|
+
// read as separate lines rather than one merged list.
|
|
66
|
+
const inferred = new Set(entry.inferredDeprecations ?? []);
|
|
70
67
|
const declared = (entry.deprecatedProperties ?? []).filter((p) => !inferred.has(p));
|
|
71
68
|
|
|
72
69
|
if (declared.length > 0) {
|
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
|