@intentius/chant-lexicon-aws 0.55.0 → 0.57.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/condition.d.ts +27 -0
- package/dist/condition.d.ts.map +1 -0
- package/dist/generated/index.d.ts +1 -1
- package/dist/generated/index.d.ts.map +1 -1
- package/dist/import/generator.d.ts +19 -0
- package/dist/import/generator.d.ts.map +1 -1
- package/dist/import/parser.d.ts +19 -0
- package/dist/import/parser.d.ts.map +1 -1
- package/dist/index.d.ts +3 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/integrity.json +4 -3
- package/dist/intrinsics.d.ts +77 -3
- package/dist/intrinsics.d.ts.map +1 -1
- 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/waw069.d.ts +15 -0
- package/dist/lint/post-synth/waw069.d.ts.map +1 -0
- package/dist/manifest.json +29 -1
- package/dist/okf/index.md +1 -0
- package/dist/okf/rules/WAW069.md +11 -0
- package/dist/plugin.d.ts.map +1 -1
- package/dist/rules/waw069.ts +0 -0
- package/dist/serializer.d.ts.map +1 -1
- package/dist/types/index.d.ts +3 -3
- package/package.json +2 -2
- package/src/codegen/generate-typescript.ts +3 -3
- package/src/codegen/generate.ts +1 -1
- package/src/condition.test.ts +129 -0
- package/src/condition.ts +40 -0
- package/src/generated/index.d.ts +3 -3
- package/src/generated/index.ts +1 -1
- package/src/import/generator.ts +131 -50
- package/src/import/parser.test.ts +96 -0
- package/src/import/parser.ts +86 -1
- package/src/import/roundtrip-fixtures.test.ts +50 -0
- package/src/index.ts +12 -0
- package/src/intrinsics.ts +130 -5
- package/src/lint/audit-catalog.ts +1 -0
- package/src/lint/post-synth/index.ts +2 -0
- package/src/lint/post-synth/waw069.test.ts +94 -0
- package/src/lint/post-synth/waw069.ts +0 -0
- package/src/plugin.test.ts +5 -1
- package/src/plugin.ts +4 -0
- package/src/serializer.ts +24 -3
- package/src/testdata/roundtrip/with-conditions.json +16 -0
package/src/intrinsics.ts
CHANGED
|
@@ -2,6 +2,25 @@ import { INTRINSIC_MARKER, resolveIntrinsicValue, isIntrinsic, type Intrinsic }
|
|
|
2
2
|
import { buildInterpolatedString, defaultInterpolationSerializer } from "@intentius/chant/intrinsic-interpolation";
|
|
3
3
|
import { type Declarable } from "@intentius/chant/declarable";
|
|
4
4
|
import { getLogicalName } from "@intentius/chant/utils";
|
|
5
|
+
import { isCondition, type Condition } from "./condition";
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* An operand allowed where CloudFormation expects a condition (#2068): a
|
|
9
|
+
* nested condition intrinsic (`Equals`/`And`/`Or`/`Not`), the `Condition`
|
|
10
|
+
* declarable itself, or a condition name string. The latter two both emit
|
|
11
|
+
* the `{ "Condition": "<name>" }` reference form.
|
|
12
|
+
*/
|
|
13
|
+
export type ConditionOperand = Intrinsic | Condition | string;
|
|
14
|
+
|
|
15
|
+
function resolveConditionOperand(operand: ConditionOperand): unknown {
|
|
16
|
+
if (typeof operand === "string") {
|
|
17
|
+
return { Condition: operand };
|
|
18
|
+
}
|
|
19
|
+
if (isCondition(operand)) {
|
|
20
|
+
return { Condition: getLogicalName(operand) };
|
|
21
|
+
}
|
|
22
|
+
return resolveIntrinsicValue(operand);
|
|
23
|
+
}
|
|
5
24
|
|
|
6
25
|
/**
|
|
7
26
|
* Fn::Sub intrinsic function implementation
|
|
@@ -99,29 +118,135 @@ export function GetAtt(logicalName: string, attribute: string): GetAttIntrinsic
|
|
|
99
118
|
*/
|
|
100
119
|
export class IfIntrinsic implements Intrinsic {
|
|
101
120
|
readonly [INTRINSIC_MARKER] = true as const;
|
|
102
|
-
private conditionName: string;
|
|
121
|
+
private conditionName: string | Condition;
|
|
103
122
|
private valueIfTrue: unknown;
|
|
104
123
|
private valueIfFalse: unknown;
|
|
105
124
|
|
|
106
|
-
constructor(conditionName: string, valueIfTrue: unknown, valueIfFalse: unknown) {
|
|
125
|
+
constructor(conditionName: string | Condition, valueIfTrue: unknown, valueIfFalse: unknown) {
|
|
107
126
|
this.conditionName = conditionName;
|
|
108
127
|
this.valueIfTrue = valueIfTrue;
|
|
109
128
|
this.valueIfFalse = valueIfFalse;
|
|
110
129
|
}
|
|
111
130
|
|
|
112
131
|
toJSON(): { "Fn::If": [string, unknown, unknown] } {
|
|
113
|
-
|
|
132
|
+
const name = typeof this.conditionName === "string" ? this.conditionName : getLogicalName(this.conditionName);
|
|
133
|
+
return { "Fn::If": [name, resolveIntrinsicValue(this.valueIfTrue), resolveIntrinsicValue(this.valueIfFalse)] };
|
|
114
134
|
}
|
|
115
135
|
}
|
|
116
136
|
|
|
117
137
|
|
|
118
138
|
/**
|
|
119
|
-
* Create an If intrinsic
|
|
139
|
+
* Create an If intrinsic. The condition is a name string, or the `Condition`
|
|
140
|
+
* declarable itself (resolved to its logical name at serialization, #2068).
|
|
120
141
|
*/
|
|
121
|
-
export function If(conditionName: string, valueIfTrue: unknown, valueIfFalse: unknown): IfIntrinsic {
|
|
142
|
+
export function If(conditionName: string | Condition, valueIfTrue: unknown, valueIfFalse: unknown): IfIntrinsic {
|
|
122
143
|
return new IfIntrinsic(conditionName, valueIfTrue, valueIfFalse);
|
|
123
144
|
}
|
|
124
145
|
|
|
146
|
+
/**
|
|
147
|
+
* Fn::Equals condition intrinsic (#2068).
|
|
148
|
+
* Compares two values; each may be a literal or a value intrinsic (Ref etc.).
|
|
149
|
+
*/
|
|
150
|
+
export class EqualsIntrinsic implements Intrinsic {
|
|
151
|
+
readonly [INTRINSIC_MARKER] = true as const;
|
|
152
|
+
private left: unknown;
|
|
153
|
+
private right: unknown;
|
|
154
|
+
|
|
155
|
+
constructor(left: unknown, right: unknown) {
|
|
156
|
+
this.left = left;
|
|
157
|
+
this.right = right;
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
toJSON(): { "Fn::Equals": [unknown, unknown] } {
|
|
161
|
+
return { "Fn::Equals": [resolveIntrinsicValue(this.left), resolveIntrinsicValue(this.right)] };
|
|
162
|
+
}
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
/**
|
|
166
|
+
* Create an Equals condition intrinsic: `Equals(Ref(cutover), "true")`.
|
|
167
|
+
*/
|
|
168
|
+
export function Equals(left: unknown, right: unknown): EqualsIntrinsic {
|
|
169
|
+
return new EqualsIntrinsic(left, right);
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
/**
|
|
173
|
+
* Fn::And condition intrinsic (#2068).
|
|
174
|
+
*/
|
|
175
|
+
export class AndIntrinsic implements Intrinsic {
|
|
176
|
+
readonly [INTRINSIC_MARKER] = true as const;
|
|
177
|
+
private conditions: ConditionOperand[];
|
|
178
|
+
|
|
179
|
+
constructor(conditions: ConditionOperand[]) {
|
|
180
|
+
this.conditions = conditions;
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
toJSON(): { "Fn::And": unknown[] } {
|
|
184
|
+
return { "Fn::And": this.conditions.map(resolveConditionOperand) };
|
|
185
|
+
}
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
/**
|
|
189
|
+
* Create an And condition intrinsic over 2–10 operands, each a nested
|
|
190
|
+
* condition intrinsic, a `Condition` declarable, or a condition name.
|
|
191
|
+
*/
|
|
192
|
+
export function And(...conditions: ConditionOperand[]): AndIntrinsic {
|
|
193
|
+
if (conditions.length < 2 || conditions.length > 10) {
|
|
194
|
+
throw new Error("And(...conditions): Fn::And takes between 2 and 10 conditions");
|
|
195
|
+
}
|
|
196
|
+
return new AndIntrinsic(conditions);
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
/**
|
|
200
|
+
* Fn::Or condition intrinsic (#2068).
|
|
201
|
+
*/
|
|
202
|
+
export class OrIntrinsic implements Intrinsic {
|
|
203
|
+
readonly [INTRINSIC_MARKER] = true as const;
|
|
204
|
+
private conditions: ConditionOperand[];
|
|
205
|
+
|
|
206
|
+
constructor(conditions: ConditionOperand[]) {
|
|
207
|
+
this.conditions = conditions;
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
toJSON(): { "Fn::Or": unknown[] } {
|
|
211
|
+
return { "Fn::Or": this.conditions.map(resolveConditionOperand) };
|
|
212
|
+
}
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
/**
|
|
216
|
+
* Create an Or condition intrinsic over 2–10 operands, each a nested
|
|
217
|
+
* condition intrinsic, a `Condition` declarable, or a condition name.
|
|
218
|
+
*/
|
|
219
|
+
export function Or(...conditions: ConditionOperand[]): OrIntrinsic {
|
|
220
|
+
if (conditions.length < 2 || conditions.length > 10) {
|
|
221
|
+
throw new Error("Or(...conditions): Fn::Or takes between 2 and 10 conditions");
|
|
222
|
+
}
|
|
223
|
+
return new OrIntrinsic(conditions);
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
/**
|
|
227
|
+
* Fn::Not condition intrinsic (#2068).
|
|
228
|
+
*/
|
|
229
|
+
export class NotIntrinsic implements Intrinsic {
|
|
230
|
+
readonly [INTRINSIC_MARKER] = true as const;
|
|
231
|
+
private condition: ConditionOperand;
|
|
232
|
+
|
|
233
|
+
constructor(condition: ConditionOperand) {
|
|
234
|
+
this.condition = condition;
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
toJSON(): { "Fn::Not": [unknown] } {
|
|
238
|
+
return { "Fn::Not": [resolveConditionOperand(this.condition)] };
|
|
239
|
+
}
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
/**
|
|
243
|
+
* Create a Not condition intrinsic over a nested condition intrinsic, a
|
|
244
|
+
* `Condition` declarable, or a condition name.
|
|
245
|
+
*/
|
|
246
|
+
export function Not(condition: ConditionOperand): NotIntrinsic {
|
|
247
|
+
return new NotIntrinsic(condition);
|
|
248
|
+
}
|
|
249
|
+
|
|
125
250
|
/**
|
|
126
251
|
* Fn::Join intrinsic function
|
|
127
252
|
* Joins values with a delimiter
|
|
@@ -88,4 +88,5 @@ export const awsAuditCatalog: Record<string, RuleMeta> = {
|
|
|
88
88
|
WAW066: auditRule("WAW066", "merge-worthy", "guidance", "Private subnet's route table has no working default route", "Add a 0.0.0.0/0 route to a NAT gateway/Transit Gateway that exists in the template.", { category: "correctness" }),
|
|
89
89
|
WAW067: auditRule("WAW067", "report-only", "guidance", "Single-AZ NAT gateway serves multi-AZ private subnets", "Add one NAT gateway per Availability Zone and point each AZ's subnets at its own.", { category: "best-practice" }),
|
|
90
90
|
WAW068: auditRule("WAW068", "report-only", "guidance", "VPN Gateway or Transit Gateway has only one attached VPN Connection", "Attach a second VPNConnection (ideally to a separate Customer Gateway) for redundancy.", { category: "best-practice" }),
|
|
91
|
+
WAW069: auditRule("WAW069", "merge-worthy", "guidance", "Template references a condition it never declares", "Declare the condition in the Conditions section (new Condition(...)), or fix the referenced name.", { category: "correctness" }),
|
|
91
92
|
};
|
|
@@ -60,6 +60,7 @@ import { waw065 } from "./waw065";
|
|
|
60
60
|
import { waw066 } from "./waw066";
|
|
61
61
|
import { waw067 } from "./waw067";
|
|
62
62
|
import { waw068 } from "./waw068";
|
|
63
|
+
import { waw069 } from "./waw069";
|
|
63
64
|
|
|
64
65
|
export const postSynthChecks: PostSynthCheck[] = [
|
|
65
66
|
cor020,
|
|
@@ -122,4 +123,5 @@ export const postSynthChecks: PostSynthCheck[] = [
|
|
|
122
123
|
waw066,
|
|
123
124
|
waw067,
|
|
124
125
|
waw068,
|
|
126
|
+
waw069,
|
|
125
127
|
];
|
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
import { describe, test, expect } from "vitest";
|
|
2
|
+
import { createPostSynthContext } from "@intentius/chant-test-utils";
|
|
3
|
+
import { waw069, checkUndeclaredConditions } from "./waw069";
|
|
4
|
+
|
|
5
|
+
function makeCtx(template: object) {
|
|
6
|
+
return createPostSynthContext({ aws: template });
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
describe("WAW069: Template references an undeclared condition", () => {
|
|
10
|
+
test("check metadata", () => {
|
|
11
|
+
expect(waw069.id).toBe("WAW069");
|
|
12
|
+
expect(waw069.description).toContain("condition");
|
|
13
|
+
});
|
|
14
|
+
|
|
15
|
+
test("resource Condition key referencing an undeclared condition → error", () => {
|
|
16
|
+
const ctx = makeCtx({
|
|
17
|
+
Resources: {
|
|
18
|
+
Rule: { Type: "AWS::Logs::LogGroup", Condition: "DoCutover", Properties: { RetentionInDays: 7 } },
|
|
19
|
+
},
|
|
20
|
+
});
|
|
21
|
+
const diags = checkUndeclaredConditions(ctx);
|
|
22
|
+
expect(diags).toHaveLength(1);
|
|
23
|
+
expect(diags[0].checkId).toBe("WAW069");
|
|
24
|
+
expect(diags[0].severity).toBe("error");
|
|
25
|
+
expect(diags[0].message).toContain("DoCutover");
|
|
26
|
+
expect(diags[0].entity).toBe("Rule");
|
|
27
|
+
});
|
|
28
|
+
|
|
29
|
+
test("Fn::If nested in properties referencing an undeclared condition → error", () => {
|
|
30
|
+
const ctx = makeCtx({
|
|
31
|
+
Resources: {
|
|
32
|
+
Rule: {
|
|
33
|
+
Type: "AWS::Logs::LogGroup",
|
|
34
|
+
Properties: { LogGroupName: { "Fn::If": ["DoCutover", "/on", "/off"] } },
|
|
35
|
+
},
|
|
36
|
+
},
|
|
37
|
+
});
|
|
38
|
+
const diags = checkUndeclaredConditions(ctx);
|
|
39
|
+
expect(diags).toHaveLength(1);
|
|
40
|
+
expect(diags[0].message).toContain("Fn::If");
|
|
41
|
+
});
|
|
42
|
+
|
|
43
|
+
test("output Condition key and Condition reference inside Conditions → error", () => {
|
|
44
|
+
const ctx = makeCtx({
|
|
45
|
+
Conditions: { NoCutover: { "Fn::Not": [{ Condition: "DoCutover" }] } },
|
|
46
|
+
Resources: { Rule: { Type: "AWS::Logs::LogGroup", Properties: {} } },
|
|
47
|
+
Outputs: { RuleName: { Condition: "AlsoMissing", Value: { Ref: "Rule" } } },
|
|
48
|
+
});
|
|
49
|
+
const diags = checkUndeclaredConditions(ctx);
|
|
50
|
+
expect(diags).toHaveLength(2);
|
|
51
|
+
const names = diags.map((d) => d.message);
|
|
52
|
+
expect(names.some((m) => m.includes('"DoCutover"'))).toBe(true);
|
|
53
|
+
expect(names.some((m) => m.includes('"AlsoMissing"'))).toBe(true);
|
|
54
|
+
});
|
|
55
|
+
|
|
56
|
+
test("declared conditions are quiet", () => {
|
|
57
|
+
const ctx = makeCtx({
|
|
58
|
+
Conditions: {
|
|
59
|
+
DoCutover: { "Fn::Equals": [{ Ref: "Cutover" }, "true"] },
|
|
60
|
+
NoCutover: { "Fn::Not": [{ Condition: "DoCutover" }] },
|
|
61
|
+
},
|
|
62
|
+
Resources: {
|
|
63
|
+
Rule: {
|
|
64
|
+
Type: "AWS::Logs::LogGroup",
|
|
65
|
+
Condition: "DoCutover",
|
|
66
|
+
Properties: { LogGroupName: { "Fn::If": ["DoCutover", "/on", "/off"] } },
|
|
67
|
+
},
|
|
68
|
+
},
|
|
69
|
+
Outputs: { RuleName: { Condition: "DoCutover", Value: { Ref: "Rule" } } },
|
|
70
|
+
});
|
|
71
|
+
expect(checkUndeclaredConditions(ctx)).toHaveLength(0);
|
|
72
|
+
});
|
|
73
|
+
|
|
74
|
+
test("an IAM policy Condition block is not a condition reference", () => {
|
|
75
|
+
const ctx = makeCtx({
|
|
76
|
+
Resources: {
|
|
77
|
+
Role: {
|
|
78
|
+
Type: "AWS::IAM::Role",
|
|
79
|
+
Properties: {
|
|
80
|
+
AssumeRolePolicyDocument: {
|
|
81
|
+
Statement: [
|
|
82
|
+
{
|
|
83
|
+
Effect: "Allow",
|
|
84
|
+
Condition: { StringEquals: { "aws:SourceAccount": "123456789012" } },
|
|
85
|
+
},
|
|
86
|
+
],
|
|
87
|
+
},
|
|
88
|
+
},
|
|
89
|
+
},
|
|
90
|
+
},
|
|
91
|
+
});
|
|
92
|
+
expect(checkUndeclaredConditions(ctx)).toHaveLength(0);
|
|
93
|
+
});
|
|
94
|
+
});
|
|
Binary file
|
package/src/plugin.test.ts
CHANGED
|
@@ -48,11 +48,15 @@ describe("awsPlugin", () => {
|
|
|
48
48
|
|
|
49
49
|
test("returns intrinsics", () => {
|
|
50
50
|
const intrinsics = awsPlugin.intrinsics!();
|
|
51
|
-
expect(intrinsics.length).toBe(
|
|
51
|
+
expect(intrinsics.length).toBe(13);
|
|
52
52
|
const names = intrinsics.map((i) => i.name);
|
|
53
53
|
expect(names).toContain("Sub");
|
|
54
54
|
expect(names).toContain("Ref");
|
|
55
55
|
expect(names).toContain("GetAtt");
|
|
56
|
+
expect(names).toContain("Equals");
|
|
57
|
+
expect(names).toContain("And");
|
|
58
|
+
expect(names).toContain("Or");
|
|
59
|
+
expect(names).toContain("Not");
|
|
56
60
|
});
|
|
57
61
|
|
|
58
62
|
test("returns pseudo-parameters", () => {
|
package/src/plugin.ts
CHANGED
|
@@ -111,6 +111,10 @@ export const awsPlugin: LexiconPlugin = {
|
|
|
111
111
|
{ name: "Split", description: "Fn::Split — split string by delimiter", isTag: false, foldsAsCall: true },
|
|
112
112
|
{ name: "Base64", description: "Fn::Base64 — encode to Base64", isTag: false, foldsAsCall: true },
|
|
113
113
|
{ name: "GetAZs", description: "Fn::GetAZs — list Availability Zones", isTag: false, foldsAsCall: true },
|
|
114
|
+
{ name: "Equals", description: "Fn::Equals — condition comparing two values", isTag: false, foldsAsCall: true },
|
|
115
|
+
{ name: "And", description: "Fn::And — condition conjunction", isTag: false, foldsAsCall: true },
|
|
116
|
+
{ name: "Or", description: "Fn::Or — condition disjunction", isTag: false, foldsAsCall: true },
|
|
117
|
+
{ name: "Not", description: "Fn::Not — condition negation", isTag: false, foldsAsCall: true },
|
|
114
118
|
];
|
|
115
119
|
},
|
|
116
120
|
|
package/src/serializer.ts
CHANGED
|
@@ -19,8 +19,9 @@ import type { LexiconOutput } from "@intentius/chant/lexicon-output";
|
|
|
19
19
|
import { walkValue, type SerializerVisitor } from "@intentius/chant/serializer-walker";
|
|
20
20
|
import { isChildProject, type ChildProjectInstance } from "@intentius/chant/child-project";
|
|
21
21
|
import { isStackOutput, type StackOutput } from "@intentius/chant/stack-output";
|
|
22
|
-
import { isAttrRefLike } from "@intentius/chant/utils";
|
|
22
|
+
import { isAttrRefLike, getLogicalName } from "@intentius/chant/utils";
|
|
23
23
|
import { resolveDependsOn } from "@intentius/chant/resource-attributes";
|
|
24
|
+
import { isCondition } from "./condition";
|
|
24
25
|
import { isDefaultTags, type TagEntry } from "./default-tags";
|
|
25
26
|
import { isTemplateTransform } from "./template-transform";
|
|
26
27
|
import { loadTaggableResources } from "./taggable";
|
|
@@ -41,6 +42,7 @@ interface CFTemplate {
|
|
|
41
42
|
Metadata?: Record<string, unknown>;
|
|
42
43
|
Transform?: string | string[];
|
|
43
44
|
Parameters?: Record<string, CFParameter>;
|
|
45
|
+
Conditions?: Record<string, unknown>;
|
|
44
46
|
Resources: Record<string, CFResource>;
|
|
45
47
|
Outputs?: Record<string, CFOutput>;
|
|
46
48
|
}
|
|
@@ -347,6 +349,13 @@ function serializeToTemplate(
|
|
|
347
349
|
}
|
|
348
350
|
|
|
349
351
|
template.Parameters[name] = param;
|
|
352
|
+
} else if (isCondition(entity)) {
|
|
353
|
+
// Condition declarable → Conditions section (#2068), lifted the way
|
|
354
|
+
// Parameter is lifted into Parameters above.
|
|
355
|
+
if (!template.Conditions) {
|
|
356
|
+
template.Conditions = {};
|
|
357
|
+
}
|
|
358
|
+
template.Conditions[name] = toCFValue(entity.expression, entityNames);
|
|
350
359
|
} else if (isChildProject(entity)) {
|
|
351
360
|
// ChildProjectInstance → AWS::CloudFormation::Stack resource
|
|
352
361
|
const childProject = entity as ChildProjectInstance;
|
|
@@ -396,8 +405,13 @@ function serializeToTemplate(
|
|
|
396
405
|
resource.DependsOn = resolved.length === 1 ? resolved[0] : resolved;
|
|
397
406
|
}
|
|
398
407
|
}
|
|
399
|
-
// Pass-through attributes
|
|
400
|
-
|
|
408
|
+
// Pass-through attributes. Condition accepts the Condition declarable
|
|
409
|
+
// as well as a name string (#2068).
|
|
410
|
+
if (attrs.Condition) {
|
|
411
|
+
resource.Condition = isCondition(attrs.Condition)
|
|
412
|
+
? entityNames.get(attrs.Condition) ?? getLogicalName(attrs.Condition)
|
|
413
|
+
: attrs.Condition as string;
|
|
414
|
+
}
|
|
401
415
|
if (attrs.DeletionPolicy) resource.DeletionPolicy = attrs.DeletionPolicy as string;
|
|
402
416
|
if (attrs.UpdateReplacePolicy) resource.UpdateReplacePolicy = attrs.UpdateReplacePolicy as string;
|
|
403
417
|
if (attrs.UpdatePolicy) resource.UpdatePolicy = attrs.UpdatePolicy;
|
|
@@ -471,6 +485,13 @@ function serializeToTemplate(
|
|
|
471
485
|
if (exportName) {
|
|
472
486
|
output.Export = { Name: exportName };
|
|
473
487
|
}
|
|
488
|
+
// Same defensive read: condition (#2068/#2069) postdates older cores.
|
|
489
|
+
const condition = (stackOutput as { condition?: unknown }).condition;
|
|
490
|
+
if (condition) {
|
|
491
|
+
output.Condition = isCondition(condition)
|
|
492
|
+
? entityNames.get(condition) ?? getLogicalName(condition)
|
|
493
|
+
: condition as string;
|
|
494
|
+
}
|
|
474
495
|
template.Outputs[name] = output;
|
|
475
496
|
}
|
|
476
497
|
}
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
{
|
|
2
|
+
"AWSTemplateFormatVersion": "2010-09-09",
|
|
3
|
+
"Parameters": { "Cutover": { "Type": "String", "Default": "false", "AllowedValues": ["true", "false"] } },
|
|
4
|
+
"Conditions": {
|
|
5
|
+
"DoCutover": { "Fn::Equals": [ { "Ref": "Cutover" }, "true" ] },
|
|
6
|
+
"NoCutover": { "Fn::Not": [ { "Condition": "DoCutover" } ] }
|
|
7
|
+
},
|
|
8
|
+
"Resources": {
|
|
9
|
+
"Rule": {
|
|
10
|
+
"Type": "AWS::Logs::LogGroup",
|
|
11
|
+
"Condition": "DoCutover",
|
|
12
|
+
"Properties": { "LogGroupName": { "Fn::If": [ "DoCutover", "/cut/on", "/cut/off" ] }, "RetentionInDays": 7 }
|
|
13
|
+
}
|
|
14
|
+
},
|
|
15
|
+
"Outputs": { "RuleName": { "Condition": "DoCutover", "Value": { "Ref": "Rule" } } }
|
|
16
|
+
}
|