@iacmp/cli 1.1.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/LICENSE +21 -0
- package/bin/run.js +73 -0
- package/dist/chat.js +4068 -0
- package/dist/commands/ai.js +3985 -0
- package/dist/commands/audit-all.js +1039 -0
- package/dist/commands/audit-dr.js +351 -0
- package/dist/commands/audit-ha.js +375 -0
- package/dist/commands/audit-improvements.js +373 -0
- package/dist/commands/audit-security.js +351 -0
- package/dist/commands/dashboard.js +417 -0
- package/dist/commands/deploy.js +188 -0
- package/dist/commands/destroy.js +194 -0
- package/dist/commands/diagram.js +896 -0
- package/dist/commands/diff.js +4420 -0
- package/dist/commands/doctor.js +191 -0
- package/dist/commands/init.js +507 -0
- package/dist/commands/ls.js +75 -0
- package/dist/commands/registry.js +170 -0
- package/dist/commands/registry.json +29 -0
- package/dist/commands/synth.js +4458 -0
- package/dist/commands/watch.js +133 -0
- package/dist/index.js +30 -0
- package/oclif.manifest.json +727 -0
- package/package.json +95 -0
|
@@ -0,0 +1,4420 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __create = Object.create;
|
|
3
|
+
var __defProp = Object.defineProperty;
|
|
4
|
+
var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
|
|
5
|
+
var __getOwnPropNames = Object.getOwnPropertyNames;
|
|
6
|
+
var __getProtoOf = Object.getPrototypeOf;
|
|
7
|
+
var __hasOwnProp = Object.prototype.hasOwnProperty;
|
|
8
|
+
var __commonJS = (cb, mod) => function __require() {
|
|
9
|
+
return mod || (0, cb[__getOwnPropNames(cb)[0]])((mod = { exports: {} }).exports, mod), mod.exports;
|
|
10
|
+
};
|
|
11
|
+
var __export = (target, all) => {
|
|
12
|
+
for (var name in all)
|
|
13
|
+
__defProp(target, name, { get: all[name], enumerable: true });
|
|
14
|
+
};
|
|
15
|
+
var __copyProps = (to, from, except, desc) => {
|
|
16
|
+
if (from && typeof from === "object" || typeof from === "function") {
|
|
17
|
+
for (let key of __getOwnPropNames(from))
|
|
18
|
+
if (!__hasOwnProp.call(to, key) && key !== except)
|
|
19
|
+
__defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
|
|
20
|
+
}
|
|
21
|
+
return to;
|
|
22
|
+
};
|
|
23
|
+
var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__getProtoOf(mod)) : {}, __copyProps(
|
|
24
|
+
// If the importer is in node compatibility mode or this is not an ESM
|
|
25
|
+
// file that has been converted to a CommonJS file using a Babel-
|
|
26
|
+
// compatible transform (i.e. "__esModule" has not been set), then set
|
|
27
|
+
// "default" to the CommonJS "module.exports" for node compatibility.
|
|
28
|
+
isNodeMode || !mod || !mod.__esModule ? __defProp(target, "default", { value: mod, enumerable: true }) : target,
|
|
29
|
+
mod
|
|
30
|
+
));
|
|
31
|
+
var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
|
|
32
|
+
|
|
33
|
+
// ../providers/aws/dist/synth/cloudformation.js
|
|
34
|
+
var require_cloudformation = __commonJS({
|
|
35
|
+
"../providers/aws/dist/synth/cloudformation.js"(exports2) {
|
|
36
|
+
"use strict";
|
|
37
|
+
Object.defineProperty(exports2, "__esModule", { value: true });
|
|
38
|
+
exports2.synthesize = synthesize;
|
|
39
|
+
var INSTANCE_TYPE_MAP = {
|
|
40
|
+
small: "t3.small",
|
|
41
|
+
medium: "t3.medium",
|
|
42
|
+
large: "t3.large"
|
|
43
|
+
};
|
|
44
|
+
var AMI_MAP = {
|
|
45
|
+
"ubuntu": "{{resolve:ssm:/aws/service/canonical/ubuntu/server/22.04/stable/current/amd64/hvm/ebs-gp2/ami-id}}",
|
|
46
|
+
"ubuntu-22.04": "{{resolve:ssm:/aws/service/canonical/ubuntu/server/22.04/stable/current/amd64/hvm/ebs-gp2/ami-id}}",
|
|
47
|
+
"ubuntu-20.04": "{{resolve:ssm:/aws/service/canonical/ubuntu/server/20.04/stable/current/amd64/hvm/ebs-gp2/ami-id}}",
|
|
48
|
+
"amazon-linux-2": "{{resolve:ssm:/aws/service/ami-amazon-linux-latest/amzn2-ami-hvm-x86_64-gp2}}",
|
|
49
|
+
"amazon-linux-2023": "{{resolve:ssm:/aws/service/ami-amazon-linux-latest/al2023-ami-kernel-default-x86_64}}",
|
|
50
|
+
"windows-2022": "{{resolve:ssm:/aws/service/ami-windows-latest/Windows_Server-2022-English-Full-Base}}",
|
|
51
|
+
"windows-2019": "{{resolve:ssm:/aws/service/ami-windows-latest/Windows_Server-2019-English-Full-Base}}",
|
|
52
|
+
"windows-2016": "{{resolve:ssm:/aws/service/ami-windows-latest/Windows_Server-2016-English-Full-Base}}"
|
|
53
|
+
};
|
|
54
|
+
var CACHE_NODE_TYPE_MAP = {
|
|
55
|
+
small: "cache.t3.micro",
|
|
56
|
+
medium: "cache.t3.medium",
|
|
57
|
+
large: "cache.r6g.large"
|
|
58
|
+
};
|
|
59
|
+
var K8S_NODE_TYPE_MAP = {
|
|
60
|
+
small: "t3.medium",
|
|
61
|
+
medium: "m5.large",
|
|
62
|
+
large: "m5.2xlarge"
|
|
63
|
+
};
|
|
64
|
+
function synthesizeConstruct(construct) {
|
|
65
|
+
const logicalId = construct.id.replace(/[^a-zA-Z0-9]/g, "");
|
|
66
|
+
const props = construct.props;
|
|
67
|
+
switch (construct.type) {
|
|
68
|
+
// ── Compute ──────────────────────────────────────────────────────────
|
|
69
|
+
case "Compute.Instance":
|
|
70
|
+
return [[logicalId, {
|
|
71
|
+
Type: "AWS::EC2::Instance",
|
|
72
|
+
Properties: {
|
|
73
|
+
InstanceType: INSTANCE_TYPE_MAP[props.instanceType] ?? "t3.small",
|
|
74
|
+
ImageId: AMI_MAP[props.image] ?? props.image
|
|
75
|
+
}
|
|
76
|
+
}]];
|
|
77
|
+
case "Compute.AutoScaling": {
|
|
78
|
+
const lcId = `${logicalId}LC`;
|
|
79
|
+
const asgId = `${logicalId}ASG`;
|
|
80
|
+
const spId = `${logicalId}ScalingPolicy`;
|
|
81
|
+
const lc = {
|
|
82
|
+
Type: "AWS::AutoScaling::LaunchConfiguration",
|
|
83
|
+
Properties: {
|
|
84
|
+
ImageId: AMI_MAP[props.image] ?? props.image,
|
|
85
|
+
InstanceType: INSTANCE_TYPE_MAP[props.instanceType] ?? "t3.small",
|
|
86
|
+
...props.securityGroupIds ? { SecurityGroups: props.securityGroupIds } : {}
|
|
87
|
+
}
|
|
88
|
+
};
|
|
89
|
+
const asg = {
|
|
90
|
+
Type: "AWS::AutoScaling::AutoScalingGroup",
|
|
91
|
+
Properties: {
|
|
92
|
+
LaunchConfigurationName: { Ref: lcId },
|
|
93
|
+
MinSize: String(props.minCapacity ?? 1),
|
|
94
|
+
MaxSize: String(props.maxCapacity ?? 3),
|
|
95
|
+
DesiredCapacity: String(props.desiredCapacity ?? props.minCapacity ?? 1),
|
|
96
|
+
...props.subnetIds ? { VPCZoneIdentifier: props.subnetIds } : {},
|
|
97
|
+
Tags: [{ Key: "Name", Value: logicalId, PropagateAtLaunch: true }]
|
|
98
|
+
}
|
|
99
|
+
};
|
|
100
|
+
const entries = [[lcId, lc], [asgId, asg]];
|
|
101
|
+
if (props.targetCpuUtilization) {
|
|
102
|
+
entries.push([spId, {
|
|
103
|
+
Type: "AWS::AutoScaling::ScalingPolicy",
|
|
104
|
+
Properties: {
|
|
105
|
+
AutoScalingGroupName: { Ref: asgId },
|
|
106
|
+
PolicyType: "TargetTrackingScaling",
|
|
107
|
+
TargetTrackingConfiguration: {
|
|
108
|
+
PredefinedMetricSpecification: { PredefinedMetricType: "ASGAverageCPUUtilization" },
|
|
109
|
+
TargetValue: props.targetCpuUtilization
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
}]);
|
|
113
|
+
}
|
|
114
|
+
return entries;
|
|
115
|
+
}
|
|
116
|
+
case "Compute.Container": {
|
|
117
|
+
const clusterLogicalId = `${logicalId}Cluster`;
|
|
118
|
+
const tdLogicalId = `${logicalId}TaskDef`;
|
|
119
|
+
const svcLogicalId = `${logicalId}Service`;
|
|
120
|
+
const environment = props.environment;
|
|
121
|
+
return [
|
|
122
|
+
[clusterLogicalId, {
|
|
123
|
+
Type: "AWS::ECS::Cluster",
|
|
124
|
+
Properties: { ClusterName: construct.id }
|
|
125
|
+
}],
|
|
126
|
+
[tdLogicalId, {
|
|
127
|
+
Type: "AWS::ECS::TaskDefinition",
|
|
128
|
+
Properties: {
|
|
129
|
+
Family: construct.id,
|
|
130
|
+
NetworkMode: "awsvpc",
|
|
131
|
+
RequiresCompatibilities: ["FARGATE"],
|
|
132
|
+
Cpu: String(props.cpu ?? 256),
|
|
133
|
+
Memory: String(props.memory ?? 512),
|
|
134
|
+
ExecutionRoleArn: { "Fn::Sub": "arn:aws:iam::${AWS::AccountId}:role/ecsTaskExecutionRole" },
|
|
135
|
+
ContainerDefinitions: [{
|
|
136
|
+
Name: construct.id,
|
|
137
|
+
Image: props.image,
|
|
138
|
+
PortMappings: props.port ? [{ ContainerPort: props.port, Protocol: "tcp" }] : [],
|
|
139
|
+
Environment: environment ? Object.entries(environment).map(([k, v]) => ({ Name: k, Value: v })) : [],
|
|
140
|
+
LogConfiguration: {
|
|
141
|
+
LogDriver: "awslogs",
|
|
142
|
+
Options: {
|
|
143
|
+
"awslogs-group": `/ecs/${construct.id}`,
|
|
144
|
+
"awslogs-region": { Ref: "AWS::Region" },
|
|
145
|
+
"awslogs-stream-prefix": "ecs"
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
}]
|
|
149
|
+
}
|
|
150
|
+
}],
|
|
151
|
+
[svcLogicalId, {
|
|
152
|
+
Type: "AWS::ECS::Service",
|
|
153
|
+
Properties: {
|
|
154
|
+
Cluster: { Ref: clusterLogicalId },
|
|
155
|
+
TaskDefinition: { Ref: tdLogicalId },
|
|
156
|
+
DesiredCount: props.desiredCount ?? 1,
|
|
157
|
+
LaunchType: "FARGATE",
|
|
158
|
+
NetworkConfiguration: {
|
|
159
|
+
AwsvpcConfiguration: {
|
|
160
|
+
AssignPublicIp: props.publicIp ? "ENABLED" : "DISABLED",
|
|
161
|
+
Subnets: []
|
|
162
|
+
}
|
|
163
|
+
}
|
|
164
|
+
}
|
|
165
|
+
}]
|
|
166
|
+
];
|
|
167
|
+
}
|
|
168
|
+
case "Compute.Kubernetes": {
|
|
169
|
+
return [
|
|
170
|
+
[logicalId, {
|
|
171
|
+
Type: "AWS::EKS::Cluster",
|
|
172
|
+
Properties: {
|
|
173
|
+
Name: construct.id,
|
|
174
|
+
Version: props.version ?? "1.29",
|
|
175
|
+
ResourcesVpcConfig: {
|
|
176
|
+
SubnetIds: [],
|
|
177
|
+
EndpointPrivateAccess: props.privateCluster ?? false,
|
|
178
|
+
EndpointPublicAccess: !props.privateCluster
|
|
179
|
+
},
|
|
180
|
+
RoleArn: { "Fn::Sub": "arn:aws:iam::${AWS::AccountId}:role/EKSClusterRole" }
|
|
181
|
+
}
|
|
182
|
+
}],
|
|
183
|
+
[`${logicalId}NodeGroup`, {
|
|
184
|
+
Type: "AWS::EKS::Nodegroup",
|
|
185
|
+
Properties: {
|
|
186
|
+
ClusterName: { Ref: logicalId },
|
|
187
|
+
NodegroupName: `${construct.id}-ng`,
|
|
188
|
+
ScalingConfig: {
|
|
189
|
+
MinSize: props.minNodes ?? 1,
|
|
190
|
+
MaxSize: props.maxNodes ?? 3,
|
|
191
|
+
DesiredSize: props.desiredNodes ?? 2
|
|
192
|
+
},
|
|
193
|
+
InstanceTypes: [K8S_NODE_TYPE_MAP[props.nodeInstanceType ?? "medium"] ?? "m5.large"],
|
|
194
|
+
NodeRole: { "Fn::Sub": "arn:aws:iam::${AWS::AccountId}:role/EKSNodeRole" },
|
|
195
|
+
Subnets: []
|
|
196
|
+
}
|
|
197
|
+
}]
|
|
198
|
+
];
|
|
199
|
+
}
|
|
200
|
+
// ── Storage ───────────────────────────────────────────────────────────
|
|
201
|
+
case "Storage.Bucket": {
|
|
202
|
+
const lifecycleRules = props.lifecycleRules ?? [];
|
|
203
|
+
return [[logicalId, {
|
|
204
|
+
Type: "AWS::S3::Bucket",
|
|
205
|
+
Properties: {
|
|
206
|
+
VersioningConfiguration: props.versioning ? { Status: "Enabled" } : { Status: "Suspended" },
|
|
207
|
+
PublicAccessBlockConfiguration: {
|
|
208
|
+
BlockPublicAcls: !props.publicAccess,
|
|
209
|
+
BlockPublicPolicy: !props.publicAccess,
|
|
210
|
+
IgnorePublicAcls: !props.publicAccess,
|
|
211
|
+
RestrictPublicBuckets: !props.publicAccess
|
|
212
|
+
},
|
|
213
|
+
...lifecycleRules.length > 0 ? {
|
|
214
|
+
LifecycleConfiguration: {
|
|
215
|
+
Rules: lifecycleRules.map((r, i) => ({
|
|
216
|
+
Id: `rule-${i}`,
|
|
217
|
+
Status: "Enabled",
|
|
218
|
+
...r.prefix ? { Prefix: r.prefix } : {},
|
|
219
|
+
...r.expireAfterDays ? { ExpirationInDays: r.expireAfterDays } : {},
|
|
220
|
+
...r.transitionToGlacierDays ? {
|
|
221
|
+
Transitions: [{ TransitionInDays: r.transitionToGlacierDays, StorageClass: "GLACIER" }]
|
|
222
|
+
} : {}
|
|
223
|
+
}))
|
|
224
|
+
}
|
|
225
|
+
} : {}
|
|
226
|
+
}
|
|
227
|
+
}]];
|
|
228
|
+
}
|
|
229
|
+
case "Storage.FileSystem": {
|
|
230
|
+
const accessPoints = props.accessPoints ?? [];
|
|
231
|
+
const entries = [[logicalId, {
|
|
232
|
+
Type: "AWS::EFS::FileSystem",
|
|
233
|
+
Properties: {
|
|
234
|
+
PerformanceMode: props.performanceMode ?? "generalPurpose",
|
|
235
|
+
ThroughputMode: props.throughputMode ?? "bursting",
|
|
236
|
+
Encrypted: props.encrypted ?? true,
|
|
237
|
+
LifecyclePolicies: [{ TransitionToIA: "AFTER_30_DAYS" }],
|
|
238
|
+
FileSystemTags: [{ Key: "Name", Value: construct.id }]
|
|
239
|
+
}
|
|
240
|
+
}]];
|
|
241
|
+
for (const ap of accessPoints) {
|
|
242
|
+
const apId = `${logicalId}AP${ap.name.replace(/[^a-zA-Z0-9]/g, "")}`;
|
|
243
|
+
entries.push([apId, {
|
|
244
|
+
Type: "AWS::EFS::AccessPoint",
|
|
245
|
+
Properties: {
|
|
246
|
+
FileSystemId: { Ref: logicalId },
|
|
247
|
+
RootDirectory: { Path: ap.path },
|
|
248
|
+
...ap.uid ? { PosixUser: { Uid: String(ap.uid), Gid: String(ap.gid ?? ap.uid) } } : {},
|
|
249
|
+
AccessPointTags: [{ Key: "Name", Value: ap.name }]
|
|
250
|
+
}
|
|
251
|
+
}]);
|
|
252
|
+
}
|
|
253
|
+
return entries;
|
|
254
|
+
}
|
|
255
|
+
case "Storage.Archive": {
|
|
256
|
+
return [[logicalId, {
|
|
257
|
+
Type: "AWS::S3::Bucket",
|
|
258
|
+
Properties: {
|
|
259
|
+
LifecycleConfiguration: {
|
|
260
|
+
Rules: [{
|
|
261
|
+
Id: "archive-rule",
|
|
262
|
+
Status: "Enabled",
|
|
263
|
+
Transitions: [{ TransitionInDays: 0, StorageClass: "DEEP_ARCHIVE" }],
|
|
264
|
+
...props.retentionDays ? { ExpirationInDays: props.retentionDays } : {}
|
|
265
|
+
}]
|
|
266
|
+
},
|
|
267
|
+
ObjectLockEnabled: props.lockEnabled ?? false,
|
|
268
|
+
PublicAccessBlockConfiguration: {
|
|
269
|
+
BlockPublicAcls: true,
|
|
270
|
+
BlockPublicPolicy: true,
|
|
271
|
+
IgnorePublicAcls: true,
|
|
272
|
+
RestrictPublicBuckets: true
|
|
273
|
+
}
|
|
274
|
+
}
|
|
275
|
+
}]];
|
|
276
|
+
}
|
|
277
|
+
// ── Network ───────────────────────────────────────────────────────────
|
|
278
|
+
case "Network.VPC":
|
|
279
|
+
return [[logicalId, {
|
|
280
|
+
Type: "AWS::EC2::VPC",
|
|
281
|
+
Properties: {
|
|
282
|
+
CidrBlock: props.cidr ?? "10.0.0.0/16",
|
|
283
|
+
EnableDnsHostnames: true,
|
|
284
|
+
EnableDnsSupport: true,
|
|
285
|
+
Tags: [{ Key: "Name", Value: logicalId }]
|
|
286
|
+
}
|
|
287
|
+
}]];
|
|
288
|
+
case "Network.Subnet": {
|
|
289
|
+
const isPublic = props.public ?? false;
|
|
290
|
+
return [[logicalId, {
|
|
291
|
+
Type: "AWS::EC2::Subnet",
|
|
292
|
+
Properties: {
|
|
293
|
+
VpcId: props.vpcId,
|
|
294
|
+
CidrBlock: props.cidr,
|
|
295
|
+
...props.availabilityZone ? { AvailabilityZone: props.availabilityZone } : {},
|
|
296
|
+
MapPublicIpOnLaunch: isPublic,
|
|
297
|
+
Tags: [{ Key: "Name", Value: logicalId }, { Key: "Type", Value: isPublic ? "public" : "private" }]
|
|
298
|
+
}
|
|
299
|
+
}]];
|
|
300
|
+
}
|
|
301
|
+
case "Network.SecurityGroup": {
|
|
302
|
+
const ingress = props.ingressRules ?? [];
|
|
303
|
+
const egress = props.egressRules ?? [];
|
|
304
|
+
return [[logicalId, {
|
|
305
|
+
Type: "AWS::EC2::SecurityGroup",
|
|
306
|
+
Properties: {
|
|
307
|
+
GroupDescription: props.description ?? `Security group ${logicalId}`,
|
|
308
|
+
VpcId: props.vpcId,
|
|
309
|
+
SecurityGroupIngress: ingress.map((r, i) => {
|
|
310
|
+
if (r.cidr === void 0) {
|
|
311
|
+
console.warn(`[aws] Security group rule sem CIDR; usando 0.0.0.0/0 \u2014 defina props.cidr explicitamente (${construct.id} ingress[${i}])`);
|
|
312
|
+
}
|
|
313
|
+
return {
|
|
314
|
+
IpProtocol: r.protocol,
|
|
315
|
+
FromPort: r.fromPort,
|
|
316
|
+
ToPort: r.toPort,
|
|
317
|
+
CidrIp: r.cidr ?? "0.0.0.0/0",
|
|
318
|
+
...r.description ? { Description: r.description } : {}
|
|
319
|
+
};
|
|
320
|
+
}),
|
|
321
|
+
SecurityGroupEgress: egress.length > 0 ? egress.map((r) => ({
|
|
322
|
+
IpProtocol: r.protocol,
|
|
323
|
+
FromPort: r.fromPort,
|
|
324
|
+
ToPort: r.toPort,
|
|
325
|
+
CidrIp: r.cidr ?? "0.0.0.0/0",
|
|
326
|
+
...r.description ? { Description: r.description } : {}
|
|
327
|
+
})) : [{ IpProtocol: "-1", CidrIp: "0.0.0.0/0", Description: "Allow all egress" }],
|
|
328
|
+
Tags: [{ Key: "Name", Value: logicalId }]
|
|
329
|
+
}
|
|
330
|
+
}]];
|
|
331
|
+
}
|
|
332
|
+
case "Network.WAF": {
|
|
333
|
+
const rules = props.rules ?? [];
|
|
334
|
+
const defaultAction = props.defaultAction ?? "allow";
|
|
335
|
+
return [[logicalId, {
|
|
336
|
+
Type: "AWS::WAFv2::WebACL",
|
|
337
|
+
Properties: {
|
|
338
|
+
Name: logicalId,
|
|
339
|
+
Scope: props.scope ?? "REGIONAL",
|
|
340
|
+
DefaultAction: { [defaultAction === "block" ? "Block" : "Allow"]: {} },
|
|
341
|
+
Description: props.description ?? `WAF ${logicalId}`,
|
|
342
|
+
Rules: rules.map((r, i) => ({
|
|
343
|
+
Name: r.name ?? `rule-${i}`,
|
|
344
|
+
Priority: r.priority ?? i + 1,
|
|
345
|
+
Action: { [r.action === "block" ? "Block" : r.action === "count" ? "Count" : "Allow"]: {} },
|
|
346
|
+
VisibilityConfig: {
|
|
347
|
+
SampledRequestsEnabled: true,
|
|
348
|
+
CloudWatchMetricsEnabled: true,
|
|
349
|
+
MetricName: (r.name ?? `rule${i}`).replace(/[^a-zA-Z0-9]/g, "")
|
|
350
|
+
},
|
|
351
|
+
Statement: r.managedGroup ? { ManagedRuleGroupStatement: { VendorName: "AWS", Name: r.managedGroup } } : {
|
|
352
|
+
ByteMatchStatement: {
|
|
353
|
+
SearchString: (r.matchValues ?? ["BadBot"])[0],
|
|
354
|
+
FieldToMatch: { SingleHeader: { Name: "user-agent" } },
|
|
355
|
+
TextTransformations: [{ Priority: 0, Type: "NONE" }],
|
|
356
|
+
PositionalConstraint: "CONTAINS"
|
|
357
|
+
}
|
|
358
|
+
}
|
|
359
|
+
})),
|
|
360
|
+
VisibilityConfig: {
|
|
361
|
+
SampledRequestsEnabled: true,
|
|
362
|
+
CloudWatchMetricsEnabled: true,
|
|
363
|
+
MetricName: logicalId
|
|
364
|
+
}
|
|
365
|
+
}
|
|
366
|
+
}]];
|
|
367
|
+
}
|
|
368
|
+
case "Network.LoadBalancer": {
|
|
369
|
+
const lbType = props.type ?? "application";
|
|
370
|
+
const listeners = props.listeners ?? [];
|
|
371
|
+
const targetGroups = props.targetGroups ?? [];
|
|
372
|
+
const entries = [[logicalId, {
|
|
373
|
+
Type: "AWS::ElasticLoadBalancingV2::LoadBalancer",
|
|
374
|
+
Properties: {
|
|
375
|
+
Name: construct.id,
|
|
376
|
+
Type: lbType,
|
|
377
|
+
Scheme: props.scheme ?? "internet-facing",
|
|
378
|
+
Subnets: props.subnetIds ?? [],
|
|
379
|
+
...lbType === "application" && props.securityGroupIds ? { SecurityGroups: props.securityGroupIds } : {},
|
|
380
|
+
LoadBalancerAttributes: [
|
|
381
|
+
{ Key: "deletion_protection.enabled", Value: String(props.deletionProtection ?? false) }
|
|
382
|
+
]
|
|
383
|
+
}
|
|
384
|
+
}]];
|
|
385
|
+
for (const tg of targetGroups) {
|
|
386
|
+
const tgId = `${logicalId}TG${tg.name.replace(/[^a-zA-Z0-9]/g, "")}`;
|
|
387
|
+
entries.push([tgId, {
|
|
388
|
+
Type: "AWS::ElasticLoadBalancingV2::TargetGroup",
|
|
389
|
+
Properties: {
|
|
390
|
+
Name: tg.name,
|
|
391
|
+
Port: tg.port,
|
|
392
|
+
Protocol: tg.protocol,
|
|
393
|
+
VpcId: "",
|
|
394
|
+
HealthCheckPath: tg.healthCheckPath ?? "/",
|
|
395
|
+
HealthCheckPort: String(tg.healthCheckPort ?? tg.port),
|
|
396
|
+
TargetType: "ip"
|
|
397
|
+
}
|
|
398
|
+
}]);
|
|
399
|
+
}
|
|
400
|
+
for (let i = 0; i < listeners.length; i++) {
|
|
401
|
+
const l = listeners[i];
|
|
402
|
+
entries.push([`${logicalId}Listener${i + 1}`, {
|
|
403
|
+
Type: "AWS::ElasticLoadBalancingV2::Listener",
|
|
404
|
+
Properties: {
|
|
405
|
+
LoadBalancerArn: { Ref: logicalId },
|
|
406
|
+
Port: l.port,
|
|
407
|
+
Protocol: l.protocol,
|
|
408
|
+
...l.certificateArn ? { Certificates: [{ CertificateArn: l.certificateArn }] } : {},
|
|
409
|
+
DefaultActions: l.redirectToHttps ? [{ Type: "redirect", RedirectConfig: { Protocol: "HTTPS", Port: "443", StatusCode: "HTTP_301" } }] : [{ Type: "fixed-response", FixedResponseConfig: { StatusCode: "404", MessageBody: "Not found", ContentType: "text/plain" } }]
|
|
410
|
+
}
|
|
411
|
+
}]);
|
|
412
|
+
}
|
|
413
|
+
return entries;
|
|
414
|
+
}
|
|
415
|
+
case "Network.CDN": {
|
|
416
|
+
const origins = props.origins ?? [];
|
|
417
|
+
const cachePolicies = props.cachePolicies ?? [];
|
|
418
|
+
return [[logicalId, {
|
|
419
|
+
Type: "AWS::CloudFront::Distribution",
|
|
420
|
+
Properties: {
|
|
421
|
+
DistributionConfig: {
|
|
422
|
+
Enabled: true,
|
|
423
|
+
HttpVersion: props.httpVersion ?? "http2",
|
|
424
|
+
PriceClass: props.priceClass ?? "PriceClass_100",
|
|
425
|
+
DefaultRootObject: props.defaultRootObject ?? "index.html",
|
|
426
|
+
...props.aliases ? { Aliases: props.aliases } : {},
|
|
427
|
+
...props.certificateArn ? { ViewerCertificate: { AcmCertificateArn: props.certificateArn, SslSupportMethod: "sni-only", MinimumProtocolVersion: "TLSv1.2_2021" } } : { ViewerCertificate: { CloudFrontDefaultCertificate: true } },
|
|
428
|
+
...props.wafAclId ? { WebACLId: props.wafAclId } : {},
|
|
429
|
+
Origins: origins.map((o) => ({
|
|
430
|
+
Id: o.id,
|
|
431
|
+
DomainName: o.domainName,
|
|
432
|
+
OriginPath: o.path ?? "",
|
|
433
|
+
CustomOriginConfig: { HTTPSPort: 443, OriginProtocolPolicy: o.protocol ?? "https-only" }
|
|
434
|
+
})),
|
|
435
|
+
DefaultCacheBehavior: {
|
|
436
|
+
TargetOriginId: origins[0].id,
|
|
437
|
+
ViewerProtocolPolicy: "redirect-to-https",
|
|
438
|
+
AllowedMethods: ["GET", "HEAD", "OPTIONS"],
|
|
439
|
+
CachedMethods: ["GET", "HEAD"],
|
|
440
|
+
Compress: true,
|
|
441
|
+
ForwardedValues: { QueryString: false, Cookies: { Forward: "none" } }
|
|
442
|
+
},
|
|
443
|
+
CacheBehaviors: cachePolicies.map((cp) => ({
|
|
444
|
+
PathPattern: cp.pathPattern,
|
|
445
|
+
TargetOriginId: origins[0].id,
|
|
446
|
+
ViewerProtocolPolicy: "redirect-to-https",
|
|
447
|
+
DefaultTTL: cp.ttlSeconds ?? 86400,
|
|
448
|
+
MaxTTL: (cp.ttlSeconds ?? 86400) * 2,
|
|
449
|
+
Compress: cp.compress ?? true,
|
|
450
|
+
ForwardedValues: { QueryString: true, Cookies: { Forward: "all" } },
|
|
451
|
+
AllowedMethods: ["GET", "HEAD", "OPTIONS", "PUT", "POST", "PATCH", "DELETE"],
|
|
452
|
+
CachedMethods: ["GET", "HEAD"]
|
|
453
|
+
}))
|
|
454
|
+
}
|
|
455
|
+
}
|
|
456
|
+
}]];
|
|
457
|
+
}
|
|
458
|
+
case "Network.Dns": {
|
|
459
|
+
const records = props.records ?? [];
|
|
460
|
+
const hostedZoneId = `${logicalId}Zone`;
|
|
461
|
+
const entries = [[hostedZoneId, {
|
|
462
|
+
Type: "AWS::Route53::HostedZone",
|
|
463
|
+
Properties: {
|
|
464
|
+
Name: props.zoneName,
|
|
465
|
+
HostedZoneConfig: { Comment: `Zone for ${props.zoneName}` }
|
|
466
|
+
}
|
|
467
|
+
}]];
|
|
468
|
+
for (const r of records) {
|
|
469
|
+
const recId = `${logicalId}${r.name.replace(/[^a-zA-Z0-9]/g, "")}${r.type}`;
|
|
470
|
+
entries.push([recId, {
|
|
471
|
+
Type: "AWS::Route53::RecordSet",
|
|
472
|
+
Properties: {
|
|
473
|
+
HostedZoneId: { Ref: hostedZoneId },
|
|
474
|
+
Name: r.name,
|
|
475
|
+
Type: r.type,
|
|
476
|
+
TTL: String(r.ttl ?? 300),
|
|
477
|
+
...r.aliasTarget ? { AliasTarget: { DNSName: r.aliasTarget, HostedZoneId: "Z35SXDOTRQ7X7K" } } : { ResourceRecords: r.values }
|
|
478
|
+
}
|
|
479
|
+
}]);
|
|
480
|
+
}
|
|
481
|
+
return entries;
|
|
482
|
+
}
|
|
483
|
+
// ── Database ──────────────────────────────────────────────────────────
|
|
484
|
+
case "Database.SQL": {
|
|
485
|
+
const engine = props.engine;
|
|
486
|
+
const edition = props.edition ?? "";
|
|
487
|
+
const engineMap = {
|
|
488
|
+
mysql: { Engine: "mysql", EngineVersion: "8.0.36" },
|
|
489
|
+
postgres: { Engine: "postgres", EngineVersion: "15.4" },
|
|
490
|
+
mariadb: { Engine: "mariadb", EngineVersion: "10.11.6" },
|
|
491
|
+
oracle: { Engine: `oracle-${edition || "se2"}`, EngineVersion: "19.0.0.0.ru-2024-01.rur-2024-01.r1" },
|
|
492
|
+
sqlserver: { Engine: `sqlserver-${edition || "ex"}`, EngineVersion: "15.00.4365.2.v1" }
|
|
493
|
+
};
|
|
494
|
+
const mapped = engineMap[engine] ?? engineMap["mysql"];
|
|
495
|
+
const isOracle = engine === "oracle";
|
|
496
|
+
const isSqlServer = engine === "sqlserver";
|
|
497
|
+
const licenseModel = props.licenseModel ?? (isOracle || isSqlServer ? "license-included" : void 0);
|
|
498
|
+
const masterUser = isSqlServer ? "sqladmin" : "dbadmin";
|
|
499
|
+
const defaultInstance = isOracle || isSqlServer ? "db.t3.small" : "db.t3.micro";
|
|
500
|
+
const rdsProps = {
|
|
501
|
+
DBInstanceClass: props.instanceType ?? defaultInstance,
|
|
502
|
+
Engine: mapped.Engine,
|
|
503
|
+
EngineVersion: mapped.EngineVersion,
|
|
504
|
+
AllocatedStorage: String(props.storageGb ?? 20),
|
|
505
|
+
MultiAZ: props.multiAz ?? false,
|
|
506
|
+
MasterUsername: masterUser,
|
|
507
|
+
MasterUserPassword: { "Fn::Sub": "{{resolve:ssm:/iacmp/${AWS::StackName}/db-password}}" },
|
|
508
|
+
StorageEncrypted: true,
|
|
509
|
+
BackupRetentionPeriod: props.backupRetentionDays ?? 7,
|
|
510
|
+
DeletionProtection: props.deletionProtection ?? false
|
|
511
|
+
};
|
|
512
|
+
if (licenseModel)
|
|
513
|
+
rdsProps["LicenseModel"] = licenseModel;
|
|
514
|
+
return [[logicalId, {
|
|
515
|
+
Type: "AWS::RDS::DBInstance",
|
|
516
|
+
DeletionPolicy: props.deletionProtection ? "Retain" : "Snapshot",
|
|
517
|
+
Properties: rdsProps
|
|
518
|
+
}]];
|
|
519
|
+
}
|
|
520
|
+
case "Database.DocumentDB": {
|
|
521
|
+
const instances = props.instances ?? 1;
|
|
522
|
+
const clusterLogicalId = `${logicalId}Cluster`;
|
|
523
|
+
const entries = [[clusterLogicalId, {
|
|
524
|
+
Type: "AWS::DocDB::DBCluster",
|
|
525
|
+
DeletionPolicy: props.deletionProtection ? "Retain" : "Snapshot",
|
|
526
|
+
Properties: {
|
|
527
|
+
DBClusterIdentifier: construct.id.toLowerCase(),
|
|
528
|
+
MasterUsername: "docdbadmin",
|
|
529
|
+
MasterUserPassword: { "Fn::Sub": "{{resolve:ssm:/iacmp/${AWS::StackName}/docdb-password}}" },
|
|
530
|
+
StorageEncrypted: true,
|
|
531
|
+
BackupRetentionPeriod: 7,
|
|
532
|
+
DeletionProtection: props.deletionProtection ?? false
|
|
533
|
+
}
|
|
534
|
+
}]];
|
|
535
|
+
for (let i = 0; i < instances; i++) {
|
|
536
|
+
entries.push([`${logicalId}Instance${i + 1}`, {
|
|
537
|
+
Type: "AWS::DocDB::DBInstance",
|
|
538
|
+
Properties: {
|
|
539
|
+
DBClusterIdentifier: { Ref: clusterLogicalId },
|
|
540
|
+
DBInstanceClass: props.instanceType ?? "db.t3.medium",
|
|
541
|
+
DBInstanceIdentifier: `${construct.id.toLowerCase()}-${i + 1}`
|
|
542
|
+
}
|
|
543
|
+
}]);
|
|
544
|
+
}
|
|
545
|
+
return entries;
|
|
546
|
+
}
|
|
547
|
+
case "Database.DynamoDB": {
|
|
548
|
+
const billingMode = props.billingMode ?? "PAY_PER_REQUEST";
|
|
549
|
+
const gsis = props.globalSecondaryIndexes ?? [];
|
|
550
|
+
const attrDefs = [
|
|
551
|
+
{ AttributeName: props.partitionKey, AttributeType: "S" },
|
|
552
|
+
...props.sortKey ? [{ AttributeName: props.sortKey, AttributeType: "S" }] : [],
|
|
553
|
+
...gsis.map((g) => ({ AttributeName: g.partitionKey, AttributeType: "S" })),
|
|
554
|
+
...gsis.filter((g) => g.sortKey).map((g) => ({ AttributeName: g.sortKey, AttributeType: "S" }))
|
|
555
|
+
].filter((v, i, a) => a.findIndex((x) => x.AttributeName === v.AttributeName) === i);
|
|
556
|
+
return [[logicalId, {
|
|
557
|
+
Type: "AWS::DynamoDB::Table",
|
|
558
|
+
DeletionPolicy: "Retain",
|
|
559
|
+
Properties: {
|
|
560
|
+
TableName: construct.id,
|
|
561
|
+
BillingMode: billingMode,
|
|
562
|
+
...billingMode === "PROVISIONED" ? {
|
|
563
|
+
ProvisionedThroughput: { ReadCapacityUnits: props.readCapacity ?? 5, WriteCapacityUnits: props.writeCapacity ?? 5 }
|
|
564
|
+
} : {},
|
|
565
|
+
AttributeDefinitions: attrDefs,
|
|
566
|
+
KeySchema: [
|
|
567
|
+
{ AttributeName: props.partitionKey, KeyType: "HASH" },
|
|
568
|
+
...props.sortKey ? [{ AttributeName: props.sortKey, KeyType: "RANGE" }] : []
|
|
569
|
+
],
|
|
570
|
+
...gsis.length > 0 ? {
|
|
571
|
+
GlobalSecondaryIndexes: gsis.map((g) => ({
|
|
572
|
+
IndexName: g.name,
|
|
573
|
+
KeySchema: [
|
|
574
|
+
{ AttributeName: g.partitionKey, KeyType: "HASH" },
|
|
575
|
+
...g.sortKey ? [{ AttributeName: g.sortKey, KeyType: "RANGE" }] : []
|
|
576
|
+
],
|
|
577
|
+
Projection: { ProjectionType: "ALL" },
|
|
578
|
+
...billingMode === "PROVISIONED" ? {
|
|
579
|
+
ProvisionedThroughput: { ReadCapacityUnits: 5, WriteCapacityUnits: 5 }
|
|
580
|
+
} : {}
|
|
581
|
+
}))
|
|
582
|
+
} : {},
|
|
583
|
+
...props.ttlAttribute ? { TimeToLiveSpecification: { AttributeName: props.ttlAttribute, Enabled: true } } : {},
|
|
584
|
+
PointInTimeRecoverySpecification: { PointInTimeRecoveryEnabled: props.pointInTimeRecovery ?? true },
|
|
585
|
+
...props.streamEnabled ? { StreamSpecification: { StreamViewType: "NEW_AND_OLD_IMAGES" } } : {}
|
|
586
|
+
}
|
|
587
|
+
}]];
|
|
588
|
+
}
|
|
589
|
+
// ── Cache ─────────────────────────────────────────────────────────────
|
|
590
|
+
case "Cache.Redis": {
|
|
591
|
+
const numNodes = props.numCacheNodes ?? 1;
|
|
592
|
+
const autoFailover = props.automaticFailoverEnabled ?? false;
|
|
593
|
+
return [[logicalId, {
|
|
594
|
+
Type: "AWS::ElastiCache::ReplicationGroup",
|
|
595
|
+
Properties: {
|
|
596
|
+
ReplicationGroupDescription: `Redis ${construct.id}`,
|
|
597
|
+
ReplicationGroupId: construct.id.toLowerCase().replace(/[^a-z0-9-]/g, "-").slice(0, 40),
|
|
598
|
+
CacheNodeType: CACHE_NODE_TYPE_MAP[props.nodeType ?? "small"] ?? "cache.t3.micro",
|
|
599
|
+
Engine: "redis",
|
|
600
|
+
EngineVersion: props.version ?? "7.0",
|
|
601
|
+
NumCacheClusters: numNodes,
|
|
602
|
+
AutomaticFailoverEnabled: autoFailover && numNodes > 1,
|
|
603
|
+
AtRestEncryptionEnabled: props.atRestEncryptionEnabled ?? true,
|
|
604
|
+
TransitEncryptionEnabled: props.transitEncryptionEnabled ?? true,
|
|
605
|
+
...props.subnetGroupName ? { CacheSubnetGroupName: props.subnetGroupName } : {},
|
|
606
|
+
...props.securityGroupIds ? { SecurityGroupIds: props.securityGroupIds } : {}
|
|
607
|
+
}
|
|
608
|
+
}]];
|
|
609
|
+
}
|
|
610
|
+
case "Cache.Memcached": {
|
|
611
|
+
return [[logicalId, {
|
|
612
|
+
Type: "AWS::ElastiCache::CacheCluster",
|
|
613
|
+
Properties: {
|
|
614
|
+
ClusterName: construct.id.toLowerCase().replace(/[^a-z0-9-]/g, "-").slice(0, 40),
|
|
615
|
+
Engine: "memcached",
|
|
616
|
+
CacheNodeType: CACHE_NODE_TYPE_MAP[props.nodeType ?? "small"] ?? "cache.t3.micro",
|
|
617
|
+
NumCacheNodes: props.numCacheNodes ?? 2,
|
|
618
|
+
...props.subnetGroupName ? { CacheSubnetGroupName: props.subnetGroupName } : {}
|
|
619
|
+
}
|
|
620
|
+
}]];
|
|
621
|
+
}
|
|
622
|
+
// ── Function ──────────────────────────────────────────────────────────
|
|
623
|
+
case "Function.Lambda": {
|
|
624
|
+
const environment = props.environment;
|
|
625
|
+
const runtimeMap = {
|
|
626
|
+
"nodejs20": "nodejs20.x",
|
|
627
|
+
"nodejs18": "nodejs18.x",
|
|
628
|
+
"python3.12": "python3.12",
|
|
629
|
+
"python3.11": "python3.11",
|
|
630
|
+
"java21": "java21",
|
|
631
|
+
"go1.x": "go1.x",
|
|
632
|
+
"dotnet8": "dotnet8"
|
|
633
|
+
};
|
|
634
|
+
return [[logicalId, {
|
|
635
|
+
Type: "AWS::Lambda::Function",
|
|
636
|
+
Properties: {
|
|
637
|
+
FunctionName: construct.id,
|
|
638
|
+
Runtime: runtimeMap[props.runtime ?? "nodejs20"] ?? "nodejs20.x",
|
|
639
|
+
Handler: props.handler,
|
|
640
|
+
Code: { ZipFile: props.code },
|
|
641
|
+
MemorySize: props.memory ?? 128,
|
|
642
|
+
Timeout: props.timeout ?? 30,
|
|
643
|
+
Role: { "Fn::Sub": "arn:aws:iam::${AWS::AccountId}:role/LambdaExecutionRole" },
|
|
644
|
+
...props.reservedConcurrency !== void 0 ? { ReservedConcurrentExecutions: props.reservedConcurrency } : {},
|
|
645
|
+
...environment && Object.keys(environment).length > 0 ? { Environment: { Variables: environment } } : {},
|
|
646
|
+
...props.vpcId ? {
|
|
647
|
+
VpcConfig: {
|
|
648
|
+
SubnetIds: props.subnetIds ?? [],
|
|
649
|
+
SecurityGroupIds: props.securityGroupIds ?? []
|
|
650
|
+
}
|
|
651
|
+
} : {}
|
|
652
|
+
}
|
|
653
|
+
}]];
|
|
654
|
+
}
|
|
655
|
+
case "Function.ApiGateway": {
|
|
656
|
+
const apigwType = props.type ?? "HTTP";
|
|
657
|
+
const routes = props.routes ?? [];
|
|
658
|
+
const stageName = props.stageName ?? "$default";
|
|
659
|
+
const entries = [[logicalId, {
|
|
660
|
+
Type: apigwType === "REST" ? "AWS::ApiGateway::RestApi" : "AWS::ApiGatewayV2::Api",
|
|
661
|
+
Properties: {
|
|
662
|
+
Name: props.name,
|
|
663
|
+
Description: props.description ?? "",
|
|
664
|
+
...apigwType !== "REST" ? { ProtocolType: apigwType } : {},
|
|
665
|
+
...props.cors ? { CorsConfiguration: { AllowOrigins: ["*"], AllowMethods: ["*"], AllowHeaders: ["*"] } } : {}
|
|
666
|
+
}
|
|
667
|
+
}]];
|
|
668
|
+
entries.push([`${logicalId}Stage`, {
|
|
669
|
+
Type: apigwType === "REST" ? "AWS::ApiGateway::Stage" : "AWS::ApiGatewayV2::Stage",
|
|
670
|
+
Properties: {
|
|
671
|
+
...apigwType === "REST" ? { RestApiId: { Ref: logicalId } } : { ApiId: { Ref: logicalId } },
|
|
672
|
+
StageName: stageName,
|
|
673
|
+
AutoDeploy: apigwType !== "REST",
|
|
674
|
+
...props.throttlingBurstLimit ? {
|
|
675
|
+
DefaultRouteSettings: {
|
|
676
|
+
ThrottlingBurstLimit: props.throttlingBurstLimit,
|
|
677
|
+
ThrottlingRateLimit: props.throttlingRateLimit ?? 1e3
|
|
678
|
+
}
|
|
679
|
+
} : {}
|
|
680
|
+
}
|
|
681
|
+
}]);
|
|
682
|
+
const authorizerLambdaId = props.authorizerLambdaId;
|
|
683
|
+
const authorizerId = authorizerLambdaId ? `${logicalId}Authorizer` : void 0;
|
|
684
|
+
if (authorizerLambdaId) {
|
|
685
|
+
entries.push([authorizerId, {
|
|
686
|
+
Type: "AWS::ApiGatewayV2::Authorizer",
|
|
687
|
+
Properties: {
|
|
688
|
+
ApiId: { Ref: logicalId },
|
|
689
|
+
AuthorizerType: "REQUEST",
|
|
690
|
+
Name: `${props.name}-authorizer`,
|
|
691
|
+
AuthorizerUri: { "Fn::Sub": `arn:aws:apigateway:\${AWS::Region}:lambda:path/2015-03-31/functions/\${${authorizerLambdaId}.Arn}/invocations` },
|
|
692
|
+
AuthorizerPayloadFormatVersion: "2.0",
|
|
693
|
+
IdentitySource: ["$request.header.Authorization"]
|
|
694
|
+
}
|
|
695
|
+
}]);
|
|
696
|
+
}
|
|
697
|
+
for (const r of routes) {
|
|
698
|
+
const routeId = `${logicalId}${r.method}${r.path.replace(/[^a-zA-Z0-9]/g, "")}Route`;
|
|
699
|
+
entries.push([routeId, {
|
|
700
|
+
Type: "AWS::ApiGatewayV2::Route",
|
|
701
|
+
Properties: {
|
|
702
|
+
ApiId: { Ref: logicalId },
|
|
703
|
+
RouteKey: `${r.method} ${r.path}`,
|
|
704
|
+
...r.lambdaId ? { Target: { "Fn::Sub": `integrations/\${${routeId}Integration}` } } : {},
|
|
705
|
+
...authorizerId ? { AuthorizationType: "CUSTOM", AuthorizerId: { Ref: authorizerId } } : {}
|
|
706
|
+
}
|
|
707
|
+
}]);
|
|
708
|
+
if (r.lambdaId) {
|
|
709
|
+
entries.push([`${routeId}Integration`, {
|
|
710
|
+
Type: "AWS::ApiGatewayV2::Integration",
|
|
711
|
+
Properties: {
|
|
712
|
+
ApiId: { Ref: logicalId },
|
|
713
|
+
IntegrationType: "AWS_PROXY",
|
|
714
|
+
IntegrationUri: { "Fn::Sub": `arn:aws:apigateway:\${AWS::Region}:lambda:path/2015-03-31/functions/\${${r.lambdaId}.Arn}/invocations` },
|
|
715
|
+
PayloadFormatVersion: "2.0"
|
|
716
|
+
}
|
|
717
|
+
}]);
|
|
718
|
+
}
|
|
719
|
+
}
|
|
720
|
+
return entries;
|
|
721
|
+
}
|
|
722
|
+
// ── Policy ────────────────────────────────────────────────────────────
|
|
723
|
+
case "Policy.IAM": {
|
|
724
|
+
const statements = props.statements ?? [];
|
|
725
|
+
const attachType = props.attachType;
|
|
726
|
+
const attachTo = props.attachTo.replace(/[^a-zA-Z0-9]/g, "");
|
|
727
|
+
const principalService = attachType === "lambda" ? "lambda.amazonaws.com" : "ec2.amazonaws.com";
|
|
728
|
+
const managedPolicies = attachType === "lambda" ? ["arn:aws:iam::aws:policy/service-role/AWSLambdaBasicExecutionRole"] : attachType === "compute" ? ["arn:aws:iam::aws:policy/AmazonSSMManagedInstanceCore"] : [];
|
|
729
|
+
const roleLogicalId = `${logicalId}Role`;
|
|
730
|
+
const roleResource = {
|
|
731
|
+
Type: "AWS::IAM::Role",
|
|
732
|
+
Properties: {
|
|
733
|
+
RoleName: { "Fn::Sub": `${attachTo}-role-\${AWS::StackName}` },
|
|
734
|
+
AssumeRolePolicyDocument: {
|
|
735
|
+
Version: "2012-10-17",
|
|
736
|
+
Statement: [{ Effect: "Allow", Principal: { Service: principalService }, Action: "sts:AssumeRole" }]
|
|
737
|
+
},
|
|
738
|
+
ManagedPolicyArns: managedPolicies,
|
|
739
|
+
Policies: [{
|
|
740
|
+
PolicyName: logicalId,
|
|
741
|
+
PolicyDocument: {
|
|
742
|
+
Version: "2012-10-17",
|
|
743
|
+
Statement: statements.map((s) => ({
|
|
744
|
+
Effect: s.effect,
|
|
745
|
+
Action: s.actions,
|
|
746
|
+
Resource: s.resources ?? ["*"],
|
|
747
|
+
...s.conditions ? { Condition: s.conditions } : {}
|
|
748
|
+
}))
|
|
749
|
+
}
|
|
750
|
+
}],
|
|
751
|
+
Tags: [{ Key: "Name", Value: roleLogicalId }]
|
|
752
|
+
}
|
|
753
|
+
};
|
|
754
|
+
if (attachType === "compute") {
|
|
755
|
+
return [[roleLogicalId, roleResource], [`${logicalId}InstanceProfile`, {
|
|
756
|
+
Type: "AWS::IAM::InstanceProfile",
|
|
757
|
+
Properties: {
|
|
758
|
+
InstanceProfileName: { "Fn::Sub": `${attachTo}-profile-\${AWS::StackName}` },
|
|
759
|
+
Roles: [{ Ref: roleLogicalId }]
|
|
760
|
+
}
|
|
761
|
+
}]];
|
|
762
|
+
}
|
|
763
|
+
return [[roleLogicalId, roleResource]];
|
|
764
|
+
}
|
|
765
|
+
// ── Events ────────────────────────────────────────────────────────────
|
|
766
|
+
case "Events.EventBridge": {
|
|
767
|
+
const rules = props.rules ?? [];
|
|
768
|
+
const busName = props.busName ?? "default";
|
|
769
|
+
const entries = [];
|
|
770
|
+
if (busName !== "default") {
|
|
771
|
+
entries.push([`${logicalId}Bus`, { Type: "AWS::Events::EventBus", Properties: { Name: busName } }]);
|
|
772
|
+
}
|
|
773
|
+
for (const r of rules) {
|
|
774
|
+
const ruleName = (r.name ?? "rule").replace(/[^a-zA-Z0-9]/g, "");
|
|
775
|
+
const pattern = {};
|
|
776
|
+
if (r.source)
|
|
777
|
+
pattern["source"] = r.source;
|
|
778
|
+
if (r.detailTypes)
|
|
779
|
+
pattern["detail-type"] = r.detailTypes;
|
|
780
|
+
entries.push([`${logicalId}${ruleName}Rule`, {
|
|
781
|
+
Type: "AWS::Events::Rule",
|
|
782
|
+
Properties: {
|
|
783
|
+
Name: r.name,
|
|
784
|
+
EventBusName: busName,
|
|
785
|
+
EventPattern: pattern,
|
|
786
|
+
State: "ENABLED",
|
|
787
|
+
...r.targetArn ? { Targets: [{ Id: `${ruleName}Target`, Arn: r.targetArn }] } : {}
|
|
788
|
+
}
|
|
789
|
+
}]);
|
|
790
|
+
}
|
|
791
|
+
return entries;
|
|
792
|
+
}
|
|
793
|
+
// ── Workflow ──────────────────────────────────────────────────────────
|
|
794
|
+
case "Workflow.StepFunctions": {
|
|
795
|
+
const steps = props.steps ?? [];
|
|
796
|
+
const definition = {
|
|
797
|
+
Comment: props.description ?? `Workflow ${construct.id}`,
|
|
798
|
+
StartAt: steps[0]?.name ?? "Start",
|
|
799
|
+
States: Object.fromEntries(steps.map((s, i) => [s.name, {
|
|
800
|
+
Type: s.type ?? "Task",
|
|
801
|
+
Resource: s.resource ?? "",
|
|
802
|
+
...s.description ? { Comment: s.description } : {},
|
|
803
|
+
...i < steps.length - 1 ? { Next: steps[i + 1].name } : { End: true }
|
|
804
|
+
}]))
|
|
805
|
+
};
|
|
806
|
+
return [[logicalId, {
|
|
807
|
+
Type: "AWS::StepFunctions::StateMachine",
|
|
808
|
+
Properties: {
|
|
809
|
+
StateMachineName: construct.id,
|
|
810
|
+
StateMachineType: props.type ?? "STANDARD",
|
|
811
|
+
DefinitionString: { "Fn::Sub": JSON.stringify(definition) },
|
|
812
|
+
RoleArn: { "Fn::Sub": "arn:aws:iam::${AWS::AccountId}:role/StepFunctionsExecutionRole" },
|
|
813
|
+
LoggingConfiguration: { Level: "ERROR", IncludeExecutionData: false }
|
|
814
|
+
}
|
|
815
|
+
}]];
|
|
816
|
+
}
|
|
817
|
+
// ── Messaging ─────────────────────────────────────────────────────────
|
|
818
|
+
case "Messaging.Queue": {
|
|
819
|
+
const fifo = props.fifo ?? false;
|
|
820
|
+
return [[logicalId, {
|
|
821
|
+
Type: "AWS::SQS::Queue",
|
|
822
|
+
Properties: {
|
|
823
|
+
QueueName: fifo ? `${construct.id}.fifo` : construct.id,
|
|
824
|
+
VisibilityTimeout: props.visibilityTimeoutSeconds ?? 30,
|
|
825
|
+
MessageRetentionPeriod: props.messageRetentionSeconds ?? 345600,
|
|
826
|
+
DelaySeconds: props.delaySeconds ?? 0,
|
|
827
|
+
FifoQueue: fifo,
|
|
828
|
+
SqsManagedSseEnabled: props.encrypted ?? true,
|
|
829
|
+
...props.dlqArn ? { RedrivePolicy: { deadLetterTargetArn: props.dlqArn, maxReceiveCount: props.maxReceiveCount ?? 3 } } : {}
|
|
830
|
+
}
|
|
831
|
+
}]];
|
|
832
|
+
}
|
|
833
|
+
case "Messaging.Topic": {
|
|
834
|
+
const fifo = props.fifo ?? false;
|
|
835
|
+
const subscriptions = props.subscriptions ?? [];
|
|
836
|
+
return [[logicalId, {
|
|
837
|
+
Type: "AWS::SNS::Topic",
|
|
838
|
+
Properties: {
|
|
839
|
+
TopicName: fifo ? `${construct.id}.fifo` : construct.id,
|
|
840
|
+
DisplayName: props.displayName ?? construct.id,
|
|
841
|
+
FifoTopic: fifo,
|
|
842
|
+
...props.encrypted ? { KmsMasterKeyId: "alias/aws/sns" } : {},
|
|
843
|
+
Subscription: subscriptions.map((s) => ({ Protocol: s.protocol, Endpoint: s.endpoint }))
|
|
844
|
+
}
|
|
845
|
+
}]];
|
|
846
|
+
}
|
|
847
|
+
// ── Secret / Certificate ──────────────────────────────────────────────
|
|
848
|
+
case "Secret.Vault": {
|
|
849
|
+
return [[logicalId, {
|
|
850
|
+
Type: "AWS::SecretsManager::Secret",
|
|
851
|
+
Properties: {
|
|
852
|
+
Name: construct.id,
|
|
853
|
+
Description: props.description ?? `Secret ${construct.id}`,
|
|
854
|
+
...props.kmsKeyId ? { KmsKeyId: props.kmsKeyId } : {},
|
|
855
|
+
...props.replicaRegions ? { ReplicaRegions: props.replicaRegions.map((r) => ({ Region: r })) } : {}
|
|
856
|
+
}
|
|
857
|
+
}]];
|
|
858
|
+
}
|
|
859
|
+
case "Certificate.TLS": {
|
|
860
|
+
const sans = props.subjectAlternativeNames ?? [];
|
|
861
|
+
return [[logicalId, {
|
|
862
|
+
Type: "AWS::CertificateManager::Certificate",
|
|
863
|
+
Properties: {
|
|
864
|
+
DomainName: props.domainName,
|
|
865
|
+
ValidationMethod: props.validationMethod ?? "DNS",
|
|
866
|
+
...sans.length > 0 ? { SubjectAlternativeNames: sans } : {},
|
|
867
|
+
Tags: [{ Key: "Name", Value: construct.id }]
|
|
868
|
+
}
|
|
869
|
+
}]];
|
|
870
|
+
}
|
|
871
|
+
// ── Monitoring ────────────────────────────────────────────────────────
|
|
872
|
+
case "Monitoring.Alarm": {
|
|
873
|
+
const dimensions = props.dimensions;
|
|
874
|
+
return [[logicalId, {
|
|
875
|
+
Type: "AWS::CloudWatch::Alarm",
|
|
876
|
+
Properties: {
|
|
877
|
+
AlarmName: construct.id,
|
|
878
|
+
MetricName: props.metricName,
|
|
879
|
+
Namespace: props.namespace ?? "AWS/Lambda",
|
|
880
|
+
Threshold: props.threshold,
|
|
881
|
+
EvaluationPeriods: props.evaluationPeriods ?? 2,
|
|
882
|
+
Period: props.periodSeconds ?? 60,
|
|
883
|
+
ComparisonOperator: props.comparisonOperator ?? "GreaterThanThreshold",
|
|
884
|
+
Statistic: props.statistic ?? "Average",
|
|
885
|
+
TreatMissingData: props.treatMissingData ?? "notBreaching",
|
|
886
|
+
...props.alarmActions ? { AlarmActions: props.alarmActions } : {},
|
|
887
|
+
...props.okActions ? { OKActions: props.okActions } : {},
|
|
888
|
+
...dimensions ? { Dimensions: Object.entries(dimensions).map(([k, v]) => ({ Name: k, Value: v })) } : {}
|
|
889
|
+
}
|
|
890
|
+
}]];
|
|
891
|
+
}
|
|
892
|
+
case "Monitoring.Dashboard": {
|
|
893
|
+
const widgets = props.widgets ?? [];
|
|
894
|
+
const dashBody = {
|
|
895
|
+
widgets: widgets.map((w, i) => ({
|
|
896
|
+
type: w.type === "text" ? "text" : "metric",
|
|
897
|
+
x: i % 3 * 8,
|
|
898
|
+
y: Math.floor(i / 3) * 6,
|
|
899
|
+
width: 8,
|
|
900
|
+
height: 6,
|
|
901
|
+
properties: w.type === "text" ? { markdown: w.markdown ?? w.title } : {
|
|
902
|
+
title: w.title,
|
|
903
|
+
metrics: [[
|
|
904
|
+
w.namespace ?? "AWS/Lambda",
|
|
905
|
+
w.metricName,
|
|
906
|
+
...w.dimensions ? Object.entries(w.dimensions).flat() : []
|
|
907
|
+
]],
|
|
908
|
+
period: w.period ?? 60,
|
|
909
|
+
stat: w.stat ?? "Average",
|
|
910
|
+
view: "timeSeries"
|
|
911
|
+
}
|
|
912
|
+
}))
|
|
913
|
+
};
|
|
914
|
+
return [[logicalId, {
|
|
915
|
+
Type: "AWS::CloudWatch::Dashboard",
|
|
916
|
+
Properties: {
|
|
917
|
+
DashboardName: construct.id,
|
|
918
|
+
DashboardBody: JSON.stringify(dashBody)
|
|
919
|
+
}
|
|
920
|
+
}]];
|
|
921
|
+
}
|
|
922
|
+
case "Logging.Stream": {
|
|
923
|
+
const filters = props.subscriptionFilters ?? [];
|
|
924
|
+
const entries = [[logicalId, {
|
|
925
|
+
Type: "AWS::Logs::LogGroup",
|
|
926
|
+
Properties: {
|
|
927
|
+
LogGroupName: `/iacmp/${construct.id}`,
|
|
928
|
+
RetentionInDays: props.retentionDays ?? 30,
|
|
929
|
+
...props.kmsKeyId ? { KmsKeyId: props.kmsKeyId } : {}
|
|
930
|
+
}
|
|
931
|
+
}]];
|
|
932
|
+
for (const f of filters) {
|
|
933
|
+
entries.push([`${logicalId}${f.name.replace(/[^a-zA-Z0-9]/g, "")}Filter`, {
|
|
934
|
+
Type: "AWS::Logs::SubscriptionFilter",
|
|
935
|
+
Properties: {
|
|
936
|
+
LogGroupName: { Ref: logicalId },
|
|
937
|
+
FilterName: f.name,
|
|
938
|
+
FilterPattern: f.filterPattern,
|
|
939
|
+
DestinationArn: f.destinationArn
|
|
940
|
+
}
|
|
941
|
+
}]);
|
|
942
|
+
}
|
|
943
|
+
return entries;
|
|
944
|
+
}
|
|
945
|
+
case "Custom.Resource": {
|
|
946
|
+
const cfn = props.cloudformation;
|
|
947
|
+
if (!cfn)
|
|
948
|
+
return [];
|
|
949
|
+
return [[logicalId, { Type: cfn.type, Properties: cfn.properties }]];
|
|
950
|
+
}
|
|
951
|
+
default:
|
|
952
|
+
console.warn(`[aws] Construct type '${construct.type}' nao suportado \u2014 descartado.`);
|
|
953
|
+
return [];
|
|
954
|
+
}
|
|
955
|
+
}
|
|
956
|
+
function synthesizeVPCChildren(logicalId, cidr, maxAzs, resources) {
|
|
957
|
+
if (!maxAzs || maxAzs <= 0)
|
|
958
|
+
return;
|
|
959
|
+
const azLetters = ["a", "b", "c", "d", "e", "f"].slice(0, maxAzs);
|
|
960
|
+
const cidrBase = cidr.split(".").slice(0, 2).join(".");
|
|
961
|
+
const igwId = `${logicalId}IGW`;
|
|
962
|
+
resources[igwId] = { Type: "AWS::EC2::InternetGateway", Properties: { Tags: [{ Key: "Name", Value: igwId }] } };
|
|
963
|
+
resources[`${igwId}Attachment`] = {
|
|
964
|
+
Type: "AWS::EC2::VPCGatewayAttachment",
|
|
965
|
+
Properties: { VpcId: { Ref: logicalId }, InternetGatewayId: { Ref: igwId } }
|
|
966
|
+
};
|
|
967
|
+
const pubRTId = `${logicalId}PublicRT`;
|
|
968
|
+
resources[pubRTId] = { Type: "AWS::EC2::RouteTable", Properties: { VpcId: { Ref: logicalId }, Tags: [{ Key: "Name", Value: pubRTId }] } };
|
|
969
|
+
resources[`${pubRTId}DefaultRoute`] = {
|
|
970
|
+
Type: "AWS::EC2::Route",
|
|
971
|
+
Properties: { RouteTableId: { Ref: pubRTId }, DestinationCidrBlock: "0.0.0.0/0", GatewayId: { Ref: igwId } }
|
|
972
|
+
};
|
|
973
|
+
azLetters.forEach((az, i) => {
|
|
974
|
+
const pubSubnetId = `${logicalId}PublicSubnet${az.toUpperCase()}`;
|
|
975
|
+
const privSubnetId = `${logicalId}PrivateSubnet${az.toUpperCase()}`;
|
|
976
|
+
resources[pubSubnetId] = {
|
|
977
|
+
Type: "AWS::EC2::Subnet",
|
|
978
|
+
Properties: {
|
|
979
|
+
VpcId: { Ref: logicalId },
|
|
980
|
+
CidrBlock: `${cidrBase}.${i * 2}.0/24`,
|
|
981
|
+
AvailabilityZone: { "Fn::Select": [i, { "Fn::GetAZs": "" }] },
|
|
982
|
+
MapPublicIpOnLaunch: true,
|
|
983
|
+
Tags: [{ Key: "Name", Value: pubSubnetId }]
|
|
984
|
+
}
|
|
985
|
+
};
|
|
986
|
+
resources[`${pubSubnetId}RTAssoc`] = {
|
|
987
|
+
Type: "AWS::EC2::SubnetRouteTableAssociation",
|
|
988
|
+
Properties: { SubnetId: { Ref: pubSubnetId }, RouteTableId: { Ref: pubRTId } }
|
|
989
|
+
};
|
|
990
|
+
resources[privSubnetId] = {
|
|
991
|
+
Type: "AWS::EC2::Subnet",
|
|
992
|
+
Properties: {
|
|
993
|
+
VpcId: { Ref: logicalId },
|
|
994
|
+
CidrBlock: `${cidrBase}.${i * 2 + 1}.0/24`,
|
|
995
|
+
AvailabilityZone: { "Fn::Select": [i, { "Fn::GetAZs": "" }] },
|
|
996
|
+
Tags: [{ Key: "Name", Value: privSubnetId }]
|
|
997
|
+
}
|
|
998
|
+
};
|
|
999
|
+
});
|
|
1000
|
+
}
|
|
1001
|
+
function synthesize(stack) {
|
|
1002
|
+
const resources = {};
|
|
1003
|
+
for (const construct of stack.constructs) {
|
|
1004
|
+
const entries = synthesizeConstruct(construct);
|
|
1005
|
+
for (const [id, resource] of entries) {
|
|
1006
|
+
resources[id] = resource;
|
|
1007
|
+
}
|
|
1008
|
+
if (construct.type === "Network.VPC") {
|
|
1009
|
+
const p = construct.props;
|
|
1010
|
+
const logicalId = construct.id.replace(/[^a-zA-Z0-9]/g, "");
|
|
1011
|
+
synthesizeVPCChildren(logicalId, p.cidr ?? "10.0.0.0/16", p.maxAzs ?? 0, resources);
|
|
1012
|
+
}
|
|
1013
|
+
}
|
|
1014
|
+
return {
|
|
1015
|
+
AWSTemplateFormatVersion: "2010-09-09",
|
|
1016
|
+
Description: `Stack ${stack.name} \u2014 gerada pelo iacmp`,
|
|
1017
|
+
Resources: resources
|
|
1018
|
+
};
|
|
1019
|
+
}
|
|
1020
|
+
}
|
|
1021
|
+
});
|
|
1022
|
+
|
|
1023
|
+
// ../providers/aws/dist/provider.js
|
|
1024
|
+
var require_provider = __commonJS({
|
|
1025
|
+
"../providers/aws/dist/provider.js"(exports2) {
|
|
1026
|
+
"use strict";
|
|
1027
|
+
Object.defineProperty(exports2, "__esModule", { value: true });
|
|
1028
|
+
exports2.AWSProvider = void 0;
|
|
1029
|
+
var cloudformation_1 = require_cloudformation();
|
|
1030
|
+
var AWSProvider2 = class {
|
|
1031
|
+
name = "aws";
|
|
1032
|
+
synthesize(stack) {
|
|
1033
|
+
return (0, cloudformation_1.synthesize)(stack);
|
|
1034
|
+
}
|
|
1035
|
+
};
|
|
1036
|
+
exports2.AWSProvider = AWSProvider2;
|
|
1037
|
+
}
|
|
1038
|
+
});
|
|
1039
|
+
|
|
1040
|
+
// ../providers/aws/dist/index.js
|
|
1041
|
+
var require_dist = __commonJS({
|
|
1042
|
+
"../providers/aws/dist/index.js"(exports2) {
|
|
1043
|
+
"use strict";
|
|
1044
|
+
Object.defineProperty(exports2, "__esModule", { value: true });
|
|
1045
|
+
exports2.synthesize = exports2.AWSProvider = void 0;
|
|
1046
|
+
var provider_1 = require_provider();
|
|
1047
|
+
Object.defineProperty(exports2, "AWSProvider", { enumerable: true, get: function() {
|
|
1048
|
+
return provider_1.AWSProvider;
|
|
1049
|
+
} });
|
|
1050
|
+
var cloudformation_1 = require_cloudformation();
|
|
1051
|
+
Object.defineProperty(exports2, "synthesize", { enumerable: true, get: function() {
|
|
1052
|
+
return cloudformation_1.synthesize;
|
|
1053
|
+
} });
|
|
1054
|
+
}
|
|
1055
|
+
});
|
|
1056
|
+
|
|
1057
|
+
// ../providers/azure/dist/synth/arm.js
|
|
1058
|
+
var require_arm = __commonJS({
|
|
1059
|
+
"../providers/azure/dist/synth/arm.js"(exports2) {
|
|
1060
|
+
"use strict";
|
|
1061
|
+
Object.defineProperty(exports2, "__esModule", { value: true });
|
|
1062
|
+
exports2.synthesize = synthesize;
|
|
1063
|
+
var INSTANCE_TYPE_MAP = {
|
|
1064
|
+
small: "Standard_B1s",
|
|
1065
|
+
medium: "Standard_B2s",
|
|
1066
|
+
large: "Standard_B4ms"
|
|
1067
|
+
};
|
|
1068
|
+
var IMAGE_MAP = {
|
|
1069
|
+
"ubuntu": { publisher: "Canonical", offer: "UbuntuServer", sku: "22_04-lts", version: "latest", isWindows: false },
|
|
1070
|
+
"ubuntu-22.04": { publisher: "Canonical", offer: "UbuntuServer", sku: "22_04-lts", version: "latest", isWindows: false },
|
|
1071
|
+
"ubuntu-20.04": { publisher: "Canonical", offer: "UbuntuServer", sku: "20_04-lts", version: "latest", isWindows: false },
|
|
1072
|
+
"windows-2022": { publisher: "MicrosoftWindowsServer", offer: "WindowsServer", sku: "2022-Datacenter", version: "latest", isWindows: true },
|
|
1073
|
+
"windows-2019": { publisher: "MicrosoftWindowsServer", offer: "WindowsServer", sku: "2019-Datacenter", version: "latest", isWindows: true },
|
|
1074
|
+
"windows-2016": { publisher: "MicrosoftWindowsServer", offer: "WindowsServer", sku: "2016-Datacenter", version: "latest", isWindows: true }
|
|
1075
|
+
};
|
|
1076
|
+
function resolveAzureImage(image) {
|
|
1077
|
+
const mapped = IMAGE_MAP[image];
|
|
1078
|
+
if (mapped) {
|
|
1079
|
+
const { isWindows, ...ref } = mapped;
|
|
1080
|
+
return { imageReference: ref, isWindows };
|
|
1081
|
+
}
|
|
1082
|
+
return { imageReference: { offer: image }, isWindows: false };
|
|
1083
|
+
}
|
|
1084
|
+
var CACHE_SKU_MAP = {
|
|
1085
|
+
small: { name: "Standard", family: "C", capacity: 1 },
|
|
1086
|
+
medium: { name: "Standard", family: "C", capacity: 2 },
|
|
1087
|
+
large: { name: "Premium", family: "P", capacity: 1 }
|
|
1088
|
+
};
|
|
1089
|
+
function tag(name) {
|
|
1090
|
+
return { Name: name };
|
|
1091
|
+
}
|
|
1092
|
+
function synthesizeConstruct(construct) {
|
|
1093
|
+
const props = construct.props;
|
|
1094
|
+
const location = props.region ?? "[resourceGroup().location]";
|
|
1095
|
+
switch (construct.type) {
|
|
1096
|
+
// ── Compute ──────────────────────────────────────────────────────────
|
|
1097
|
+
case "Compute.Instance": {
|
|
1098
|
+
const { imageReference, isWindows } = resolveAzureImage(props.image ?? "ubuntu");
|
|
1099
|
+
return [{
|
|
1100
|
+
type: "Microsoft.Compute/virtualMachines",
|
|
1101
|
+
apiVersion: "2023-03-01",
|
|
1102
|
+
name: construct.id,
|
|
1103
|
+
location,
|
|
1104
|
+
tags: tag(construct.id),
|
|
1105
|
+
properties: {
|
|
1106
|
+
hardwareProfile: { vmSize: INSTANCE_TYPE_MAP[props.instanceType] ?? "Standard_B1s" },
|
|
1107
|
+
storageProfile: { imageReference },
|
|
1108
|
+
osProfile: {
|
|
1109
|
+
computerName: construct.id,
|
|
1110
|
+
adminUsername: isWindows ? "adminuser" : "azureuser",
|
|
1111
|
+
...isWindows ? { windowsConfiguration: { provisionVMAgent: true, enableAutomaticUpdates: true } } : { linuxConfiguration: { disablePasswordAuthentication: true } }
|
|
1112
|
+
}
|
|
1113
|
+
}
|
|
1114
|
+
}];
|
|
1115
|
+
}
|
|
1116
|
+
case "Compute.AutoScaling": {
|
|
1117
|
+
const { imageReference: asImageRef, isWindows: asIsWindows } = resolveAzureImage(props.image ?? "ubuntu");
|
|
1118
|
+
const vmssBody = {
|
|
1119
|
+
type: "Microsoft.Compute/virtualMachineScaleSets",
|
|
1120
|
+
apiVersion: "2023-03-01",
|
|
1121
|
+
name: construct.id,
|
|
1122
|
+
location,
|
|
1123
|
+
tags: tag(construct.id),
|
|
1124
|
+
sku: {
|
|
1125
|
+
name: INSTANCE_TYPE_MAP[props.instanceType] ?? "Standard_B1s",
|
|
1126
|
+
tier: "Standard",
|
|
1127
|
+
capacity: props.desiredCapacity ?? props.minCapacity
|
|
1128
|
+
},
|
|
1129
|
+
properties: {
|
|
1130
|
+
overprovision: true,
|
|
1131
|
+
upgradePolicy: { mode: "Automatic" },
|
|
1132
|
+
virtualMachineProfile: {
|
|
1133
|
+
storageProfile: { imageReference: asImageRef },
|
|
1134
|
+
osProfile: {
|
|
1135
|
+
computerNamePrefix: construct.id.slice(0, 9),
|
|
1136
|
+
adminUsername: asIsWindows ? "adminuser" : "azureuser",
|
|
1137
|
+
...asIsWindows ? { windowsConfiguration: { provisionVMAgent: true, enableAutomaticUpdates: true } } : { linuxConfiguration: { disablePasswordAuthentication: true } }
|
|
1138
|
+
}
|
|
1139
|
+
}
|
|
1140
|
+
}
|
|
1141
|
+
};
|
|
1142
|
+
const autoscaleBody = {
|
|
1143
|
+
type: "Microsoft.Insights/autoscaleSettings",
|
|
1144
|
+
apiVersion: "2022-10-01",
|
|
1145
|
+
name: `${construct.id}-autoscale`,
|
|
1146
|
+
location,
|
|
1147
|
+
properties: {
|
|
1148
|
+
enabled: true,
|
|
1149
|
+
targetResourceUri: `[resourceId('Microsoft.Compute/virtualMachineScaleSets', '${construct.id}')]`,
|
|
1150
|
+
profiles: [{
|
|
1151
|
+
name: "default",
|
|
1152
|
+
capacity: {
|
|
1153
|
+
minimum: String(props.minCapacity ?? 1),
|
|
1154
|
+
maximum: String(props.maxCapacity ?? 10),
|
|
1155
|
+
default: String(props.desiredCapacity ?? props.minCapacity ?? 1)
|
|
1156
|
+
},
|
|
1157
|
+
rules: props.targetCpuUtilization ? [{
|
|
1158
|
+
metricTrigger: {
|
|
1159
|
+
metricName: "Percentage CPU",
|
|
1160
|
+
metricResourceUri: `[resourceId('Microsoft.Compute/virtualMachineScaleSets', '${construct.id}')]`,
|
|
1161
|
+
timeGrain: "PT1M",
|
|
1162
|
+
statistic: "Average",
|
|
1163
|
+
timeWindow: "PT5M",
|
|
1164
|
+
timeAggregation: "Average",
|
|
1165
|
+
operator: "GreaterThan",
|
|
1166
|
+
threshold: props.targetCpuUtilization
|
|
1167
|
+
},
|
|
1168
|
+
scaleAction: { direction: "Increase", type: "ChangeCount", value: "1", cooldown: "PT5M" }
|
|
1169
|
+
}] : []
|
|
1170
|
+
}]
|
|
1171
|
+
}
|
|
1172
|
+
};
|
|
1173
|
+
return [vmssBody, autoscaleBody];
|
|
1174
|
+
}
|
|
1175
|
+
case "Compute.Container": {
|
|
1176
|
+
const environment = props.environment ?? {};
|
|
1177
|
+
const envVars = Object.entries(environment).map(([k, v]) => ({ name: k, value: v }));
|
|
1178
|
+
return [{
|
|
1179
|
+
type: "Microsoft.ContainerInstance/containerGroups",
|
|
1180
|
+
apiVersion: "2023-05-01",
|
|
1181
|
+
name: construct.id,
|
|
1182
|
+
location,
|
|
1183
|
+
tags: tag(construct.id),
|
|
1184
|
+
properties: {
|
|
1185
|
+
containers: [{
|
|
1186
|
+
name: construct.id,
|
|
1187
|
+
properties: {
|
|
1188
|
+
image: props.image,
|
|
1189
|
+
resources: {
|
|
1190
|
+
requests: {
|
|
1191
|
+
cpu: Math.round((props.cpu ?? 256) / 1024 * 10) / 10,
|
|
1192
|
+
memoryInGB: Math.round((props.memory ?? 512) / 1024 * 10) / 10
|
|
1193
|
+
}
|
|
1194
|
+
},
|
|
1195
|
+
ports: props.port ? [{ port: props.port, protocol: "TCP" }] : [],
|
|
1196
|
+
environmentVariables: envVars
|
|
1197
|
+
}
|
|
1198
|
+
}],
|
|
1199
|
+
osType: "Linux",
|
|
1200
|
+
ipAddress: {
|
|
1201
|
+
type: "Public",
|
|
1202
|
+
ports: props.port ? [{ port: props.port, protocol: "TCP" }] : []
|
|
1203
|
+
},
|
|
1204
|
+
restartPolicy: "OnFailure"
|
|
1205
|
+
}
|
|
1206
|
+
}];
|
|
1207
|
+
}
|
|
1208
|
+
case "Compute.Kubernetes": {
|
|
1209
|
+
const nodeType = INSTANCE_TYPE_MAP[props.nodeInstanceType] ?? "Standard_B2s";
|
|
1210
|
+
return [{
|
|
1211
|
+
type: "Microsoft.ContainerService/managedClusters",
|
|
1212
|
+
apiVersion: "2023-05-01",
|
|
1213
|
+
name: construct.id,
|
|
1214
|
+
location,
|
|
1215
|
+
tags: tag(construct.id),
|
|
1216
|
+
identity: { type: "SystemAssigned" },
|
|
1217
|
+
properties: {
|
|
1218
|
+
kubernetesVersion: props.version ?? "1.29",
|
|
1219
|
+
dnsPrefix: construct.id,
|
|
1220
|
+
enableRBAC: true,
|
|
1221
|
+
agentPoolProfiles: [{
|
|
1222
|
+
name: "nodepool1",
|
|
1223
|
+
count: props.desiredNodes ?? 2,
|
|
1224
|
+
minCount: props.minNodes ?? 1,
|
|
1225
|
+
maxCount: props.maxNodes ?? 3,
|
|
1226
|
+
enableAutoScaling: true,
|
|
1227
|
+
vmSize: nodeType,
|
|
1228
|
+
mode: "System"
|
|
1229
|
+
}],
|
|
1230
|
+
apiServerAccessProfile: {
|
|
1231
|
+
enablePrivateCluster: props.privateCluster ?? false
|
|
1232
|
+
},
|
|
1233
|
+
networkProfile: { networkPlugin: "kubenet", loadBalancerSku: "standard" }
|
|
1234
|
+
}
|
|
1235
|
+
}];
|
|
1236
|
+
}
|
|
1237
|
+
// ── Storage ───────────────────────────────────────────────────────────
|
|
1238
|
+
case "Storage.Bucket": {
|
|
1239
|
+
const resources = [{
|
|
1240
|
+
type: "Microsoft.Storage/storageAccounts",
|
|
1241
|
+
apiVersion: "2023-01-01",
|
|
1242
|
+
name: construct.id.toLowerCase().replace(/[^a-z0-9]/g, ""),
|
|
1243
|
+
location,
|
|
1244
|
+
kind: "StorageV2",
|
|
1245
|
+
sku: { name: "Standard_LRS" },
|
|
1246
|
+
tags: tag(construct.id),
|
|
1247
|
+
properties: {
|
|
1248
|
+
allowBlobPublicAccess: props.publicAccess ?? false,
|
|
1249
|
+
supportsHttpsTrafficOnly: true,
|
|
1250
|
+
minimumTlsVersion: "TLS1_2"
|
|
1251
|
+
}
|
|
1252
|
+
}];
|
|
1253
|
+
if (props.versioning) {
|
|
1254
|
+
resources.push({
|
|
1255
|
+
type: "Microsoft.Storage/storageAccounts/blobServices",
|
|
1256
|
+
apiVersion: "2023-01-01",
|
|
1257
|
+
name: `${construct.id.toLowerCase().replace(/[^a-z0-9]/g, "")}/default`,
|
|
1258
|
+
location,
|
|
1259
|
+
properties: { isVersioningEnabled: true }
|
|
1260
|
+
});
|
|
1261
|
+
}
|
|
1262
|
+
return resources;
|
|
1263
|
+
}
|
|
1264
|
+
case "Storage.FileSystem":
|
|
1265
|
+
return [{
|
|
1266
|
+
type: "Microsoft.Storage/storageAccounts/fileServices/shares",
|
|
1267
|
+
apiVersion: "2023-01-01",
|
|
1268
|
+
name: `${construct.id.toLowerCase().replace(/[^a-z0-9]/g, "")}share/default/${construct.id}`,
|
|
1269
|
+
location,
|
|
1270
|
+
properties: {
|
|
1271
|
+
shareQuota: 100,
|
|
1272
|
+
enabledProtocols: "SMB",
|
|
1273
|
+
accessTier: "Hot"
|
|
1274
|
+
}
|
|
1275
|
+
}];
|
|
1276
|
+
case "Storage.Archive":
|
|
1277
|
+
return [{
|
|
1278
|
+
type: "Microsoft.Storage/storageAccounts",
|
|
1279
|
+
apiVersion: "2023-01-01",
|
|
1280
|
+
name: `${construct.id.toLowerCase().replace(/[^a-z0-9]/g, "")}arc`,
|
|
1281
|
+
location,
|
|
1282
|
+
kind: "BlobStorage",
|
|
1283
|
+
sku: { name: "Standard_LRS" },
|
|
1284
|
+
tags: tag(construct.id),
|
|
1285
|
+
properties: {
|
|
1286
|
+
accessTier: "Archive",
|
|
1287
|
+
allowBlobPublicAccess: false,
|
|
1288
|
+
supportsHttpsTrafficOnly: true,
|
|
1289
|
+
minimumTlsVersion: "TLS1_2"
|
|
1290
|
+
}
|
|
1291
|
+
}];
|
|
1292
|
+
// ── Network ───────────────────────────────────────────────────────────
|
|
1293
|
+
case "Network.VPC":
|
|
1294
|
+
return [{
|
|
1295
|
+
type: "Microsoft.Network/virtualNetworks",
|
|
1296
|
+
apiVersion: "2023-04-01",
|
|
1297
|
+
name: construct.id,
|
|
1298
|
+
location,
|
|
1299
|
+
tags: tag(construct.id),
|
|
1300
|
+
properties: {
|
|
1301
|
+
addressSpace: { addressPrefixes: [props.cidr ?? "10.0.0.0/16"] },
|
|
1302
|
+
dhcpOptions: { dnsServers: [] }
|
|
1303
|
+
}
|
|
1304
|
+
}];
|
|
1305
|
+
case "Network.Subnet": {
|
|
1306
|
+
const vnetName = props.vpcId;
|
|
1307
|
+
return [{
|
|
1308
|
+
type: "Microsoft.Network/virtualNetworks/subnets",
|
|
1309
|
+
apiVersion: "2023-04-01",
|
|
1310
|
+
name: `${vnetName}/${construct.id}`,
|
|
1311
|
+
location,
|
|
1312
|
+
dependsOn: [`[resourceId('Microsoft.Network/virtualNetworks', '${vnetName}')]`],
|
|
1313
|
+
properties: {
|
|
1314
|
+
addressPrefix: props.cidr,
|
|
1315
|
+
privateEndpointNetworkPolicies: props.public ? "Disabled" : "Enabled"
|
|
1316
|
+
}
|
|
1317
|
+
}];
|
|
1318
|
+
}
|
|
1319
|
+
case "Network.SecurityGroup": {
|
|
1320
|
+
const ingress = props.ingressRules ?? [];
|
|
1321
|
+
const egress = props.egressRules ?? [];
|
|
1322
|
+
const protocolMap = {
|
|
1323
|
+
tcp: "Tcp",
|
|
1324
|
+
udp: "Udp",
|
|
1325
|
+
icmp: "Icmp",
|
|
1326
|
+
"-1": "*"
|
|
1327
|
+
};
|
|
1328
|
+
const mapProtocol = (raw) => {
|
|
1329
|
+
if (raw === void 0 || raw === null)
|
|
1330
|
+
return "*";
|
|
1331
|
+
const key = String(raw).toLowerCase();
|
|
1332
|
+
return protocolMap[key] ?? "*";
|
|
1333
|
+
};
|
|
1334
|
+
const secRules = [
|
|
1335
|
+
...ingress.map((r, i) => {
|
|
1336
|
+
if (r.cidr === void 0) {
|
|
1337
|
+
console.warn(`[azure] Security group rule sem CIDR; usando * \u2014 defina props.cidr explicitamente (${construct.id} ingress[${i}])`);
|
|
1338
|
+
}
|
|
1339
|
+
return {
|
|
1340
|
+
name: `ingress-rule-${i}`,
|
|
1341
|
+
properties: {
|
|
1342
|
+
priority: 100 + i,
|
|
1343
|
+
direction: "Inbound",
|
|
1344
|
+
access: "Allow",
|
|
1345
|
+
protocol: mapProtocol(r.protocol),
|
|
1346
|
+
sourcePortRange: "*",
|
|
1347
|
+
destinationPortRange: r.fromPort === r.toPort ? String(r.fromPort) : `${r.fromPort}-${r.toPort}`,
|
|
1348
|
+
sourceAddressPrefix: r.cidr ?? "*",
|
|
1349
|
+
destinationAddressPrefix: "*",
|
|
1350
|
+
description: r.description ?? ""
|
|
1351
|
+
}
|
|
1352
|
+
};
|
|
1353
|
+
}),
|
|
1354
|
+
...egress.map((r, i) => ({
|
|
1355
|
+
name: `egress-rule-${i}`,
|
|
1356
|
+
properties: {
|
|
1357
|
+
priority: 200 + i,
|
|
1358
|
+
direction: "Outbound",
|
|
1359
|
+
access: "Allow",
|
|
1360
|
+
protocol: mapProtocol(r.protocol),
|
|
1361
|
+
sourcePortRange: "*",
|
|
1362
|
+
destinationPortRange: r.fromPort === r.toPort ? String(r.fromPort) : `${r.fromPort}-${r.toPort}`,
|
|
1363
|
+
sourceAddressPrefix: "*",
|
|
1364
|
+
destinationAddressPrefix: r.cidr ?? "*",
|
|
1365
|
+
description: r.description ?? ""
|
|
1366
|
+
}
|
|
1367
|
+
}))
|
|
1368
|
+
];
|
|
1369
|
+
return [{
|
|
1370
|
+
type: "Microsoft.Network/networkSecurityGroups",
|
|
1371
|
+
apiVersion: "2023-04-01",
|
|
1372
|
+
name: construct.id,
|
|
1373
|
+
location,
|
|
1374
|
+
tags: tag(construct.id),
|
|
1375
|
+
properties: { securityRules: secRules }
|
|
1376
|
+
}];
|
|
1377
|
+
}
|
|
1378
|
+
case "Network.WAF": {
|
|
1379
|
+
const rules = props.rules ?? [];
|
|
1380
|
+
const customRules = rules.filter((r) => !r.managedGroup).map((r, i) => ({
|
|
1381
|
+
name: r.name ?? `custom-rule-${i}`,
|
|
1382
|
+
priority: r.priority ?? i + 1,
|
|
1383
|
+
ruleType: "MatchRule",
|
|
1384
|
+
action: r.action ?? "Block",
|
|
1385
|
+
matchConditions: [{
|
|
1386
|
+
matchVariables: [{ variableName: "RequestHeaders", selector: "User-Agent" }],
|
|
1387
|
+
operator: "Contains",
|
|
1388
|
+
matchValues: r.matchValues ?? ["BadBot"]
|
|
1389
|
+
}]
|
|
1390
|
+
}));
|
|
1391
|
+
const managedRules = rules.filter((r) => r.managedGroup).map((r) => ({
|
|
1392
|
+
ruleSetType: r.managedGroup ?? "OWASP",
|
|
1393
|
+
ruleSetVersion: "3.2"
|
|
1394
|
+
}));
|
|
1395
|
+
return [{
|
|
1396
|
+
type: "Microsoft.Network/ApplicationGatewayWebApplicationFirewallPolicies",
|
|
1397
|
+
apiVersion: "2023-04-01",
|
|
1398
|
+
name: construct.id,
|
|
1399
|
+
location,
|
|
1400
|
+
tags: tag(construct.id),
|
|
1401
|
+
properties: {
|
|
1402
|
+
policySettings: {
|
|
1403
|
+
requestBodyCheck: true,
|
|
1404
|
+
maxRequestBodySizeInKb: 128,
|
|
1405
|
+
fileUploadLimitInMb: 100,
|
|
1406
|
+
state: "Enabled",
|
|
1407
|
+
mode: props.mode ?? "Prevention"
|
|
1408
|
+
},
|
|
1409
|
+
customRules,
|
|
1410
|
+
managedRules: {
|
|
1411
|
+
managedRuleSets: managedRules.length > 0 ? managedRules : [{ ruleSetType: "OWASP", ruleSetVersion: "3.2" }]
|
|
1412
|
+
}
|
|
1413
|
+
}
|
|
1414
|
+
}];
|
|
1415
|
+
}
|
|
1416
|
+
case "Network.LoadBalancer": {
|
|
1417
|
+
const lbType = props.type ?? "application";
|
|
1418
|
+
const isInternal = props.scheme === "internal";
|
|
1419
|
+
const listeners = props.listeners ?? [];
|
|
1420
|
+
const targetGroups = props.targetGroups ?? [];
|
|
1421
|
+
if (lbType === "application") {
|
|
1422
|
+
return [{
|
|
1423
|
+
type: "Microsoft.Network/applicationGateways",
|
|
1424
|
+
apiVersion: "2023-04-01",
|
|
1425
|
+
name: construct.id,
|
|
1426
|
+
location,
|
|
1427
|
+
tags: tag(construct.id),
|
|
1428
|
+
properties: {
|
|
1429
|
+
sku: { name: "Standard_v2", tier: "Standard_v2", capacity: 2 },
|
|
1430
|
+
frontendIPConfigurations: [{ name: "appGatewayFrontendIP", properties: { publicIPAddress: null } }],
|
|
1431
|
+
frontendPorts: listeners.map((l, i) => ({ name: `port${i}`, properties: { port: l.port } })),
|
|
1432
|
+
backendAddressPools: targetGroups.map((tg) => ({ name: tg.name, properties: {} })),
|
|
1433
|
+
httpListeners: listeners.map((l, i) => ({
|
|
1434
|
+
name: `listener${i}`,
|
|
1435
|
+
properties: {
|
|
1436
|
+
frontendPort: { id: `port${i}` },
|
|
1437
|
+
protocol: l.protocol.toLowerCase() === "https" ? "Https" : "Http"
|
|
1438
|
+
}
|
|
1439
|
+
})),
|
|
1440
|
+
requestRoutingRules: [{
|
|
1441
|
+
name: "rule1",
|
|
1442
|
+
properties: { ruleType: "Basic", priority: 100 }
|
|
1443
|
+
}]
|
|
1444
|
+
}
|
|
1445
|
+
}];
|
|
1446
|
+
}
|
|
1447
|
+
return [{
|
|
1448
|
+
type: "Microsoft.Network/loadBalancers",
|
|
1449
|
+
apiVersion: "2023-04-01",
|
|
1450
|
+
name: construct.id,
|
|
1451
|
+
location,
|
|
1452
|
+
tags: tag(construct.id),
|
|
1453
|
+
sku: { name: "Standard" },
|
|
1454
|
+
properties: {
|
|
1455
|
+
frontendIPConfigurations: [{ name: "loadBalancerFrontEnd", properties: {} }],
|
|
1456
|
+
backendAddressPools: targetGroups.map((tg) => ({ name: tg.name })),
|
|
1457
|
+
loadBalancingRules: listeners.map((l, i) => ({
|
|
1458
|
+
name: `rule${i}`,
|
|
1459
|
+
properties: {
|
|
1460
|
+
frontendPort: l.port,
|
|
1461
|
+
backendPort: l.port,
|
|
1462
|
+
protocol: l.protocol.toLowerCase() === "tcp" ? "Tcp" : "Udp",
|
|
1463
|
+
enableFloatingIP: false
|
|
1464
|
+
}
|
|
1465
|
+
}))
|
|
1466
|
+
}
|
|
1467
|
+
}];
|
|
1468
|
+
}
|
|
1469
|
+
case "Network.CDN": {
|
|
1470
|
+
const origins = props.origins ?? [];
|
|
1471
|
+
return [{
|
|
1472
|
+
type: "Microsoft.Cdn/profiles/endpoints",
|
|
1473
|
+
apiVersion: "2023-05-01",
|
|
1474
|
+
name: `${construct.id}-profile/${construct.id}`,
|
|
1475
|
+
location,
|
|
1476
|
+
tags: tag(construct.id),
|
|
1477
|
+
properties: {
|
|
1478
|
+
originHostHeader: origins[0]?.domainName ?? "",
|
|
1479
|
+
isHttpAllowed: false,
|
|
1480
|
+
isHttpsAllowed: true,
|
|
1481
|
+
origins: origins.map((o) => ({
|
|
1482
|
+
name: o.id ?? "origin1",
|
|
1483
|
+
properties: { hostName: o.domainName, httpPort: 80, httpsPort: 443 }
|
|
1484
|
+
})),
|
|
1485
|
+
deliveryPolicy: {
|
|
1486
|
+
rules: [{
|
|
1487
|
+
name: "enforceHttps",
|
|
1488
|
+
order: 1,
|
|
1489
|
+
conditions: [{ name: "RequestScheme", parameters: { operator: "Equal", matchValues: ["HTTP"] } }],
|
|
1490
|
+
actions: [{ name: "UrlRedirect", parameters: { redirectType: "Moved", destinationProtocol: "Https" } }]
|
|
1491
|
+
}]
|
|
1492
|
+
}
|
|
1493
|
+
}
|
|
1494
|
+
}];
|
|
1495
|
+
}
|
|
1496
|
+
case "Network.Dns": {
|
|
1497
|
+
const records = props.records ?? [];
|
|
1498
|
+
const zoneName = props.zoneName;
|
|
1499
|
+
const resources = [{
|
|
1500
|
+
type: "Microsoft.Network/dnsZones",
|
|
1501
|
+
apiVersion: "2018-05-01",
|
|
1502
|
+
name: zoneName,
|
|
1503
|
+
location: "global",
|
|
1504
|
+
tags: tag(construct.id),
|
|
1505
|
+
properties: {}
|
|
1506
|
+
}];
|
|
1507
|
+
for (const r of records) {
|
|
1508
|
+
const recordType = r.type.toLowerCase();
|
|
1509
|
+
resources.push({
|
|
1510
|
+
type: `Microsoft.Network/dnsZones/${recordType}`,
|
|
1511
|
+
apiVersion: "2018-05-01",
|
|
1512
|
+
name: `${zoneName}/${r.name}`,
|
|
1513
|
+
location: "global",
|
|
1514
|
+
dependsOn: [`[resourceId('Microsoft.Network/dnsZones', '${zoneName}')]`],
|
|
1515
|
+
properties: {
|
|
1516
|
+
TTL: r.ttl ?? 300,
|
|
1517
|
+
[`${recordType.toUpperCase()}Records`]: r.values.map((v) => ({ value: v }))
|
|
1518
|
+
}
|
|
1519
|
+
});
|
|
1520
|
+
}
|
|
1521
|
+
return resources;
|
|
1522
|
+
}
|
|
1523
|
+
// ── Database ──────────────────────────────────────────────────────────
|
|
1524
|
+
case "Database.SQL": {
|
|
1525
|
+
const engine = props.engine ?? "mysql";
|
|
1526
|
+
const serverName = `${construct.id.toLowerCase()}-server`;
|
|
1527
|
+
const storageBytes = (props.storageGb ?? 20) * 1024 * 1024 * 1024;
|
|
1528
|
+
const zoneRedundant = props.multiAz ?? false;
|
|
1529
|
+
if (engine === "mysql") {
|
|
1530
|
+
return [
|
|
1531
|
+
{
|
|
1532
|
+
type: "Microsoft.DBforMySQL/flexibleServers",
|
|
1533
|
+
apiVersion: "2023-06-30",
|
|
1534
|
+
name: serverName,
|
|
1535
|
+
location,
|
|
1536
|
+
tags: tag(construct.id),
|
|
1537
|
+
sku: { name: "Standard_D2ds_v4", tier: "GeneralPurpose" },
|
|
1538
|
+
properties: {
|
|
1539
|
+
administratorLogin: "mysqladmin",
|
|
1540
|
+
administratorLoginPassword: "[parameters('adminPassword')]",
|
|
1541
|
+
version: "8.0.21",
|
|
1542
|
+
storage: { storageSizeGB: props.storageGb ?? 20, autoGrow: "Enabled" },
|
|
1543
|
+
backup: { backupRetentionDays: props.backupRetentionDays ?? 7, geoRedundantBackup: "Disabled" },
|
|
1544
|
+
highAvailability: { mode: zoneRedundant ? "ZoneRedundant" : "Disabled" }
|
|
1545
|
+
}
|
|
1546
|
+
}
|
|
1547
|
+
];
|
|
1548
|
+
}
|
|
1549
|
+
if (engine === "postgres") {
|
|
1550
|
+
return [
|
|
1551
|
+
{
|
|
1552
|
+
type: "Microsoft.DBforPostgreSQL/flexibleServers",
|
|
1553
|
+
apiVersion: "2023-06-01-preview",
|
|
1554
|
+
name: serverName,
|
|
1555
|
+
location,
|
|
1556
|
+
tags: tag(construct.id),
|
|
1557
|
+
sku: { name: "Standard_D2ds_v5", tier: "GeneralPurpose" },
|
|
1558
|
+
properties: {
|
|
1559
|
+
administratorLogin: "pgadmin",
|
|
1560
|
+
administratorLoginPassword: "[parameters('adminPassword')]",
|
|
1561
|
+
version: "15",
|
|
1562
|
+
storage: { storageSizeGB: props.storageGb ?? 32 },
|
|
1563
|
+
backup: { backupRetentionDays: props.backupRetentionDays ?? 7, geoRedundantBackup: "Disabled" },
|
|
1564
|
+
highAvailability: { mode: zoneRedundant ? "ZoneRedundant" : "Disabled" }
|
|
1565
|
+
}
|
|
1566
|
+
}
|
|
1567
|
+
];
|
|
1568
|
+
}
|
|
1569
|
+
if (engine === "mariadb") {
|
|
1570
|
+
return [
|
|
1571
|
+
{
|
|
1572
|
+
type: "Microsoft.DBforMariaDB/servers",
|
|
1573
|
+
apiVersion: "2018-06-01",
|
|
1574
|
+
name: serverName,
|
|
1575
|
+
location,
|
|
1576
|
+
tags: tag(construct.id),
|
|
1577
|
+
sku: { name: "GP_Gen5_2", tier: "GeneralPurpose", capacity: 2, family: "Gen5" },
|
|
1578
|
+
properties: {
|
|
1579
|
+
administratorLogin: "mariadbadmin",
|
|
1580
|
+
administratorLoginPassword: "[parameters('adminPassword')]",
|
|
1581
|
+
version: "10.3",
|
|
1582
|
+
storageProfile: {
|
|
1583
|
+
storageMB: (props.storageGb ?? 20) * 1024,
|
|
1584
|
+
backupRetentionDays: props.backupRetentionDays ?? 7,
|
|
1585
|
+
geoRedundantBackup: "Disabled"
|
|
1586
|
+
}
|
|
1587
|
+
}
|
|
1588
|
+
}
|
|
1589
|
+
];
|
|
1590
|
+
}
|
|
1591
|
+
if (engine === "oracle") {
|
|
1592
|
+
return [
|
|
1593
|
+
{
|
|
1594
|
+
type: "Oracle.Database/cloudExadataInfrastructures",
|
|
1595
|
+
apiVersion: "2023-09-01",
|
|
1596
|
+
name: serverName,
|
|
1597
|
+
location,
|
|
1598
|
+
tags: tag(construct.id),
|
|
1599
|
+
properties: {
|
|
1600
|
+
displayName: construct.id,
|
|
1601
|
+
shape: "Exadata.X9M",
|
|
1602
|
+
computeCount: 2,
|
|
1603
|
+
storageCount: 3
|
|
1604
|
+
}
|
|
1605
|
+
}
|
|
1606
|
+
];
|
|
1607
|
+
}
|
|
1608
|
+
const edition = props.edition ?? "Standard";
|
|
1609
|
+
return [
|
|
1610
|
+
{
|
|
1611
|
+
type: "Microsoft.Sql/servers",
|
|
1612
|
+
apiVersion: "2023-02-01-preview",
|
|
1613
|
+
name: serverName,
|
|
1614
|
+
location,
|
|
1615
|
+
tags: tag(construct.id),
|
|
1616
|
+
properties: {
|
|
1617
|
+
administratorLogin: "sqladmin",
|
|
1618
|
+
administratorLoginPassword: "[parameters('adminPassword')]",
|
|
1619
|
+
version: "12.0"
|
|
1620
|
+
}
|
|
1621
|
+
},
|
|
1622
|
+
{
|
|
1623
|
+
type: "Microsoft.Sql/servers/databases",
|
|
1624
|
+
apiVersion: "2023-02-01-preview",
|
|
1625
|
+
name: `${serverName}/${construct.id}`,
|
|
1626
|
+
location,
|
|
1627
|
+
dependsOn: [`[resourceId('Microsoft.Sql/servers', '${serverName}')]`],
|
|
1628
|
+
sku: { name: edition === "ee" ? "BusinessCritical" : "Standard", tier: edition === "ee" ? "BusinessCritical" : "Standard" },
|
|
1629
|
+
properties: {
|
|
1630
|
+
collation: "SQL_Latin1_General_CP1_CI_AS",
|
|
1631
|
+
maxSizeBytes: storageBytes,
|
|
1632
|
+
zoneRedundant
|
|
1633
|
+
}
|
|
1634
|
+
}
|
|
1635
|
+
];
|
|
1636
|
+
}
|
|
1637
|
+
case "Database.DocumentDB":
|
|
1638
|
+
return [{
|
|
1639
|
+
type: "Microsoft.DocumentDB/databaseAccounts",
|
|
1640
|
+
apiVersion: "2023-04-15",
|
|
1641
|
+
name: construct.id.toLowerCase(),
|
|
1642
|
+
location,
|
|
1643
|
+
tags: tag(construct.id),
|
|
1644
|
+
properties: {
|
|
1645
|
+
databaseAccountOfferType: "Standard",
|
|
1646
|
+
kind: "MongoDB",
|
|
1647
|
+
locations: [{ locationName: location, failoverPriority: 0, isZoneRedundant: false }],
|
|
1648
|
+
backupPolicy: { type: "Periodic", periodicModeProperties: { backupIntervalInMinutes: 1440, backupRetentionIntervalInHours: 168 } },
|
|
1649
|
+
enableAutomaticFailover: props.deletionProtection ?? false
|
|
1650
|
+
}
|
|
1651
|
+
}];
|
|
1652
|
+
case "Database.DynamoDB":
|
|
1653
|
+
return [{
|
|
1654
|
+
type: "Microsoft.DocumentDB/databaseAccounts",
|
|
1655
|
+
apiVersion: "2023-04-15",
|
|
1656
|
+
name: construct.id.toLowerCase(),
|
|
1657
|
+
location,
|
|
1658
|
+
tags: tag(construct.id),
|
|
1659
|
+
properties: {
|
|
1660
|
+
databaseAccountOfferType: "Standard",
|
|
1661
|
+
kind: "GlobalDocumentDB",
|
|
1662
|
+
capabilities: [{ name: "EnableTable" }],
|
|
1663
|
+
locations: [{ locationName: location, failoverPriority: 0, isZoneRedundant: false }],
|
|
1664
|
+
backupPolicy: { type: "Continuous", continuousModeProperties: { tier: "Continuous30Days" } }
|
|
1665
|
+
}
|
|
1666
|
+
}];
|
|
1667
|
+
// ── Cache ─────────────────────────────────────────────────────────────
|
|
1668
|
+
case "Cache.Redis": {
|
|
1669
|
+
const skuInfo = CACHE_SKU_MAP[props.nodeType ?? "small"];
|
|
1670
|
+
return [{
|
|
1671
|
+
type: "Microsoft.Cache/redis",
|
|
1672
|
+
apiVersion: "2023-08-01",
|
|
1673
|
+
name: construct.id,
|
|
1674
|
+
location,
|
|
1675
|
+
tags: tag(construct.id),
|
|
1676
|
+
sku: { name: skuInfo.name, family: skuInfo.family, capacity: skuInfo.capacity },
|
|
1677
|
+
properties: {
|
|
1678
|
+
enableNonSslPort: false,
|
|
1679
|
+
minimumTlsVersion: "1.2",
|
|
1680
|
+
redisVersion: props.version ?? "7.0",
|
|
1681
|
+
redisConfiguration: {
|
|
1682
|
+
"maxmemory-policy": "volatile-lru"
|
|
1683
|
+
}
|
|
1684
|
+
}
|
|
1685
|
+
}];
|
|
1686
|
+
}
|
|
1687
|
+
case "Cache.Memcached":
|
|
1688
|
+
return [{
|
|
1689
|
+
type: "Microsoft.Cache/redis",
|
|
1690
|
+
apiVersion: "2023-08-01",
|
|
1691
|
+
name: `${construct.id}-cache`,
|
|
1692
|
+
location,
|
|
1693
|
+
tags: tag(construct.id),
|
|
1694
|
+
sku: { name: "Standard", family: "C", capacity: props.numCacheNodes ?? 2 },
|
|
1695
|
+
properties: {
|
|
1696
|
+
enableNonSslPort: false,
|
|
1697
|
+
minimumTlsVersion: "1.2",
|
|
1698
|
+
redisConfiguration: {}
|
|
1699
|
+
}
|
|
1700
|
+
}];
|
|
1701
|
+
// ── Function ──────────────────────────────────────────────────────────
|
|
1702
|
+
case "Function.Lambda": {
|
|
1703
|
+
const environment = props.environment ?? {};
|
|
1704
|
+
const runtimeMap = {
|
|
1705
|
+
"nodejs20": "node|20",
|
|
1706
|
+
"nodejs18": "node|18",
|
|
1707
|
+
"python3.12": "python|3.12",
|
|
1708
|
+
"python3.11": "python|3.11",
|
|
1709
|
+
"java21": "java|21",
|
|
1710
|
+
"go1.x": "go|1",
|
|
1711
|
+
"dotnet8": "dotnet|8"
|
|
1712
|
+
};
|
|
1713
|
+
const baseSettings = [
|
|
1714
|
+
{ name: "FUNCTIONS_EXTENSION_VERSION", value: "~4" },
|
|
1715
|
+
{ name: "FUNCTIONS_WORKER_RUNTIME", value: props.runtime?.startsWith("nodejs") ? "node" : props.runtime?.startsWith("python") ? "python" : "dotnet" }
|
|
1716
|
+
];
|
|
1717
|
+
const envSettings = Object.entries(environment).map(([k, v]) => ({ name: k, value: v }));
|
|
1718
|
+
return [{
|
|
1719
|
+
type: "Microsoft.Web/sites",
|
|
1720
|
+
apiVersion: "2023-01-01",
|
|
1721
|
+
name: construct.id,
|
|
1722
|
+
location,
|
|
1723
|
+
kind: "functionapp",
|
|
1724
|
+
tags: tag(construct.id),
|
|
1725
|
+
properties: {
|
|
1726
|
+
siteConfig: {
|
|
1727
|
+
linuxFxVersion: runtimeMap[props.runtime ?? "nodejs20"] ?? "node|20",
|
|
1728
|
+
appSettings: [...baseSettings, ...envSettings],
|
|
1729
|
+
...props.memory ? { memoryAllocation: props.memory } : {}
|
|
1730
|
+
},
|
|
1731
|
+
httpsOnly: true
|
|
1732
|
+
}
|
|
1733
|
+
}];
|
|
1734
|
+
}
|
|
1735
|
+
case "Function.ApiGateway": {
|
|
1736
|
+
const apimName = props.name ?? construct.id;
|
|
1737
|
+
const authorizerLambdaId = props.authorizerLambdaId;
|
|
1738
|
+
const resources = [{
|
|
1739
|
+
type: "Microsoft.ApiManagement/service",
|
|
1740
|
+
apiVersion: "2023-05-01-preview",
|
|
1741
|
+
name: apimName,
|
|
1742
|
+
location,
|
|
1743
|
+
tags: tag(construct.id),
|
|
1744
|
+
sku: { name: "Consumption", capacity: 0 },
|
|
1745
|
+
properties: {
|
|
1746
|
+
publisherEmail: "admin@example.com",
|
|
1747
|
+
publisherName: construct.id,
|
|
1748
|
+
virtualNetworkType: "None",
|
|
1749
|
+
customProperties: {
|
|
1750
|
+
"Microsoft.WindowsAzure.ApiManagement.Gateway.Security.Protocols.Tls10": "false",
|
|
1751
|
+
"Microsoft.WindowsAzure.ApiManagement.Gateway.Security.Protocols.Tls11": "false"
|
|
1752
|
+
}
|
|
1753
|
+
}
|
|
1754
|
+
}];
|
|
1755
|
+
if (authorizerLambdaId) {
|
|
1756
|
+
resources.push({
|
|
1757
|
+
type: "Microsoft.ApiManagement/service/backends",
|
|
1758
|
+
apiVersion: "2023-05-01-preview",
|
|
1759
|
+
name: `${apimName}/authorizer-backend`,
|
|
1760
|
+
location,
|
|
1761
|
+
properties: {
|
|
1762
|
+
description: `Lambda authorizer backend (${authorizerLambdaId})`,
|
|
1763
|
+
url: `[reference(resourceId('Microsoft.Web/sites', '${authorizerLambdaId}')).defaultHostName]`,
|
|
1764
|
+
protocol: "http"
|
|
1765
|
+
},
|
|
1766
|
+
dependsOn: [
|
|
1767
|
+
`[resourceId('Microsoft.ApiManagement/service', '${apimName}')]`,
|
|
1768
|
+
`[resourceId('Microsoft.Web/sites', '${authorizerLambdaId}')]`
|
|
1769
|
+
]
|
|
1770
|
+
});
|
|
1771
|
+
}
|
|
1772
|
+
return resources;
|
|
1773
|
+
}
|
|
1774
|
+
// ── Policy ────────────────────────────────────────────────────────────
|
|
1775
|
+
case "Policy.IAM": {
|
|
1776
|
+
const statements = props.statements ?? [];
|
|
1777
|
+
const attachType = props.attachType;
|
|
1778
|
+
const attachTo = props.attachTo;
|
|
1779
|
+
const actions = [];
|
|
1780
|
+
const notActions = [];
|
|
1781
|
+
for (const s of statements) {
|
|
1782
|
+
const stmtActions = s.actions;
|
|
1783
|
+
if (s.effect === "Allow")
|
|
1784
|
+
actions.push(...stmtActions);
|
|
1785
|
+
else
|
|
1786
|
+
notActions.push(...stmtActions);
|
|
1787
|
+
}
|
|
1788
|
+
const principalType = attachType === "lambda" ? "FunctionApp" : "VirtualMachine";
|
|
1789
|
+
return [
|
|
1790
|
+
{
|
|
1791
|
+
type: "Microsoft.Authorization/roleDefinitions",
|
|
1792
|
+
apiVersion: "2022-04-01",
|
|
1793
|
+
name: `[guid(resourceGroup().id, '${construct.id}')]`,
|
|
1794
|
+
location,
|
|
1795
|
+
properties: {
|
|
1796
|
+
roleName: `${construct.id}-role`,
|
|
1797
|
+
description: props.description ?? `Custom role for ${attachTo}`,
|
|
1798
|
+
type: "CustomRole",
|
|
1799
|
+
permissions: [{
|
|
1800
|
+
actions,
|
|
1801
|
+
notActions,
|
|
1802
|
+
dataActions: [],
|
|
1803
|
+
notDataActions: []
|
|
1804
|
+
}],
|
|
1805
|
+
assignableScopes: [`[resourceGroup().id]`]
|
|
1806
|
+
}
|
|
1807
|
+
},
|
|
1808
|
+
{
|
|
1809
|
+
type: "Microsoft.Authorization/roleAssignments",
|
|
1810
|
+
apiVersion: "2022-04-01",
|
|
1811
|
+
name: `[guid(resourceGroup().id, '${attachTo}', '${construct.id}')]`,
|
|
1812
|
+
location,
|
|
1813
|
+
dependsOn: [`[resourceId('Microsoft.Authorization/roleDefinitions', guid(resourceGroup().id, '${construct.id}'))]`],
|
|
1814
|
+
properties: {
|
|
1815
|
+
roleDefinitionId: `[resourceId('Microsoft.Authorization/roleDefinitions', guid(resourceGroup().id, '${construct.id}'))]`,
|
|
1816
|
+
principalType,
|
|
1817
|
+
description: `Role assignment for ${attachTo} (${principalType})`
|
|
1818
|
+
}
|
|
1819
|
+
}
|
|
1820
|
+
];
|
|
1821
|
+
}
|
|
1822
|
+
// ── Events ────────────────────────────────────────────────────────────
|
|
1823
|
+
case "Events.EventBridge": {
|
|
1824
|
+
const rules = props.rules ?? [];
|
|
1825
|
+
return [{
|
|
1826
|
+
type: "Microsoft.EventGrid/namespaces",
|
|
1827
|
+
apiVersion: "2023-06-01-preview",
|
|
1828
|
+
name: props.busName ?? construct.id,
|
|
1829
|
+
location,
|
|
1830
|
+
tags: tag(construct.id),
|
|
1831
|
+
sku: { name: "Standard", capacity: 1 },
|
|
1832
|
+
properties: {
|
|
1833
|
+
topicsConfiguration: {},
|
|
1834
|
+
topicSpacesConfiguration: {
|
|
1835
|
+
state: "Enabled"
|
|
1836
|
+
}
|
|
1837
|
+
}
|
|
1838
|
+
}, ...rules.map((r) => ({
|
|
1839
|
+
type: "Microsoft.EventGrid/eventSubscriptions",
|
|
1840
|
+
apiVersion: "2022-06-15",
|
|
1841
|
+
name: r.name ?? construct.id,
|
|
1842
|
+
location,
|
|
1843
|
+
properties: {
|
|
1844
|
+
destination: {
|
|
1845
|
+
endpointType: "WebHook",
|
|
1846
|
+
properties: { endpointUrl: r.targetArn ?? "" }
|
|
1847
|
+
},
|
|
1848
|
+
filter: {
|
|
1849
|
+
includedEventTypes: r.detailTypes ?? ["*"]
|
|
1850
|
+
}
|
|
1851
|
+
}
|
|
1852
|
+
}))];
|
|
1853
|
+
}
|
|
1854
|
+
// ── Workflow ──────────────────────────────────────────────────────────
|
|
1855
|
+
case "Workflow.StepFunctions": {
|
|
1856
|
+
const steps = props.steps ?? [];
|
|
1857
|
+
const definition = {
|
|
1858
|
+
definition: {
|
|
1859
|
+
"$schema": "https://schema.management.azure.com/providers/Microsoft.Logic/schemas/2016-06-01/workflowdefinition.json#",
|
|
1860
|
+
contentVersion: "1.0.0.0",
|
|
1861
|
+
triggers: {},
|
|
1862
|
+
actions: Object.fromEntries(steps.map((s, i) => [s.name, {
|
|
1863
|
+
type: "Http",
|
|
1864
|
+
inputs: { method: "POST", uri: s.resource ?? "" },
|
|
1865
|
+
runAfter: i > 0 ? { [steps[i - 1].name]: ["Succeeded"] } : {}
|
|
1866
|
+
}]))
|
|
1867
|
+
}
|
|
1868
|
+
};
|
|
1869
|
+
return [{
|
|
1870
|
+
type: "Microsoft.Logic/workflows",
|
|
1871
|
+
apiVersion: "2019-05-01",
|
|
1872
|
+
name: construct.id,
|
|
1873
|
+
location,
|
|
1874
|
+
tags: tag(construct.id),
|
|
1875
|
+
properties: definition
|
|
1876
|
+
}];
|
|
1877
|
+
}
|
|
1878
|
+
// ── Messaging ─────────────────────────────────────────────────────────
|
|
1879
|
+
case "Messaging.Queue": {
|
|
1880
|
+
const nsName = `${construct.id}-ns`;
|
|
1881
|
+
return [
|
|
1882
|
+
{
|
|
1883
|
+
type: "Microsoft.ServiceBus/namespaces",
|
|
1884
|
+
apiVersion: "2022-10-01-preview",
|
|
1885
|
+
name: nsName,
|
|
1886
|
+
location,
|
|
1887
|
+
tags: tag(construct.id),
|
|
1888
|
+
sku: { name: "Standard", tier: "Standard" },
|
|
1889
|
+
properties: {}
|
|
1890
|
+
},
|
|
1891
|
+
{
|
|
1892
|
+
type: "Microsoft.ServiceBus/namespaces/queues",
|
|
1893
|
+
apiVersion: "2022-10-01-preview",
|
|
1894
|
+
name: `${nsName}/${construct.id}`,
|
|
1895
|
+
location,
|
|
1896
|
+
dependsOn: [`[resourceId('Microsoft.ServiceBus/namespaces', '${nsName}')]`],
|
|
1897
|
+
properties: {
|
|
1898
|
+
lockDuration: `PT${props.visibilityTimeoutSeconds ?? 30}S`,
|
|
1899
|
+
maxSizeInMegabytes: 1024,
|
|
1900
|
+
requiresDuplicateDetection: false,
|
|
1901
|
+
requiresSession: false,
|
|
1902
|
+
defaultMessageTimeToLive: `P${Math.floor((props.messageRetentionSeconds ?? 345600) / 86400)}D`,
|
|
1903
|
+
deadLetteringOnMessageExpiration: false
|
|
1904
|
+
}
|
|
1905
|
+
}
|
|
1906
|
+
];
|
|
1907
|
+
}
|
|
1908
|
+
case "Messaging.Topic": {
|
|
1909
|
+
const nsName = `${construct.id}-ns`;
|
|
1910
|
+
const subscriptions = props.subscriptions ?? [];
|
|
1911
|
+
const resources = [
|
|
1912
|
+
{
|
|
1913
|
+
type: "Microsoft.ServiceBus/namespaces",
|
|
1914
|
+
apiVersion: "2022-10-01-preview",
|
|
1915
|
+
name: nsName,
|
|
1916
|
+
location,
|
|
1917
|
+
tags: tag(construct.id),
|
|
1918
|
+
sku: { name: "Standard", tier: "Standard" },
|
|
1919
|
+
properties: {}
|
|
1920
|
+
},
|
|
1921
|
+
{
|
|
1922
|
+
type: "Microsoft.ServiceBus/namespaces/topics",
|
|
1923
|
+
apiVersion: "2022-10-01-preview",
|
|
1924
|
+
name: `${nsName}/${construct.id}`,
|
|
1925
|
+
location,
|
|
1926
|
+
dependsOn: [`[resourceId('Microsoft.ServiceBus/namespaces', '${nsName}')]`],
|
|
1927
|
+
properties: {
|
|
1928
|
+
defaultMessageTimeToLive: "P14D",
|
|
1929
|
+
requiresDuplicateDetection: false
|
|
1930
|
+
}
|
|
1931
|
+
}
|
|
1932
|
+
];
|
|
1933
|
+
subscriptions.forEach((s, i) => {
|
|
1934
|
+
resources.push({
|
|
1935
|
+
type: "Microsoft.ServiceBus/namespaces/topics/subscriptions",
|
|
1936
|
+
apiVersion: "2022-10-01-preview",
|
|
1937
|
+
name: `${nsName}/${construct.id}/sub-${i}`,
|
|
1938
|
+
location,
|
|
1939
|
+
dependsOn: [`[resourceId('Microsoft.ServiceBus/namespaces/topics', '${nsName}', '${construct.id}')]`],
|
|
1940
|
+
properties: {
|
|
1941
|
+
lockDuration: "PT30S",
|
|
1942
|
+
deadLetteringOnMessageExpiration: false,
|
|
1943
|
+
forwardTo: s.endpoint
|
|
1944
|
+
}
|
|
1945
|
+
});
|
|
1946
|
+
});
|
|
1947
|
+
return resources;
|
|
1948
|
+
}
|
|
1949
|
+
// ── Secret / Certificate ──────────────────────────────────────────────
|
|
1950
|
+
case "Secret.Vault": {
|
|
1951
|
+
const kvName = `${construct.id.toLowerCase().replace(/[^a-z0-9-]/g, "-").slice(0, 24)}-kv`;
|
|
1952
|
+
return [{
|
|
1953
|
+
type: "Microsoft.KeyVault/vaults",
|
|
1954
|
+
apiVersion: "2023-02-01",
|
|
1955
|
+
name: kvName,
|
|
1956
|
+
location,
|
|
1957
|
+
tags: tag(construct.id),
|
|
1958
|
+
properties: {
|
|
1959
|
+
sku: { family: "A", name: "standard" },
|
|
1960
|
+
tenantId: "[subscription().tenantId]",
|
|
1961
|
+
enableSoftDelete: true,
|
|
1962
|
+
softDeleteRetentionInDays: 90,
|
|
1963
|
+
enablePurgeProtection: true,
|
|
1964
|
+
enabledForDeployment: false,
|
|
1965
|
+
accessPolicies: []
|
|
1966
|
+
}
|
|
1967
|
+
}];
|
|
1968
|
+
}
|
|
1969
|
+
case "Certificate.TLS": {
|
|
1970
|
+
const kvName = `${construct.id.toLowerCase().replace(/[^a-z0-9-]/g, "-").slice(0, 20)}-kv`;
|
|
1971
|
+
return [{
|
|
1972
|
+
type: "Microsoft.KeyVault/vaults/certificates",
|
|
1973
|
+
apiVersion: "2023-02-01",
|
|
1974
|
+
name: `${kvName}/${construct.id.replace(/[^a-zA-Z0-9-]/g, "-")}`,
|
|
1975
|
+
location,
|
|
1976
|
+
properties: {
|
|
1977
|
+
properties: {
|
|
1978
|
+
x509CertificateProperties: {
|
|
1979
|
+
subject: `CN=${props.domainName}`,
|
|
1980
|
+
subjectAlternativeNames: {
|
|
1981
|
+
dnsNames: [props.domainName, ...props.subjectAlternativeNames ?? []]
|
|
1982
|
+
},
|
|
1983
|
+
validityInMonths: 12
|
|
1984
|
+
},
|
|
1985
|
+
issuerParameters: { name: "Self", issuerName: "Self" },
|
|
1986
|
+
keyProperties: { keyType: "RSA", keySize: 2048, exportable: true }
|
|
1987
|
+
}
|
|
1988
|
+
}
|
|
1989
|
+
}];
|
|
1990
|
+
}
|
|
1991
|
+
// ── Monitoring ────────────────────────────────────────────────────────
|
|
1992
|
+
case "Monitoring.Alarm": {
|
|
1993
|
+
const dimensions = props.dimensions;
|
|
1994
|
+
const operatorMap = {
|
|
1995
|
+
GreaterThanThreshold: "GreaterThan",
|
|
1996
|
+
LessThanThreshold: "LessThan",
|
|
1997
|
+
GreaterThanOrEqualToThreshold: "GreaterThanOrEqual",
|
|
1998
|
+
LessThanOrEqualToThreshold: "LessThanOrEqual"
|
|
1999
|
+
};
|
|
2000
|
+
return [{
|
|
2001
|
+
type: "Microsoft.Insights/metricAlerts",
|
|
2002
|
+
apiVersion: "2018-03-01",
|
|
2003
|
+
name: construct.id,
|
|
2004
|
+
location: "global",
|
|
2005
|
+
tags: tag(construct.id),
|
|
2006
|
+
properties: {
|
|
2007
|
+
description: `Alarm for ${props.metricName}`,
|
|
2008
|
+
severity: 2,
|
|
2009
|
+
enabled: true,
|
|
2010
|
+
evaluationFrequency: `PT${props.periodSeconds ?? 60}S`,
|
|
2011
|
+
windowSize: `PT${(props.periodSeconds ?? 60) * (props.evaluationPeriods ?? 2)}S`,
|
|
2012
|
+
criteria: {
|
|
2013
|
+
"odata.type": "Microsoft.Azure.Monitor.SingleResourceMultipleMetricCriteria",
|
|
2014
|
+
allOf: [{
|
|
2015
|
+
name: "criterion1",
|
|
2016
|
+
metricName: props.metricName,
|
|
2017
|
+
metricNamespace: props.namespace ?? "Microsoft.Web/sites",
|
|
2018
|
+
operator: operatorMap[props.comparisonOperator ?? "GreaterThanThreshold"] ?? "GreaterThan",
|
|
2019
|
+
threshold: props.threshold,
|
|
2020
|
+
timeAggregation: props.statistic ?? "Average",
|
|
2021
|
+
dimensions: dimensions ? Object.entries(dimensions).map(([k, v]) => ({ name: k, operator: "Include", values: [v] })) : []
|
|
2022
|
+
}]
|
|
2023
|
+
},
|
|
2024
|
+
actions: (props.alarmActions ?? []).map((a) => ({ actionGroupId: a }))
|
|
2025
|
+
}
|
|
2026
|
+
}];
|
|
2027
|
+
}
|
|
2028
|
+
case "Monitoring.Dashboard": {
|
|
2029
|
+
const widgets = props.widgets ?? [];
|
|
2030
|
+
return [{
|
|
2031
|
+
type: "Microsoft.Portal/dashboards",
|
|
2032
|
+
apiVersion: "2020-09-01-preview",
|
|
2033
|
+
name: construct.id,
|
|
2034
|
+
location,
|
|
2035
|
+
tags: {
|
|
2036
|
+
"hidden-title": construct.id
|
|
2037
|
+
},
|
|
2038
|
+
properties: {
|
|
2039
|
+
lenses: [{
|
|
2040
|
+
order: 0,
|
|
2041
|
+
parts: widgets.map((w, i) => ({
|
|
2042
|
+
position: { x: i % 3 * 4, y: Math.floor(i / 3) * 4, colSpan: 4, rowSpan: 4 },
|
|
2043
|
+
metadata: {
|
|
2044
|
+
type: "Extension/Microsoft_Azure_Monitoring/PartType/MetricsChartPart",
|
|
2045
|
+
settings: {
|
|
2046
|
+
content: {
|
|
2047
|
+
options: { chart: { metrics: [{ name: w.metricName, resourceMetadata: {} }] } },
|
|
2048
|
+
title: w.title
|
|
2049
|
+
}
|
|
2050
|
+
}
|
|
2051
|
+
}
|
|
2052
|
+
}))
|
|
2053
|
+
}]
|
|
2054
|
+
}
|
|
2055
|
+
}];
|
|
2056
|
+
}
|
|
2057
|
+
case "Logging.Stream": {
|
|
2058
|
+
const wsName = `${construct.id}-law`;
|
|
2059
|
+
return [{
|
|
2060
|
+
type: "Microsoft.OperationalInsights/workspaces",
|
|
2061
|
+
apiVersion: "2022-10-01",
|
|
2062
|
+
name: wsName,
|
|
2063
|
+
location,
|
|
2064
|
+
tags: tag(construct.id),
|
|
2065
|
+
properties: {
|
|
2066
|
+
sku: { name: "PerGB2018" },
|
|
2067
|
+
retentionInDays: props.retentionDays ?? 30,
|
|
2068
|
+
features: { enableLogAccessUsingOnlyResourcePermissions: true }
|
|
2069
|
+
}
|
|
2070
|
+
}];
|
|
2071
|
+
}
|
|
2072
|
+
case "Custom.Resource": {
|
|
2073
|
+
const arm = props.arm;
|
|
2074
|
+
if (!arm)
|
|
2075
|
+
return [];
|
|
2076
|
+
return [{
|
|
2077
|
+
type: arm.type,
|
|
2078
|
+
apiVersion: arm.apiVersion,
|
|
2079
|
+
name: props.name ?? construct.id,
|
|
2080
|
+
location,
|
|
2081
|
+
tags: tag(construct.id),
|
|
2082
|
+
properties: arm.properties,
|
|
2083
|
+
...arm.sku ? { sku: arm.sku } : {},
|
|
2084
|
+
...arm.kind ? { kind: arm.kind } : {}
|
|
2085
|
+
}];
|
|
2086
|
+
}
|
|
2087
|
+
default:
|
|
2088
|
+
console.warn(`[azure] Construct type '${construct.type}' nao suportado \u2014 descartado.`);
|
|
2089
|
+
return [];
|
|
2090
|
+
}
|
|
2091
|
+
}
|
|
2092
|
+
function synthesize(stack) {
|
|
2093
|
+
const resources = [];
|
|
2094
|
+
for (const construct of stack.constructs) {
|
|
2095
|
+
const result = synthesizeConstruct(construct);
|
|
2096
|
+
resources.push(...result);
|
|
2097
|
+
}
|
|
2098
|
+
return {
|
|
2099
|
+
$schema: "https://schema.management.azure.com/schemas/2019-04-01/deploymentTemplate.json#",
|
|
2100
|
+
contentVersion: "1.0.0.0",
|
|
2101
|
+
resources
|
|
2102
|
+
};
|
|
2103
|
+
}
|
|
2104
|
+
}
|
|
2105
|
+
});
|
|
2106
|
+
|
|
2107
|
+
// ../providers/azure/dist/provider.js
|
|
2108
|
+
var require_provider2 = __commonJS({
|
|
2109
|
+
"../providers/azure/dist/provider.js"(exports2) {
|
|
2110
|
+
"use strict";
|
|
2111
|
+
Object.defineProperty(exports2, "__esModule", { value: true });
|
|
2112
|
+
exports2.AzureProvider = void 0;
|
|
2113
|
+
var arm_1 = require_arm();
|
|
2114
|
+
var AzureProvider2 = class {
|
|
2115
|
+
name = "azure";
|
|
2116
|
+
synthesize(stack) {
|
|
2117
|
+
return (0, arm_1.synthesize)(stack);
|
|
2118
|
+
}
|
|
2119
|
+
};
|
|
2120
|
+
exports2.AzureProvider = AzureProvider2;
|
|
2121
|
+
}
|
|
2122
|
+
});
|
|
2123
|
+
|
|
2124
|
+
// ../providers/azure/dist/index.js
|
|
2125
|
+
var require_dist2 = __commonJS({
|
|
2126
|
+
"../providers/azure/dist/index.js"(exports2) {
|
|
2127
|
+
"use strict";
|
|
2128
|
+
Object.defineProperty(exports2, "__esModule", { value: true });
|
|
2129
|
+
exports2.synthesize = exports2.AzureProvider = void 0;
|
|
2130
|
+
var provider_1 = require_provider2();
|
|
2131
|
+
Object.defineProperty(exports2, "AzureProvider", { enumerable: true, get: function() {
|
|
2132
|
+
return provider_1.AzureProvider;
|
|
2133
|
+
} });
|
|
2134
|
+
var arm_1 = require_arm();
|
|
2135
|
+
Object.defineProperty(exports2, "synthesize", { enumerable: true, get: function() {
|
|
2136
|
+
return arm_1.synthesize;
|
|
2137
|
+
} });
|
|
2138
|
+
}
|
|
2139
|
+
});
|
|
2140
|
+
|
|
2141
|
+
// ../providers/gcp/dist/synth/deployment-manager.js
|
|
2142
|
+
var require_deployment_manager = __commonJS({
|
|
2143
|
+
"../providers/gcp/dist/synth/deployment-manager.js"(exports2) {
|
|
2144
|
+
"use strict";
|
|
2145
|
+
Object.defineProperty(exports2, "__esModule", { value: true });
|
|
2146
|
+
exports2.synthesize = synthesize;
|
|
2147
|
+
var INSTANCE_TYPE_MAP = {
|
|
2148
|
+
small: "e2-small",
|
|
2149
|
+
medium: "e2-medium",
|
|
2150
|
+
large: "e2-standard-4"
|
|
2151
|
+
};
|
|
2152
|
+
var GCP_IMAGE_MAP = {
|
|
2153
|
+
"ubuntu": "projects/ubuntu-os-cloud/global/images/family/ubuntu-2204-lts",
|
|
2154
|
+
"ubuntu-22.04": "projects/ubuntu-os-cloud/global/images/family/ubuntu-2204-lts",
|
|
2155
|
+
"ubuntu-20.04": "projects/ubuntu-os-cloud/global/images/family/ubuntu-2004-lts",
|
|
2156
|
+
"windows-2022": "projects/windows-cloud/global/images/family/windows-2022",
|
|
2157
|
+
"windows-2019": "projects/windows-cloud/global/images/family/windows-2019",
|
|
2158
|
+
"windows-2016": "projects/windows-cloud/global/images/family/windows-2016"
|
|
2159
|
+
};
|
|
2160
|
+
function resolveGcpImage(image) {
|
|
2161
|
+
if (!image)
|
|
2162
|
+
return "projects/ubuntu-os-cloud/global/images/family/ubuntu-2204-lts";
|
|
2163
|
+
return GCP_IMAGE_MAP[image] ?? `global/images/${image}`;
|
|
2164
|
+
}
|
|
2165
|
+
var CACHE_TIER_MAP = {
|
|
2166
|
+
small: "BASIC",
|
|
2167
|
+
medium: "STANDARD_HA",
|
|
2168
|
+
large: "STANDARD_HA"
|
|
2169
|
+
};
|
|
2170
|
+
var CACHE_CAPACITY_MAP = {
|
|
2171
|
+
small: 1,
|
|
2172
|
+
medium: 5,
|
|
2173
|
+
large: 16
|
|
2174
|
+
};
|
|
2175
|
+
var K8S_MACHINE_MAP = {
|
|
2176
|
+
small: "e2-medium",
|
|
2177
|
+
medium: "e2-standard-4",
|
|
2178
|
+
large: "n2-standard-8"
|
|
2179
|
+
};
|
|
2180
|
+
function gcpRegion(regionOrZone) {
|
|
2181
|
+
if (!regionOrZone)
|
|
2182
|
+
return "us-central1";
|
|
2183
|
+
const parts = regionOrZone.split("-");
|
|
2184
|
+
if (parts.length >= 3 && parts[parts.length - 1].match(/^[a-z]$/)) {
|
|
2185
|
+
return parts.slice(0, -1).join("-");
|
|
2186
|
+
}
|
|
2187
|
+
return regionOrZone;
|
|
2188
|
+
}
|
|
2189
|
+
function synthesizeConstruct(construct) {
|
|
2190
|
+
const props = construct.props;
|
|
2191
|
+
const zone = props.region ?? "us-central1-a";
|
|
2192
|
+
const region = gcpRegion(props.region);
|
|
2193
|
+
switch (construct.type) {
|
|
2194
|
+
// ── Compute ──────────────────────────────────────────────────────────
|
|
2195
|
+
case "Compute.Instance":
|
|
2196
|
+
return [{
|
|
2197
|
+
name: construct.id,
|
|
2198
|
+
type: "compute.v1.instance",
|
|
2199
|
+
properties: {
|
|
2200
|
+
zone,
|
|
2201
|
+
machineType: `zones/${zone}/machineTypes/${INSTANCE_TYPE_MAP[props.instanceType] ?? "e2-small"}`,
|
|
2202
|
+
disks: [{
|
|
2203
|
+
boot: true,
|
|
2204
|
+
autoDelete: true,
|
|
2205
|
+
initializeParams: {
|
|
2206
|
+
sourceImage: resolveGcpImage(props.image)
|
|
2207
|
+
}
|
|
2208
|
+
}],
|
|
2209
|
+
networkInterfaces: [{ network: "global/networks/default" }]
|
|
2210
|
+
}
|
|
2211
|
+
}];
|
|
2212
|
+
case "Compute.AutoScaling": {
|
|
2213
|
+
const machineType = INSTANCE_TYPE_MAP[props.instanceType] ?? "e2-small";
|
|
2214
|
+
const templateName = `${construct.id}-template`;
|
|
2215
|
+
const resources = [
|
|
2216
|
+
{
|
|
2217
|
+
name: templateName,
|
|
2218
|
+
type: "compute.v1.instanceTemplate",
|
|
2219
|
+
properties: {
|
|
2220
|
+
properties: {
|
|
2221
|
+
machineType,
|
|
2222
|
+
disks: [{
|
|
2223
|
+
boot: true,
|
|
2224
|
+
autoDelete: true,
|
|
2225
|
+
initializeParams: {
|
|
2226
|
+
sourceImage: resolveGcpImage(props.image)
|
|
2227
|
+
}
|
|
2228
|
+
}],
|
|
2229
|
+
networkInterfaces: [{ network: "global/networks/default" }]
|
|
2230
|
+
}
|
|
2231
|
+
}
|
|
2232
|
+
},
|
|
2233
|
+
{
|
|
2234
|
+
name: construct.id,
|
|
2235
|
+
type: "compute.v1.regionInstanceGroupManager",
|
|
2236
|
+
properties: {
|
|
2237
|
+
region,
|
|
2238
|
+
baseInstanceName: construct.id,
|
|
2239
|
+
instanceTemplate: `global/instanceTemplates/${templateName}`,
|
|
2240
|
+
targetSize: props.desiredCapacity ?? props.minCapacity ?? 1
|
|
2241
|
+
}
|
|
2242
|
+
}
|
|
2243
|
+
];
|
|
2244
|
+
if (props.minCapacity !== void 0 || props.targetCpuUtilization) {
|
|
2245
|
+
resources.push({
|
|
2246
|
+
name: `${construct.id}-autoscaler`,
|
|
2247
|
+
type: "compute.v1.regionAutoscaler",
|
|
2248
|
+
properties: {
|
|
2249
|
+
region,
|
|
2250
|
+
target: `regions/${region}/instanceGroupManagers/${construct.id}`,
|
|
2251
|
+
autoscalingPolicy: {
|
|
2252
|
+
minNumReplicas: props.minCapacity ?? 1,
|
|
2253
|
+
maxNumReplicas: props.maxCapacity ?? 10,
|
|
2254
|
+
coolDownPeriodSec: 60,
|
|
2255
|
+
...props.targetCpuUtilization ? {
|
|
2256
|
+
cpuUtilization: { utilizationTarget: props.targetCpuUtilization / 100 }
|
|
2257
|
+
} : {}
|
|
2258
|
+
}
|
|
2259
|
+
}
|
|
2260
|
+
});
|
|
2261
|
+
}
|
|
2262
|
+
return resources;
|
|
2263
|
+
}
|
|
2264
|
+
case "Compute.Container": {
|
|
2265
|
+
const environment = props.environment ?? {};
|
|
2266
|
+
const envVars = Object.entries(environment).map(([k, v]) => ({ name: k, value: v }));
|
|
2267
|
+
return [{
|
|
2268
|
+
name: construct.id,
|
|
2269
|
+
type: "run.v2.service",
|
|
2270
|
+
properties: {
|
|
2271
|
+
location: region,
|
|
2272
|
+
template: {
|
|
2273
|
+
containers: [{
|
|
2274
|
+
image: props.image,
|
|
2275
|
+
resources: {
|
|
2276
|
+
limits: {
|
|
2277
|
+
cpu: String(Math.round((props.cpu ?? 256) / 1e3)),
|
|
2278
|
+
memory: `${props.memory ?? 512}Mi`
|
|
2279
|
+
}
|
|
2280
|
+
},
|
|
2281
|
+
ports: props.port ? [{ containerPort: props.port }] : [],
|
|
2282
|
+
env: envVars
|
|
2283
|
+
}],
|
|
2284
|
+
scaling: {
|
|
2285
|
+
minInstanceCount: props.minInstances ?? 0,
|
|
2286
|
+
maxInstanceCount: props.desiredCount ?? 10
|
|
2287
|
+
}
|
|
2288
|
+
},
|
|
2289
|
+
ingress: props.publicIp ? "INGRESS_TRAFFIC_ALL" : "INGRESS_TRAFFIC_INTERNAL_ONLY"
|
|
2290
|
+
}
|
|
2291
|
+
}];
|
|
2292
|
+
}
|
|
2293
|
+
case "Compute.Kubernetes": {
|
|
2294
|
+
const machineType = K8S_MACHINE_MAP[props.nodeInstanceType ?? "medium"] ?? "e2-standard-4";
|
|
2295
|
+
return [{
|
|
2296
|
+
name: construct.id,
|
|
2297
|
+
type: "container.v1.cluster",
|
|
2298
|
+
properties: {
|
|
2299
|
+
location: region,
|
|
2300
|
+
cluster: {
|
|
2301
|
+
name: construct.id,
|
|
2302
|
+
currentMasterVersion: props.version ?? "1.29",
|
|
2303
|
+
initialNodeCount: props.desiredNodes ?? 2,
|
|
2304
|
+
nodePools: [{
|
|
2305
|
+
name: "default-pool",
|
|
2306
|
+
config: {
|
|
2307
|
+
machineType,
|
|
2308
|
+
diskSizeGb: 100,
|
|
2309
|
+
oauthScopes: ["https://www.googleapis.com/auth/cloud-platform"]
|
|
2310
|
+
},
|
|
2311
|
+
autoscaling: {
|
|
2312
|
+
enabled: true,
|
|
2313
|
+
minNodeCount: props.minNodes ?? 1,
|
|
2314
|
+
maxNodeCount: props.maxNodes ?? 3
|
|
2315
|
+
},
|
|
2316
|
+
initialNodeCount: props.desiredNodes ?? 2
|
|
2317
|
+
}],
|
|
2318
|
+
masterAuth: { clientCertificateConfig: { issueClientCertificate: false } },
|
|
2319
|
+
networkConfig: {
|
|
2320
|
+
enableIntraNodeVisibility: true,
|
|
2321
|
+
datapathProvider: "ADVANCED_DATAPATH"
|
|
2322
|
+
},
|
|
2323
|
+
privateClusterConfig: props.privateCluster ? {
|
|
2324
|
+
enablePrivateNodes: true,
|
|
2325
|
+
enablePrivateEndpoint: true,
|
|
2326
|
+
masterIpv4CidrBlock: "172.16.0.32/28"
|
|
2327
|
+
} : {}
|
|
2328
|
+
}
|
|
2329
|
+
}
|
|
2330
|
+
}];
|
|
2331
|
+
}
|
|
2332
|
+
// ── Storage ───────────────────────────────────────────────────────────
|
|
2333
|
+
case "Storage.Bucket": {
|
|
2334
|
+
const lifecycleRules = props.lifecycleRules ?? [];
|
|
2335
|
+
const gcsRules = [];
|
|
2336
|
+
for (const r of lifecycleRules) {
|
|
2337
|
+
const prefixCond = r.prefix ? { matchesPrefix: [r.prefix] } : {};
|
|
2338
|
+
if (r.transitionToGlacierDays) {
|
|
2339
|
+
gcsRules.push({
|
|
2340
|
+
action: { type: "SetStorageClass", storageClass: "ARCHIVE" },
|
|
2341
|
+
condition: { age: r.transitionToGlacierDays, ...prefixCond }
|
|
2342
|
+
});
|
|
2343
|
+
}
|
|
2344
|
+
if (r.expireAfterDays) {
|
|
2345
|
+
gcsRules.push({
|
|
2346
|
+
action: { type: "Delete" },
|
|
2347
|
+
condition: { age: r.expireAfterDays, ...prefixCond }
|
|
2348
|
+
});
|
|
2349
|
+
}
|
|
2350
|
+
}
|
|
2351
|
+
return [{
|
|
2352
|
+
name: construct.id.toLowerCase().replace(/[^a-z0-9-]/g, "-"),
|
|
2353
|
+
type: "storage.v1.bucket",
|
|
2354
|
+
properties: {
|
|
2355
|
+
location: props.location ?? "US",
|
|
2356
|
+
versioning: { enabled: props.versioning ?? false },
|
|
2357
|
+
iamConfiguration: {
|
|
2358
|
+
uniformBucketLevelAccess: { enabled: !props.publicAccess }
|
|
2359
|
+
},
|
|
2360
|
+
...gcsRules.length > 0 ? { lifecycle: { rule: gcsRules } } : {}
|
|
2361
|
+
}
|
|
2362
|
+
}];
|
|
2363
|
+
}
|
|
2364
|
+
case "Storage.FileSystem":
|
|
2365
|
+
return [{
|
|
2366
|
+
name: construct.id.toLowerCase(),
|
|
2367
|
+
type: "file.v1.instance",
|
|
2368
|
+
properties: {
|
|
2369
|
+
location: `${region}-a`,
|
|
2370
|
+
tier: props.performanceMode === "maxIO" ? "HIGH_SCALE_SSD" : "STANDARD",
|
|
2371
|
+
networks: [{ network: "default", modes: ["MODE_IPV4"] }],
|
|
2372
|
+
fileShares: [{
|
|
2373
|
+
name: construct.id,
|
|
2374
|
+
capacityGb: 1024
|
|
2375
|
+
}]
|
|
2376
|
+
}
|
|
2377
|
+
}];
|
|
2378
|
+
case "Storage.Archive":
|
|
2379
|
+
return [{
|
|
2380
|
+
name: `${construct.id.toLowerCase().replace(/[^a-z0-9-]/g, "-")}-archive`,
|
|
2381
|
+
type: "storage.v1.bucket",
|
|
2382
|
+
properties: {
|
|
2383
|
+
location: props.location ?? "US",
|
|
2384
|
+
storageClass: "ARCHIVE",
|
|
2385
|
+
iamConfiguration: { uniformBucketLevelAccess: { enabled: true } },
|
|
2386
|
+
lifecycle: {
|
|
2387
|
+
rule: props.retentionDays ? [{
|
|
2388
|
+
action: { type: "Delete" },
|
|
2389
|
+
condition: { age: props.retentionDays }
|
|
2390
|
+
}] : []
|
|
2391
|
+
}
|
|
2392
|
+
}
|
|
2393
|
+
}];
|
|
2394
|
+
// ── Network ───────────────────────────────────────────────────────────
|
|
2395
|
+
case "Network.VPC":
|
|
2396
|
+
return [{
|
|
2397
|
+
name: construct.id,
|
|
2398
|
+
type: "compute.v1.network",
|
|
2399
|
+
properties: {
|
|
2400
|
+
autoCreateSubnetworks: false,
|
|
2401
|
+
routingConfig: { routingMode: "REGIONAL" },
|
|
2402
|
+
description: `VPC ${construct.id}`
|
|
2403
|
+
}
|
|
2404
|
+
}];
|
|
2405
|
+
case "Network.Subnet":
|
|
2406
|
+
return [{
|
|
2407
|
+
name: construct.id,
|
|
2408
|
+
type: "compute.v1.subnetwork",
|
|
2409
|
+
properties: {
|
|
2410
|
+
network: `global/networks/${props.vpcId}`,
|
|
2411
|
+
ipCidrRange: props.cidr,
|
|
2412
|
+
region,
|
|
2413
|
+
privateIpGoogleAccess: !props.public,
|
|
2414
|
+
...props.availabilityZone ? { description: `AZ: ${props.availabilityZone}` } : {}
|
|
2415
|
+
}
|
|
2416
|
+
}];
|
|
2417
|
+
case "Network.SecurityGroup": {
|
|
2418
|
+
const ingress = props.ingressRules ?? [];
|
|
2419
|
+
const egress = props.egressRules ?? [];
|
|
2420
|
+
const ingressResources = ingress.map((r, i) => {
|
|
2421
|
+
if (r.cidr === void 0) {
|
|
2422
|
+
console.warn(`[gcp] Security group rule sem CIDR; usando 0.0.0.0/0 \u2014 defina props.cidr explicitamente (${construct.id} ingress[${i}])`);
|
|
2423
|
+
}
|
|
2424
|
+
return {
|
|
2425
|
+
name: `${construct.id}-ingress-${i}`,
|
|
2426
|
+
type: "compute.v1.firewall",
|
|
2427
|
+
properties: {
|
|
2428
|
+
network: `global/networks/${props.vpcId}`,
|
|
2429
|
+
direction: "INGRESS",
|
|
2430
|
+
priority: 1e3 + i,
|
|
2431
|
+
allowed: [{
|
|
2432
|
+
IPProtocol: r.protocol === "-1" ? "all" : r.protocol,
|
|
2433
|
+
...r.protocol !== "-1" ? {
|
|
2434
|
+
ports: r.fromPort === r.toPort ? [`${r.fromPort}`] : [`${r.fromPort}-${r.toPort}`]
|
|
2435
|
+
} : {}
|
|
2436
|
+
}],
|
|
2437
|
+
sourceRanges: [r.cidr ?? "0.0.0.0/0"],
|
|
2438
|
+
description: r.description ?? ""
|
|
2439
|
+
}
|
|
2440
|
+
};
|
|
2441
|
+
});
|
|
2442
|
+
const egressList = egress.length > 0 ? egress : [{
|
|
2443
|
+
protocol: "-1",
|
|
2444
|
+
fromPort: 0,
|
|
2445
|
+
toPort: 0,
|
|
2446
|
+
cidr: "0.0.0.0/0",
|
|
2447
|
+
description: "Allow all egress"
|
|
2448
|
+
}];
|
|
2449
|
+
const egressResources = egressList.map((r, i) => ({
|
|
2450
|
+
name: `${construct.id}-egress-${i}`,
|
|
2451
|
+
type: "compute.v1.firewall",
|
|
2452
|
+
properties: {
|
|
2453
|
+
network: `global/networks/${props.vpcId}`,
|
|
2454
|
+
direction: "EGRESS",
|
|
2455
|
+
priority: 1e3 + i,
|
|
2456
|
+
allowed: [{
|
|
2457
|
+
IPProtocol: r.protocol === "-1" ? "all" : r.protocol,
|
|
2458
|
+
...r.protocol !== "-1" && r.fromPort !== 0 ? {
|
|
2459
|
+
ports: r.fromPort === r.toPort ? [`${r.fromPort}`] : [`${r.fromPort}-${r.toPort}`]
|
|
2460
|
+
} : {}
|
|
2461
|
+
}],
|
|
2462
|
+
destinationRanges: [r.cidr ?? "0.0.0.0/0"],
|
|
2463
|
+
description: r.description ?? ""
|
|
2464
|
+
}
|
|
2465
|
+
}));
|
|
2466
|
+
return [...ingressResources, ...egressResources];
|
|
2467
|
+
}
|
|
2468
|
+
case "Network.WAF": {
|
|
2469
|
+
const rules = props.rules ?? [];
|
|
2470
|
+
const securityRules = rules.map((r, i) => ({
|
|
2471
|
+
priority: r.priority ?? i + 1,
|
|
2472
|
+
action: r.action ?? "allow",
|
|
2473
|
+
match: r.managedGroup ? { expr: { expression: 'evaluatePreconfiguredExpr("sqli-stable")' } } : {
|
|
2474
|
+
versionedExpr: "SRC_IPS_V1",
|
|
2475
|
+
config: { srcIpRanges: r.sourceIps ?? ["*"] }
|
|
2476
|
+
},
|
|
2477
|
+
description: r.description ?? ""
|
|
2478
|
+
}));
|
|
2479
|
+
return [{
|
|
2480
|
+
name: construct.id,
|
|
2481
|
+
type: "compute.v1.securityPolicy",
|
|
2482
|
+
properties: {
|
|
2483
|
+
description: props.description ?? `WAF ${construct.id}`,
|
|
2484
|
+
rules: [
|
|
2485
|
+
...securityRules,
|
|
2486
|
+
{
|
|
2487
|
+
priority: 2147483647,
|
|
2488
|
+
action: props.defaultAction ?? "allow",
|
|
2489
|
+
match: { versionedExpr: "SRC_IPS_V1", config: { srcIpRanges: ["*"] } },
|
|
2490
|
+
description: "Default rule"
|
|
2491
|
+
}
|
|
2492
|
+
]
|
|
2493
|
+
}
|
|
2494
|
+
}];
|
|
2495
|
+
}
|
|
2496
|
+
case "Network.LoadBalancer": {
|
|
2497
|
+
const targetGroups = props.targetGroups ?? [];
|
|
2498
|
+
const lbType = props.type ?? "application";
|
|
2499
|
+
const resources = [{
|
|
2500
|
+
name: `${construct.id}-backend`,
|
|
2501
|
+
type: "compute.v1.backendService",
|
|
2502
|
+
properties: {
|
|
2503
|
+
backends: [],
|
|
2504
|
+
healthChecks: [],
|
|
2505
|
+
protocol: lbType === "network" ? "TCP" : "HTTP",
|
|
2506
|
+
loadBalancingScheme: props.scheme === "internal" ? "INTERNAL" : "EXTERNAL"
|
|
2507
|
+
}
|
|
2508
|
+
}];
|
|
2509
|
+
if (lbType === "application") {
|
|
2510
|
+
resources.push({
|
|
2511
|
+
name: `${construct.id}-url-map`,
|
|
2512
|
+
type: "compute.v1.urlMap",
|
|
2513
|
+
properties: {
|
|
2514
|
+
defaultService: `global/backendServices/${construct.id}-backend`
|
|
2515
|
+
}
|
|
2516
|
+
});
|
|
2517
|
+
resources.push({
|
|
2518
|
+
name: `${construct.id}-http-proxy`,
|
|
2519
|
+
type: "compute.v1.targetHttpProxy",
|
|
2520
|
+
properties: {
|
|
2521
|
+
urlMap: `global/urlMaps/${construct.id}-url-map`
|
|
2522
|
+
}
|
|
2523
|
+
});
|
|
2524
|
+
resources.push({
|
|
2525
|
+
name: `${construct.id}-forwarding-rule`,
|
|
2526
|
+
type: "compute.v1.globalForwardingRule",
|
|
2527
|
+
properties: {
|
|
2528
|
+
target: `global/targetHttpProxies/${construct.id}-http-proxy`,
|
|
2529
|
+
portRange: "80",
|
|
2530
|
+
loadBalancingScheme: "EXTERNAL"
|
|
2531
|
+
}
|
|
2532
|
+
});
|
|
2533
|
+
}
|
|
2534
|
+
return resources;
|
|
2535
|
+
}
|
|
2536
|
+
case "Network.CDN": {
|
|
2537
|
+
const origins = props.origins ?? [];
|
|
2538
|
+
return [
|
|
2539
|
+
{
|
|
2540
|
+
name: `${construct.id}-backend-bucket`,
|
|
2541
|
+
type: "compute.v1.backendBucket",
|
|
2542
|
+
properties: {
|
|
2543
|
+
bucketName: origins[0]?.bucketName ?? origins[0]?.domainName ?? construct.id,
|
|
2544
|
+
enableCdn: true,
|
|
2545
|
+
cdnPolicy: {
|
|
2546
|
+
cacheMode: "CACHE_ALL_STATIC",
|
|
2547
|
+
defaultTtl: 3600,
|
|
2548
|
+
maxTtl: 86400
|
|
2549
|
+
}
|
|
2550
|
+
}
|
|
2551
|
+
},
|
|
2552
|
+
{
|
|
2553
|
+
name: `${construct.id}-url-map`,
|
|
2554
|
+
type: "compute.v1.urlMap",
|
|
2555
|
+
properties: {
|
|
2556
|
+
defaultService: `global/backendBuckets/${construct.id}-backend-bucket`
|
|
2557
|
+
}
|
|
2558
|
+
}
|
|
2559
|
+
];
|
|
2560
|
+
}
|
|
2561
|
+
case "Network.Dns": {
|
|
2562
|
+
const records = props.records ?? [];
|
|
2563
|
+
const zoneName = props.zoneName.replace(/\./g, "-").replace(/-+$/, "");
|
|
2564
|
+
const resources = [{
|
|
2565
|
+
name: `${zoneName}-zone`,
|
|
2566
|
+
type: "dns.v1.managedZone",
|
|
2567
|
+
properties: {
|
|
2568
|
+
dnsName: `${props.zoneName}.`,
|
|
2569
|
+
description: `DNS zone for ${props.zoneName}`,
|
|
2570
|
+
visibility: "public"
|
|
2571
|
+
}
|
|
2572
|
+
}];
|
|
2573
|
+
if (records.length > 0) {
|
|
2574
|
+
resources.push({
|
|
2575
|
+
name: `${zoneName}-records`,
|
|
2576
|
+
type: "dns.v1.resourceRecordSet",
|
|
2577
|
+
properties: {
|
|
2578
|
+
managedZone: `${zoneName}-zone`,
|
|
2579
|
+
name: `${records[0].name}.`,
|
|
2580
|
+
type: records[0].type,
|
|
2581
|
+
ttl: records[0].ttl ?? 300,
|
|
2582
|
+
rrdatas: records[0].values
|
|
2583
|
+
}
|
|
2584
|
+
});
|
|
2585
|
+
}
|
|
2586
|
+
return resources;
|
|
2587
|
+
}
|
|
2588
|
+
// ── Database ──────────────────────────────────────────────────────────
|
|
2589
|
+
case "Database.SQL": {
|
|
2590
|
+
const engine = props.engine ?? "mysql";
|
|
2591
|
+
const edition = props.edition ?? "";
|
|
2592
|
+
const dbVersionMap = {
|
|
2593
|
+
mysql: "MYSQL_8_0",
|
|
2594
|
+
postgres: "POSTGRES_15",
|
|
2595
|
+
mariadb: "MYSQL_8_0",
|
|
2596
|
+
// MariaDB → MySQL no Cloud SQL (mais compatível)
|
|
2597
|
+
sqlserver: `SQLSERVER_2019_${(edition || "EXPRESS").toUpperCase()}`,
|
|
2598
|
+
oracle: "POSTGRES_15"
|
|
2599
|
+
// Oracle → AlloyDB/PostgreSQL (postgres-compatible)
|
|
2600
|
+
};
|
|
2601
|
+
const dbVersion = dbVersionMap[engine] ?? "MYSQL_8_0";
|
|
2602
|
+
const defaultTier = engine === "sqlserver" ? "db-custom-2-4096" : "db-f1-micro";
|
|
2603
|
+
return [{
|
|
2604
|
+
name: construct.id,
|
|
2605
|
+
type: "sqladmin.v1beta4.instance",
|
|
2606
|
+
properties: {
|
|
2607
|
+
databaseVersion: dbVersion,
|
|
2608
|
+
region,
|
|
2609
|
+
settings: {
|
|
2610
|
+
tier: props.instanceType ?? defaultTier,
|
|
2611
|
+
backupConfiguration: {
|
|
2612
|
+
enabled: true,
|
|
2613
|
+
binaryLogEnabled: engine === "mysql" || engine === "mariadb"
|
|
2614
|
+
},
|
|
2615
|
+
storageAutoResize: true,
|
|
2616
|
+
storageAutoResizeLimit: props.storageGb ?? 20,
|
|
2617
|
+
availabilityType: props.multiAz ? "REGIONAL" : "ZONAL"
|
|
2618
|
+
}
|
|
2619
|
+
}
|
|
2620
|
+
}];
|
|
2621
|
+
}
|
|
2622
|
+
case "Database.DocumentDB":
|
|
2623
|
+
return [{
|
|
2624
|
+
name: construct.id,
|
|
2625
|
+
type: "firestore.v1.database",
|
|
2626
|
+
properties: {
|
|
2627
|
+
locationId: region,
|
|
2628
|
+
type: "FIRESTORE_NATIVE",
|
|
2629
|
+
concurrencyMode: "PESSIMISTIC",
|
|
2630
|
+
appEngineIntegrationMode: "DISABLED",
|
|
2631
|
+
deleteProtectionState: props.deletionProtection ? "DELETE_PROTECTION_ENABLED" : "DELETE_PROTECTION_DISABLED"
|
|
2632
|
+
}
|
|
2633
|
+
}];
|
|
2634
|
+
case "Database.DynamoDB":
|
|
2635
|
+
return [{
|
|
2636
|
+
name: construct.id.toLowerCase(),
|
|
2637
|
+
type: "bigtableadmin.v2.instance",
|
|
2638
|
+
properties: {
|
|
2639
|
+
parent: "projects/PROJECT_ID",
|
|
2640
|
+
instanceId: construct.id.toLowerCase(),
|
|
2641
|
+
instance: {
|
|
2642
|
+
displayName: construct.id,
|
|
2643
|
+
type: "PRODUCTION"
|
|
2644
|
+
},
|
|
2645
|
+
clusters: {
|
|
2646
|
+
"cluster-1": {
|
|
2647
|
+
location: `projects/PROJECT_ID/locations/${region}-a`,
|
|
2648
|
+
serveNodes: 1,
|
|
2649
|
+
defaultStorageType: "SSD"
|
|
2650
|
+
}
|
|
2651
|
+
}
|
|
2652
|
+
}
|
|
2653
|
+
}];
|
|
2654
|
+
// ── Cache ─────────────────────────────────────────────────────────────
|
|
2655
|
+
case "Cache.Redis": {
|
|
2656
|
+
const nodeType = props.nodeType ?? "small";
|
|
2657
|
+
return [{
|
|
2658
|
+
name: construct.id.toLowerCase().replace(/[^a-z0-9-]/g, "-"),
|
|
2659
|
+
type: "redis.v1.instance",
|
|
2660
|
+
properties: {
|
|
2661
|
+
region,
|
|
2662
|
+
tier: CACHE_TIER_MAP[nodeType] ?? "BASIC",
|
|
2663
|
+
memorySizeGb: CACHE_CAPACITY_MAP[nodeType] ?? 1,
|
|
2664
|
+
redisVersion: `REDIS_${(props.version ?? "7.0").replace(".", "_")}`,
|
|
2665
|
+
authEnabled: props.transitEncryptionEnabled ?? true,
|
|
2666
|
+
transitEncryptionMode: props.transitEncryptionEnabled ? "SERVER_AUTHENTICATION" : "DISABLED",
|
|
2667
|
+
...props.subnetGroupName ? { authorizedNetwork: `projects/PROJECT_ID/global/networks/${props.subnetGroupName}` } : {}
|
|
2668
|
+
}
|
|
2669
|
+
}];
|
|
2670
|
+
}
|
|
2671
|
+
case "Cache.Memcached": {
|
|
2672
|
+
const numNodes = props.numCacheNodes ?? 2;
|
|
2673
|
+
return [{
|
|
2674
|
+
name: construct.id.toLowerCase().replace(/[^a-z0-9-]/g, "-"),
|
|
2675
|
+
type: "memcache.v1.instance",
|
|
2676
|
+
properties: {
|
|
2677
|
+
region,
|
|
2678
|
+
nodeCount: numNodes,
|
|
2679
|
+
nodeConfig: {
|
|
2680
|
+
cpuCount: 1,
|
|
2681
|
+
memorySizeMb: 1024
|
|
2682
|
+
},
|
|
2683
|
+
...props.subnetGroupName ? { authorizedNetwork: `projects/PROJECT_ID/global/networks/${props.subnetGroupName}` } : {}
|
|
2684
|
+
}
|
|
2685
|
+
}];
|
|
2686
|
+
}
|
|
2687
|
+
// ── Function ──────────────────────────────────────────────────────────
|
|
2688
|
+
case "Function.Lambda": {
|
|
2689
|
+
const environment = props.environment ?? {};
|
|
2690
|
+
const runtimeMap = {
|
|
2691
|
+
"nodejs20": "nodejs20",
|
|
2692
|
+
"nodejs18": "nodejs18",
|
|
2693
|
+
"python3.12": "python312",
|
|
2694
|
+
"python3.11": "python311",
|
|
2695
|
+
"java21": "java21",
|
|
2696
|
+
"go1.x": "go121",
|
|
2697
|
+
"dotnet8": "dotnet8"
|
|
2698
|
+
};
|
|
2699
|
+
return [{
|
|
2700
|
+
name: construct.id,
|
|
2701
|
+
type: "cloudfunctions.v2.function",
|
|
2702
|
+
properties: {
|
|
2703
|
+
location: region,
|
|
2704
|
+
description: `Function ${construct.id}`,
|
|
2705
|
+
buildConfig: {
|
|
2706
|
+
runtime: runtimeMap[props.runtime ?? "nodejs20"] ?? "nodejs20",
|
|
2707
|
+
entryPoint: props.handler ?? "handler"
|
|
2708
|
+
},
|
|
2709
|
+
serviceConfig: {
|
|
2710
|
+
availableMemory: `${props.memory ?? 128}Mi`,
|
|
2711
|
+
timeoutSeconds: props.timeout ?? 30,
|
|
2712
|
+
...props.reservedConcurrency ? { maxInstanceCount: props.reservedConcurrency } : {},
|
|
2713
|
+
...Object.keys(environment).length > 0 ? { environmentVariables: environment } : {}
|
|
2714
|
+
}
|
|
2715
|
+
}
|
|
2716
|
+
}];
|
|
2717
|
+
}
|
|
2718
|
+
case "Function.ApiGateway": {
|
|
2719
|
+
const routes = props.routes ?? [];
|
|
2720
|
+
const authorizerLambdaId = props.authorizerLambdaId;
|
|
2721
|
+
const securityDefinitions = authorizerLambdaId ? {
|
|
2722
|
+
lambdaAuthorizer: {
|
|
2723
|
+
type: "apiKey",
|
|
2724
|
+
name: "Authorization",
|
|
2725
|
+
in: "header",
|
|
2726
|
+
"x-google-authorizer-backend": { address: `https://${region}-PROJECT_ID.cloudfunctions.net/${authorizerLambdaId}` }
|
|
2727
|
+
}
|
|
2728
|
+
} : void 0;
|
|
2729
|
+
return [
|
|
2730
|
+
{
|
|
2731
|
+
name: props.name ?? construct.id,
|
|
2732
|
+
type: "apigateway.v1.api",
|
|
2733
|
+
properties: {
|
|
2734
|
+
displayName: props.name ?? construct.id
|
|
2735
|
+
}
|
|
2736
|
+
},
|
|
2737
|
+
{
|
|
2738
|
+
name: `${construct.id}-config`,
|
|
2739
|
+
type: "apigateway.v1.apiConfig",
|
|
2740
|
+
properties: {
|
|
2741
|
+
api: props.name ?? construct.id,
|
|
2742
|
+
displayName: `${construct.id} config`,
|
|
2743
|
+
openapiDocuments: [{
|
|
2744
|
+
document: {
|
|
2745
|
+
path: "openapi.yaml",
|
|
2746
|
+
contents: Buffer.from(JSON.stringify({
|
|
2747
|
+
openapi: "3.0.0",
|
|
2748
|
+
info: { title: props.name ?? construct.id, version: "1.0" },
|
|
2749
|
+
...securityDefinitions ? { securityDefinitions } : {},
|
|
2750
|
+
paths: Object.fromEntries(routes.map((r) => [r.path, {
|
|
2751
|
+
[r.method.toLowerCase()]: {
|
|
2752
|
+
"x-google-backend": { address: `https://${region}-PROJECT_ID.cloudfunctions.net/${r.lambdaId ?? "function"}` },
|
|
2753
|
+
...securityDefinitions ? { security: [{ lambdaAuthorizer: [] }] } : {},
|
|
2754
|
+
responses: { "200": { description: "OK" } }
|
|
2755
|
+
}
|
|
2756
|
+
}]))
|
|
2757
|
+
})).toString("base64")
|
|
2758
|
+
}
|
|
2759
|
+
}]
|
|
2760
|
+
}
|
|
2761
|
+
}
|
|
2762
|
+
];
|
|
2763
|
+
}
|
|
2764
|
+
// ── Policy ────────────────────────────────────────────────────────────
|
|
2765
|
+
case "Policy.IAM": {
|
|
2766
|
+
const statements = props.statements ?? [];
|
|
2767
|
+
const attachTo = props.attachTo;
|
|
2768
|
+
const serviceAccount = `${attachTo.toLowerCase().replace(/[^a-z0-9-]/g, "-").slice(0, 30)}@PROJECT_ID.iam.gserviceaccount.com`;
|
|
2769
|
+
return [
|
|
2770
|
+
{
|
|
2771
|
+
name: `${construct.id}-sa`,
|
|
2772
|
+
type: "iam.v1.serviceAccount",
|
|
2773
|
+
properties: {
|
|
2774
|
+
accountId: attachTo.toLowerCase().replace(/[^a-z0-9-]/g, "-").slice(0, 30),
|
|
2775
|
+
displayName: `Service Account for ${attachTo}`,
|
|
2776
|
+
description: props.description ?? ""
|
|
2777
|
+
}
|
|
2778
|
+
},
|
|
2779
|
+
...statements.map((s, i) => ({
|
|
2780
|
+
name: `${construct.id}-binding-${i}`,
|
|
2781
|
+
type: "gcp-types/cloudresourcemanager-v1:virtual.projects.iamMemberBinding",
|
|
2782
|
+
properties: {
|
|
2783
|
+
resource: "PROJECT_ID",
|
|
2784
|
+
role: s.actions[0]?.startsWith("roles/") ? s.actions[0] : `roles/custom.${construct.id.replace(/[^a-zA-Z0-9]/g, "")}`,
|
|
2785
|
+
member: `serviceAccount:${serviceAccount}`
|
|
2786
|
+
}
|
|
2787
|
+
}))
|
|
2788
|
+
];
|
|
2789
|
+
}
|
|
2790
|
+
// ── Events ────────────────────────────────────────────────────────────
|
|
2791
|
+
case "Events.EventBridge": {
|
|
2792
|
+
const rules = props.rules ?? [];
|
|
2793
|
+
const resources = [];
|
|
2794
|
+
if (props.busName && props.busName !== "default") {
|
|
2795
|
+
resources.push({
|
|
2796
|
+
name: props.busName,
|
|
2797
|
+
type: "pubsub.v1.topic",
|
|
2798
|
+
properties: {
|
|
2799
|
+
messageStoragePolicy: {
|
|
2800
|
+
allowedPersistenceRegions: [region]
|
|
2801
|
+
}
|
|
2802
|
+
}
|
|
2803
|
+
});
|
|
2804
|
+
}
|
|
2805
|
+
for (const r of rules) {
|
|
2806
|
+
const topicName = `${construct.id}-${r.name ?? "rule"}`;
|
|
2807
|
+
resources.push({
|
|
2808
|
+
name: topicName,
|
|
2809
|
+
type: "pubsub.v1.topic",
|
|
2810
|
+
properties: {
|
|
2811
|
+
messageStoragePolicy: {
|
|
2812
|
+
allowedPersistenceRegions: [region]
|
|
2813
|
+
}
|
|
2814
|
+
}
|
|
2815
|
+
});
|
|
2816
|
+
if (r.targetArn) {
|
|
2817
|
+
resources.push({
|
|
2818
|
+
name: `${topicName}-sub`,
|
|
2819
|
+
type: "pubsub.v1.subscription",
|
|
2820
|
+
properties: {
|
|
2821
|
+
topic: topicName,
|
|
2822
|
+
pushConfig: { pushEndpoint: r.targetArn },
|
|
2823
|
+
ackDeadlineSeconds: 30
|
|
2824
|
+
}
|
|
2825
|
+
});
|
|
2826
|
+
}
|
|
2827
|
+
}
|
|
2828
|
+
return resources;
|
|
2829
|
+
}
|
|
2830
|
+
// ── Workflow ──────────────────────────────────────────────────────────
|
|
2831
|
+
case "Workflow.StepFunctions": {
|
|
2832
|
+
const steps = props.steps ?? [];
|
|
2833
|
+
const definition = {
|
|
2834
|
+
main: {
|
|
2835
|
+
steps: steps.map((s, i) => ({
|
|
2836
|
+
[s.name]: {
|
|
2837
|
+
call: "http.post",
|
|
2838
|
+
args: { url: s.resource ?? "" },
|
|
2839
|
+
...i < steps.length - 1 ? { next: steps[i + 1].name } : {}
|
|
2840
|
+
}
|
|
2841
|
+
}))
|
|
2842
|
+
}
|
|
2843
|
+
};
|
|
2844
|
+
return [{
|
|
2845
|
+
name: construct.id,
|
|
2846
|
+
type: "workflows.v1.workflow",
|
|
2847
|
+
properties: {
|
|
2848
|
+
region,
|
|
2849
|
+
description: props.description ?? "",
|
|
2850
|
+
sourceContents: JSON.stringify(definition, null, 2)
|
|
2851
|
+
}
|
|
2852
|
+
}];
|
|
2853
|
+
}
|
|
2854
|
+
// ── Messaging ─────────────────────────────────────────────────────────
|
|
2855
|
+
case "Messaging.Queue": {
|
|
2856
|
+
const topicName = construct.id;
|
|
2857
|
+
const subName = `${construct.id}-sub`;
|
|
2858
|
+
return [
|
|
2859
|
+
{
|
|
2860
|
+
name: topicName,
|
|
2861
|
+
type: "pubsub.v1.topic",
|
|
2862
|
+
properties: {
|
|
2863
|
+
messageStoragePolicy: { allowedPersistenceRegions: [region] },
|
|
2864
|
+
...props.encrypted ? { kmsKeyName: `projects/PROJECT_ID/locations/${region}/keyRings/default/cryptoKeys/default` } : {}
|
|
2865
|
+
}
|
|
2866
|
+
},
|
|
2867
|
+
{
|
|
2868
|
+
name: subName,
|
|
2869
|
+
type: "pubsub.v1.subscription",
|
|
2870
|
+
properties: {
|
|
2871
|
+
topic: topicName,
|
|
2872
|
+
ackDeadlineSeconds: props.visibilityTimeoutSeconds ?? 30,
|
|
2873
|
+
messageRetentionDuration: `${props.messageRetentionSeconds ?? 345600}s`,
|
|
2874
|
+
retainAckedMessages: false,
|
|
2875
|
+
...props.dlqArn ? {
|
|
2876
|
+
deadLetterPolicy: {
|
|
2877
|
+
deadLetterTopic: props.dlqArn,
|
|
2878
|
+
maxDeliveryAttempts: props.maxReceiveCount ?? 5
|
|
2879
|
+
}
|
|
2880
|
+
} : {}
|
|
2881
|
+
}
|
|
2882
|
+
}
|
|
2883
|
+
];
|
|
2884
|
+
}
|
|
2885
|
+
case "Messaging.Topic": {
|
|
2886
|
+
const subscriptions = props.subscriptions ?? [];
|
|
2887
|
+
const resources = [{
|
|
2888
|
+
name: construct.id,
|
|
2889
|
+
type: "pubsub.v1.topic",
|
|
2890
|
+
properties: {
|
|
2891
|
+
messageStoragePolicy: { allowedPersistenceRegions: [region] },
|
|
2892
|
+
...props.encrypted ? { kmsKeyName: `projects/PROJECT_ID/locations/${region}/keyRings/default/cryptoKeys/default` } : {}
|
|
2893
|
+
}
|
|
2894
|
+
}];
|
|
2895
|
+
subscriptions.forEach((s, i) => {
|
|
2896
|
+
resources.push({
|
|
2897
|
+
name: `${construct.id}-sub-${i}`,
|
|
2898
|
+
type: "pubsub.v1.subscription",
|
|
2899
|
+
properties: {
|
|
2900
|
+
topic: construct.id,
|
|
2901
|
+
ackDeadlineSeconds: 30,
|
|
2902
|
+
...s.protocol === "https" || s.protocol === "http" ? { pushConfig: { pushEndpoint: s.endpoint } } : {}
|
|
2903
|
+
}
|
|
2904
|
+
});
|
|
2905
|
+
});
|
|
2906
|
+
return resources;
|
|
2907
|
+
}
|
|
2908
|
+
// ── Secret / Certificate ──────────────────────────────────────────────
|
|
2909
|
+
case "Secret.Vault": {
|
|
2910
|
+
const resources = [{
|
|
2911
|
+
name: construct.id.replace(/[^a-zA-Z0-9_-]/g, "-"),
|
|
2912
|
+
type: "secretmanager.v1.secret",
|
|
2913
|
+
properties: {
|
|
2914
|
+
parent: "projects/PROJECT_ID",
|
|
2915
|
+
secretId: construct.id.replace(/[^a-zA-Z0-9_-]/g, "-"),
|
|
2916
|
+
secret: {
|
|
2917
|
+
replication: {
|
|
2918
|
+
automatic: {}
|
|
2919
|
+
},
|
|
2920
|
+
...props.kmsKeyId ? { customerManagedEncryption: { kmsKeyName: props.kmsKeyId } } : {},
|
|
2921
|
+
...props.rotationDays ? {
|
|
2922
|
+
rotation: {
|
|
2923
|
+
nextRotationTime: new Date(Date.now() + props.rotationDays * 864e5).toISOString(),
|
|
2924
|
+
rotationPeriod: `${props.rotationDays * 86400}s`
|
|
2925
|
+
}
|
|
2926
|
+
} : {}
|
|
2927
|
+
}
|
|
2928
|
+
}
|
|
2929
|
+
}];
|
|
2930
|
+
return resources;
|
|
2931
|
+
}
|
|
2932
|
+
case "Certificate.TLS": {
|
|
2933
|
+
const sans = props.subjectAlternativeNames ?? [];
|
|
2934
|
+
return [{
|
|
2935
|
+
name: construct.id.replace(/[^a-zA-Z0-9-]/g, "-"),
|
|
2936
|
+
type: "certificatemanager.v1.certificate",
|
|
2937
|
+
properties: {
|
|
2938
|
+
name: construct.id.replace(/[^a-zA-Z0-9-]/g, "-"),
|
|
2939
|
+
managed: {
|
|
2940
|
+
domains: [props.domainName, ...sans]
|
|
2941
|
+
},
|
|
2942
|
+
scope: props.region ? "REGIONAL" : "DEFAULT"
|
|
2943
|
+
}
|
|
2944
|
+
}];
|
|
2945
|
+
}
|
|
2946
|
+
// ── Monitoring ────────────────────────────────────────────────────────
|
|
2947
|
+
case "Monitoring.Alarm": {
|
|
2948
|
+
const dimensions = props.dimensions;
|
|
2949
|
+
const operatorMap = {
|
|
2950
|
+
GreaterThanThreshold: "COMPARISON_GT",
|
|
2951
|
+
LessThanThreshold: "COMPARISON_LT",
|
|
2952
|
+
GreaterThanOrEqualToThreshold: "COMPARISON_GE",
|
|
2953
|
+
LessThanOrEqualToThreshold: "COMPARISON_LE"
|
|
2954
|
+
};
|
|
2955
|
+
const dimFilter = dimensions ? Object.entries(dimensions).map(([k, v]) => `metric.labels.${k}="${v}"`).join(" AND ") : "";
|
|
2956
|
+
return [{
|
|
2957
|
+
name: construct.id,
|
|
2958
|
+
type: "monitoring.v3.alertPolicy",
|
|
2959
|
+
properties: {
|
|
2960
|
+
displayName: construct.id,
|
|
2961
|
+
conditions: [{
|
|
2962
|
+
displayName: `${props.metricName} condition`,
|
|
2963
|
+
conditionThreshold: {
|
|
2964
|
+
filter: [
|
|
2965
|
+
`metric.type="${props.namespace ?? "cloudfunctions.googleapis.com/function"}/${props.metricName}"`,
|
|
2966
|
+
dimFilter
|
|
2967
|
+
].filter(Boolean).join(" AND "),
|
|
2968
|
+
comparison: operatorMap[props.comparisonOperator ?? "GreaterThanThreshold"] ?? "COMPARISON_GT",
|
|
2969
|
+
thresholdValue: props.threshold,
|
|
2970
|
+
duration: `${(props.periodSeconds ?? 60) * (props.evaluationPeriods ?? 2)}s`,
|
|
2971
|
+
aggregations: [{
|
|
2972
|
+
alignmentPeriod: `${props.periodSeconds ?? 60}s`,
|
|
2973
|
+
perSeriesAligner: "ALIGN_MEAN"
|
|
2974
|
+
}]
|
|
2975
|
+
}
|
|
2976
|
+
}],
|
|
2977
|
+
alertStrategy: { notificationRateLimit: { period: "300s" } },
|
|
2978
|
+
combiner: "OR",
|
|
2979
|
+
enabled: true,
|
|
2980
|
+
notificationChannels: props.alarmActions ?? []
|
|
2981
|
+
}
|
|
2982
|
+
}];
|
|
2983
|
+
}
|
|
2984
|
+
case "Monitoring.Dashboard": {
|
|
2985
|
+
const widgets = props.widgets ?? [];
|
|
2986
|
+
return [{
|
|
2987
|
+
name: construct.id,
|
|
2988
|
+
type: "monitoring.v1.dashboard",
|
|
2989
|
+
properties: {
|
|
2990
|
+
displayName: construct.id,
|
|
2991
|
+
gridLayout: {
|
|
2992
|
+
columns: 3,
|
|
2993
|
+
widgets: widgets.map((w) => ({
|
|
2994
|
+
title: w.title,
|
|
2995
|
+
...w.type === "text" ? {
|
|
2996
|
+
text: { content: w.markdown ?? w.title, format: "MARKDOWN" }
|
|
2997
|
+
} : {
|
|
2998
|
+
xyChart: {
|
|
2999
|
+
dataSets: [{
|
|
3000
|
+
timeSeriesQuery: {
|
|
3001
|
+
timeSeriesFilter: {
|
|
3002
|
+
filter: `metric.type="${w.namespace ?? "cloudfunctions.googleapis.com"}/${w.metricName}"`,
|
|
3003
|
+
aggregation: { alignmentPeriod: `${w.period ?? 60}s`, perSeriesAligner: "ALIGN_MEAN" }
|
|
3004
|
+
}
|
|
3005
|
+
}
|
|
3006
|
+
}]
|
|
3007
|
+
}
|
|
3008
|
+
}
|
|
3009
|
+
}))
|
|
3010
|
+
}
|
|
3011
|
+
}
|
|
3012
|
+
}];
|
|
3013
|
+
}
|
|
3014
|
+
case "Logging.Stream": {
|
|
3015
|
+
const filters = props.subscriptionFilters ?? [];
|
|
3016
|
+
const resources = [{
|
|
3017
|
+
name: construct.id.replace(/[^a-zA-Z0-9_-]/g, "-"),
|
|
3018
|
+
type: "logging.v2.logBucket",
|
|
3019
|
+
properties: {
|
|
3020
|
+
parent: `projects/PROJECT_ID/locations/${region}`,
|
|
3021
|
+
bucketId: construct.id.replace(/[^a-zA-Z0-9_-]/g, "-"),
|
|
3022
|
+
retentionDays: props.retentionDays ?? 30
|
|
3023
|
+
}
|
|
3024
|
+
}];
|
|
3025
|
+
for (const f of filters) {
|
|
3026
|
+
resources.push({
|
|
3027
|
+
name: `${construct.id}-sink-${f.name.replace(/[^a-zA-Z0-9-]/g, "-")}`,
|
|
3028
|
+
type: "logging.v2.sink",
|
|
3029
|
+
properties: {
|
|
3030
|
+
parent: `projects/PROJECT_ID`,
|
|
3031
|
+
uniqueWriterIdentity: true,
|
|
3032
|
+
filter: f.filterPattern,
|
|
3033
|
+
destination: f.destinationArn
|
|
3034
|
+
}
|
|
3035
|
+
});
|
|
3036
|
+
}
|
|
3037
|
+
return resources;
|
|
3038
|
+
}
|
|
3039
|
+
case "Custom.Resource": {
|
|
3040
|
+
const dm = props.deploymentManager;
|
|
3041
|
+
if (!dm)
|
|
3042
|
+
return [];
|
|
3043
|
+
return [{ name: props.name ?? construct.id, type: dm.type, properties: dm.properties }];
|
|
3044
|
+
}
|
|
3045
|
+
default:
|
|
3046
|
+
console.warn(`[gcp] Construct type '${construct.type}' nao suportado \u2014 descartado.`);
|
|
3047
|
+
return [];
|
|
3048
|
+
}
|
|
3049
|
+
}
|
|
3050
|
+
function synthesize(stack) {
|
|
3051
|
+
const resources = [];
|
|
3052
|
+
for (const construct of stack.constructs) {
|
|
3053
|
+
const result = synthesizeConstruct(construct);
|
|
3054
|
+
resources.push(...result);
|
|
3055
|
+
}
|
|
3056
|
+
return { resources };
|
|
3057
|
+
}
|
|
3058
|
+
}
|
|
3059
|
+
});
|
|
3060
|
+
|
|
3061
|
+
// ../providers/gcp/dist/provider.js
|
|
3062
|
+
var require_provider3 = __commonJS({
|
|
3063
|
+
"../providers/gcp/dist/provider.js"(exports2) {
|
|
3064
|
+
"use strict";
|
|
3065
|
+
Object.defineProperty(exports2, "__esModule", { value: true });
|
|
3066
|
+
exports2.GCPProvider = void 0;
|
|
3067
|
+
var deployment_manager_1 = require_deployment_manager();
|
|
3068
|
+
var GCPProvider2 = class {
|
|
3069
|
+
name = "gcp";
|
|
3070
|
+
synthesize(stack) {
|
|
3071
|
+
return (0, deployment_manager_1.synthesize)(stack);
|
|
3072
|
+
}
|
|
3073
|
+
};
|
|
3074
|
+
exports2.GCPProvider = GCPProvider2;
|
|
3075
|
+
}
|
|
3076
|
+
});
|
|
3077
|
+
|
|
3078
|
+
// ../providers/gcp/dist/index.js
|
|
3079
|
+
var require_dist3 = __commonJS({
|
|
3080
|
+
"../providers/gcp/dist/index.js"(exports2) {
|
|
3081
|
+
"use strict";
|
|
3082
|
+
Object.defineProperty(exports2, "__esModule", { value: true });
|
|
3083
|
+
exports2.synthesize = exports2.GCPProvider = void 0;
|
|
3084
|
+
var provider_1 = require_provider3();
|
|
3085
|
+
Object.defineProperty(exports2, "GCPProvider", { enumerable: true, get: function() {
|
|
3086
|
+
return provider_1.GCPProvider;
|
|
3087
|
+
} });
|
|
3088
|
+
var deployment_manager_1 = require_deployment_manager();
|
|
3089
|
+
Object.defineProperty(exports2, "synthesize", { enumerable: true, get: function() {
|
|
3090
|
+
return deployment_manager_1.synthesize;
|
|
3091
|
+
} });
|
|
3092
|
+
}
|
|
3093
|
+
});
|
|
3094
|
+
|
|
3095
|
+
// ../providers/terraform/dist/synth/hcl.js
|
|
3096
|
+
var require_hcl = __commonJS({
|
|
3097
|
+
"../providers/terraform/dist/synth/hcl.js"(exports2) {
|
|
3098
|
+
"use strict";
|
|
3099
|
+
Object.defineProperty(exports2, "__esModule", { value: true });
|
|
3100
|
+
exports2.hclString = hclString;
|
|
3101
|
+
exports2.synthesize = synthesize;
|
|
3102
|
+
var INSTANCE_TYPE_MAP = {
|
|
3103
|
+
small: "t3.small",
|
|
3104
|
+
medium: "t3.medium",
|
|
3105
|
+
large: "t3.large"
|
|
3106
|
+
};
|
|
3107
|
+
var AMI_MAP = {
|
|
3108
|
+
"ubuntu": "data.aws_ami.ubuntu.id",
|
|
3109
|
+
"ubuntu-22.04": "data.aws_ami.ubuntu.id",
|
|
3110
|
+
"amazon-linux-2": "data.aws_ami.amazon_linux.id",
|
|
3111
|
+
"amazon-linux-2023": "data.aws_ami.amazon_linux_2023.id",
|
|
3112
|
+
"windows-2022": "data.aws_ami.windows_2022.id",
|
|
3113
|
+
"windows-2019": "data.aws_ami.windows_2019.id",
|
|
3114
|
+
"windows-2016": "data.aws_ami.windows_2016.id"
|
|
3115
|
+
};
|
|
3116
|
+
var CACHE_NODE_TYPE_MAP = {
|
|
3117
|
+
small: "cache.t3.micro",
|
|
3118
|
+
medium: "cache.t3.medium",
|
|
3119
|
+
large: "cache.r6g.large"
|
|
3120
|
+
};
|
|
3121
|
+
var K8S_NODE_TYPE_MAP = {
|
|
3122
|
+
small: "t3.medium",
|
|
3123
|
+
medium: "m5.large",
|
|
3124
|
+
large: "m5.2xlarge"
|
|
3125
|
+
};
|
|
3126
|
+
function indent(text, spaces = 2) {
|
|
3127
|
+
return text.split("\n").map((l) => l.trim() ? " ".repeat(spaces) + l : l).join("\n");
|
|
3128
|
+
}
|
|
3129
|
+
function block(type, labels, body) {
|
|
3130
|
+
const labelStr = labels.map((l) => ` "${l}"`).join("");
|
|
3131
|
+
return `${type}${labelStr} {
|
|
3132
|
+
${body}
|
|
3133
|
+
}
|
|
3134
|
+
`;
|
|
3135
|
+
}
|
|
3136
|
+
function hclString(value) {
|
|
3137
|
+
return String(value ?? "").replace(/\\/g, "\\\\").replace(/"/g, '\\"').replace(/\$\{/g, "$$$${").replace(/%\{/g, "%%{");
|
|
3138
|
+
}
|
|
3139
|
+
function attr(key, value) {
|
|
3140
|
+
if (typeof value === "string")
|
|
3141
|
+
return `${key} = "${hclString(value)}"`;
|
|
3142
|
+
return `${key} = ${value}`;
|
|
3143
|
+
}
|
|
3144
|
+
function tagsBlock(name) {
|
|
3145
|
+
return indent(`tags = {
|
|
3146
|
+
${indent(`Name = "${hclString(name)}"`)}
|
|
3147
|
+
}`);
|
|
3148
|
+
}
|
|
3149
|
+
function hclValue(value) {
|
|
3150
|
+
if (typeof value === "string") {
|
|
3151
|
+
if (/^[a-zA-Z_][a-zA-Z0-9_.\[\]]*$/.test(value) && /\./.test(value))
|
|
3152
|
+
return value;
|
|
3153
|
+
return `"${hclString(value)}"`;
|
|
3154
|
+
}
|
|
3155
|
+
if (typeof value === "number" || typeof value === "boolean")
|
|
3156
|
+
return String(value);
|
|
3157
|
+
if (Array.isArray(value))
|
|
3158
|
+
return `[${value.map(hclValue).join(", ")}]`;
|
|
3159
|
+
if (value && typeof value === "object")
|
|
3160
|
+
return hclBody(value);
|
|
3161
|
+
return "null";
|
|
3162
|
+
}
|
|
3163
|
+
function hclBody(obj) {
|
|
3164
|
+
const lines = Object.entries(obj).map(([k, v]) => `${k} = ${hclValue(v)}`);
|
|
3165
|
+
return `{
|
|
3166
|
+
${indent(lines.join("\n"))}
|
|
3167
|
+
}`;
|
|
3168
|
+
}
|
|
3169
|
+
function synthesizeConstruct(construct) {
|
|
3170
|
+
const props = construct.props;
|
|
3171
|
+
const id = construct.id.replace(/[^a-zA-Z0-9_]/g, "_");
|
|
3172
|
+
switch (construct.type) {
|
|
3173
|
+
// ── Compute ──────────────────────────────────────────────────────────
|
|
3174
|
+
case "Compute.Instance": {
|
|
3175
|
+
const instanceType = INSTANCE_TYPE_MAP[props.instanceType] ?? "t3.small";
|
|
3176
|
+
const ami = AMI_MAP[props.image];
|
|
3177
|
+
const body = indent([
|
|
3178
|
+
`ami = ${ami ? ami : `"${hclString(props.image)}"`}`,
|
|
3179
|
+
attr("instance_type", instanceType),
|
|
3180
|
+
"",
|
|
3181
|
+
tagsBlock(construct.id)
|
|
3182
|
+
].join("\n"));
|
|
3183
|
+
return block("resource", ["aws_instance", id], body);
|
|
3184
|
+
}
|
|
3185
|
+
case "Compute.AutoScaling": {
|
|
3186
|
+
const instanceType = INSTANCE_TYPE_MAP[props.instanceType] ?? "t3.small";
|
|
3187
|
+
const image = props.image;
|
|
3188
|
+
const lcBody = indent([
|
|
3189
|
+
`image_id = "${hclString(image)}"`,
|
|
3190
|
+
attr("instance_type", instanceType)
|
|
3191
|
+
].join("\n"));
|
|
3192
|
+
const asgBody = indent([
|
|
3193
|
+
`launch_configuration = aws_launch_configuration.${id}_lc.name`,
|
|
3194
|
+
attr("min_size", props.minCapacity),
|
|
3195
|
+
attr("max_size", props.maxCapacity),
|
|
3196
|
+
attr("desired_capacity", props.desiredCapacity ?? props.minCapacity),
|
|
3197
|
+
...props.subnetIds ? [`vpc_zone_identifier = ${JSON.stringify(props.subnetIds)}`] : [],
|
|
3198
|
+
"",
|
|
3199
|
+
tagsBlock(construct.id)
|
|
3200
|
+
].join("\n"));
|
|
3201
|
+
const parts = [
|
|
3202
|
+
block("resource", ["aws_launch_configuration", `${id}_lc`], lcBody),
|
|
3203
|
+
block("resource", ["aws_autoscaling_group", id], asgBody)
|
|
3204
|
+
];
|
|
3205
|
+
if (props.targetCpuUtilization) {
|
|
3206
|
+
const spBody = indent([
|
|
3207
|
+
`autoscaling_group_name = aws_autoscaling_group.${id}.name`,
|
|
3208
|
+
attr("policy_type", "TargetTrackingScaling"),
|
|
3209
|
+
`target_tracking_configuration {
|
|
3210
|
+
predefined_metric_specification {
|
|
3211
|
+
predefined_metric_type = "ASGAverageCPUUtilization"
|
|
3212
|
+
}
|
|
3213
|
+
target_value = ${props.targetCpuUtilization}
|
|
3214
|
+
}`
|
|
3215
|
+
].join("\n"));
|
|
3216
|
+
parts.push(block("resource", ["aws_autoscaling_policy", `${id}_sp`], spBody));
|
|
3217
|
+
}
|
|
3218
|
+
return parts.join("\n");
|
|
3219
|
+
}
|
|
3220
|
+
case "Compute.Container": {
|
|
3221
|
+
const environment = props.environment;
|
|
3222
|
+
const containerDef = JSON.stringify([{
|
|
3223
|
+
name: construct.id,
|
|
3224
|
+
image: props.image,
|
|
3225
|
+
cpu: props.cpu ?? 256,
|
|
3226
|
+
memory: props.memory ?? 512,
|
|
3227
|
+
portMappings: props.port ? [{ containerPort: props.port, protocol: "tcp" }] : [],
|
|
3228
|
+
environment: environment ? Object.entries(environment).map(([k, v]) => ({ name: k, value: v })) : []
|
|
3229
|
+
}]);
|
|
3230
|
+
const clusterBody = indent([attr("name", construct.id)].join("\n"));
|
|
3231
|
+
const tdBody = indent([
|
|
3232
|
+
attr("family", construct.id),
|
|
3233
|
+
attr("network_mode", "awsvpc"),
|
|
3234
|
+
`requires_compatibilities = ["FARGATE"]`,
|
|
3235
|
+
attr("cpu", String(props.cpu ?? 256)),
|
|
3236
|
+
attr("memory", String(props.memory ?? 512)),
|
|
3237
|
+
`execution_role_arn = "arn:aws:iam::ACCOUNT_ID:role/ecsTaskExecutionRole"`,
|
|
3238
|
+
`container_definitions = jsonencode(${containerDef})`
|
|
3239
|
+
].join("\n"));
|
|
3240
|
+
const svcBody = indent([
|
|
3241
|
+
`cluster = aws_ecs_cluster.${id}.id`,
|
|
3242
|
+
`task_definition = aws_ecs_task_definition.${id}.arn`,
|
|
3243
|
+
attr("desired_count", props.desiredCount ?? 1),
|
|
3244
|
+
attr("launch_type", "FARGATE"),
|
|
3245
|
+
`network_configuration {
|
|
3246
|
+
assign_public_ip = ${props.publicIp ?? false}
|
|
3247
|
+
subnets = []
|
|
3248
|
+
}`
|
|
3249
|
+
].join("\n"));
|
|
3250
|
+
return [
|
|
3251
|
+
block("resource", ["aws_ecs_cluster", id], clusterBody),
|
|
3252
|
+
block("resource", ["aws_ecs_task_definition", id], tdBody),
|
|
3253
|
+
block("resource", ["aws_ecs_service", id], svcBody)
|
|
3254
|
+
].join("\n");
|
|
3255
|
+
}
|
|
3256
|
+
case "Compute.Kubernetes": {
|
|
3257
|
+
const nodeType = K8S_NODE_TYPE_MAP[props.nodeInstanceType ?? "medium"] ?? "m5.large";
|
|
3258
|
+
const clusterBody = indent([
|
|
3259
|
+
attr("name", construct.id),
|
|
3260
|
+
attr("version", props.version ?? "1.29"),
|
|
3261
|
+
`role_arn = "arn:aws:iam::ACCOUNT_ID:role/EKSClusterRole"`,
|
|
3262
|
+
`vpc_config {
|
|
3263
|
+
subnet_ids = []
|
|
3264
|
+
endpoint_private_access = ${props.privateCluster ?? false}
|
|
3265
|
+
endpoint_public_access = ${!props.privateCluster}
|
|
3266
|
+
}`,
|
|
3267
|
+
"",
|
|
3268
|
+
tagsBlock(construct.id)
|
|
3269
|
+
].join("\n"));
|
|
3270
|
+
const ngBody = indent([
|
|
3271
|
+
`cluster_name = aws_eks_cluster.${id}.name`,
|
|
3272
|
+
attr("node_group_name", `${construct.id}-ng`),
|
|
3273
|
+
`node_role_arn = "arn:aws:iam::ACCOUNT_ID:role/EKSNodeRole"`,
|
|
3274
|
+
`subnet_ids = []`,
|
|
3275
|
+
`scaling_config {
|
|
3276
|
+
desired_size = ${props.desiredNodes ?? 2}
|
|
3277
|
+
max_size = ${props.maxNodes ?? 3}
|
|
3278
|
+
min_size = ${props.minNodes ?? 1}
|
|
3279
|
+
}`,
|
|
3280
|
+
`instance_types = ["${nodeType}"]`
|
|
3281
|
+
].join("\n"));
|
|
3282
|
+
return [
|
|
3283
|
+
block("resource", ["aws_eks_cluster", id], clusterBody),
|
|
3284
|
+
block("resource", ["aws_eks_node_group", `${id}_ng`], ngBody)
|
|
3285
|
+
].join("\n");
|
|
3286
|
+
}
|
|
3287
|
+
// ── Storage ───────────────────────────────────────────────────────────
|
|
3288
|
+
case "Storage.Bucket": {
|
|
3289
|
+
const versioning = props.versioning ?? false;
|
|
3290
|
+
const blockPublic = !props.publicAccess;
|
|
3291
|
+
const lifecycleRules = props.lifecycleRules ?? [];
|
|
3292
|
+
const bucketBody = indent([
|
|
3293
|
+
attr("bucket", construct.id.toLowerCase()),
|
|
3294
|
+
"",
|
|
3295
|
+
tagsBlock(construct.id)
|
|
3296
|
+
].join("\n"));
|
|
3297
|
+
const versioningBody = indent([
|
|
3298
|
+
`bucket = aws_s3_bucket.${id}.id`,
|
|
3299
|
+
`versioning_configuration {
|
|
3300
|
+
status = "${versioning ? "Enabled" : "Suspended"}"
|
|
3301
|
+
}`
|
|
3302
|
+
].join("\n"));
|
|
3303
|
+
const pabBody = indent([
|
|
3304
|
+
`bucket = aws_s3_bucket.${id}.id`,
|
|
3305
|
+
attr("block_public_acls", blockPublic),
|
|
3306
|
+
attr("block_public_policy", blockPublic),
|
|
3307
|
+
attr("ignore_public_acls", blockPublic),
|
|
3308
|
+
attr("restrict_public_buckets", blockPublic)
|
|
3309
|
+
].join("\n"));
|
|
3310
|
+
const parts = [
|
|
3311
|
+
block("resource", ["aws_s3_bucket", id], bucketBody),
|
|
3312
|
+
block("resource", ["aws_s3_bucket_versioning", `${id}_versioning`], versioningBody),
|
|
3313
|
+
block("resource", ["aws_s3_bucket_public_access_block", `${id}_pab`], pabBody)
|
|
3314
|
+
];
|
|
3315
|
+
if (lifecycleRules.length > 0) {
|
|
3316
|
+
const lcLines = lifecycleRules.map((r, i) => {
|
|
3317
|
+
return [
|
|
3318
|
+
` rule {`,
|
|
3319
|
+
` id = "rule-${i}"`,
|
|
3320
|
+
` status = "Enabled"`,
|
|
3321
|
+
r.prefix ? ` filter {
|
|
3322
|
+
prefix = "${hclString(r.prefix)}"
|
|
3323
|
+
}` : "",
|
|
3324
|
+
r.expireAfterDays ? ` expiration {
|
|
3325
|
+
days = ${r.expireAfterDays}
|
|
3326
|
+
}` : "",
|
|
3327
|
+
r.transitionToGlacierDays ? ` transition {
|
|
3328
|
+
days = ${r.transitionToGlacierDays}
|
|
3329
|
+
storage_class = "GLACIER"
|
|
3330
|
+
}` : "",
|
|
3331
|
+
` }`
|
|
3332
|
+
].filter(Boolean).join("\n");
|
|
3333
|
+
}).join("\n");
|
|
3334
|
+
parts.push(block("resource", ["aws_s3_bucket_lifecycle_configuration", `${id}_lc`], indent(`bucket = aws_s3_bucket.${id}.id
|
|
3335
|
+
${lcLines}`)));
|
|
3336
|
+
}
|
|
3337
|
+
return parts.join("\n");
|
|
3338
|
+
}
|
|
3339
|
+
case "Storage.FileSystem": {
|
|
3340
|
+
const fsBody = indent([
|
|
3341
|
+
attr("performance_mode", props.performanceMode ?? "generalPurpose"),
|
|
3342
|
+
attr("throughput_mode", props.throughputMode ?? "bursting"),
|
|
3343
|
+
attr("encrypted", props.encrypted ?? true),
|
|
3344
|
+
`lifecycle_policy {
|
|
3345
|
+
transition_to_ia = "AFTER_30_DAYS"
|
|
3346
|
+
}`,
|
|
3347
|
+
"",
|
|
3348
|
+
tagsBlock(construct.id)
|
|
3349
|
+
].join("\n"));
|
|
3350
|
+
const parts = [block("resource", ["aws_efs_file_system", id], fsBody)];
|
|
3351
|
+
const accessPoints = props.accessPoints ?? [];
|
|
3352
|
+
for (const ap of accessPoints) {
|
|
3353
|
+
const apId = `${id}_ap_${ap.name.replace(/[^a-zA-Z0-9_]/g, "_")}`;
|
|
3354
|
+
const apBody = indent([
|
|
3355
|
+
`file_system_id = aws_efs_file_system.${id}.id`,
|
|
3356
|
+
`root_directory {
|
|
3357
|
+
path = "${hclString(ap.path)}"
|
|
3358
|
+
}`,
|
|
3359
|
+
ap.uid ? `posix_user {
|
|
3360
|
+
uid = ${ap.uid}
|
|
3361
|
+
gid = ${ap.gid ?? ap.uid}
|
|
3362
|
+
}` : ""
|
|
3363
|
+
].filter(Boolean).join("\n"));
|
|
3364
|
+
parts.push(block("resource", ["aws_efs_access_point", apId], apBody));
|
|
3365
|
+
}
|
|
3366
|
+
return parts.join("\n");
|
|
3367
|
+
}
|
|
3368
|
+
case "Storage.Archive": {
|
|
3369
|
+
const body = indent([
|
|
3370
|
+
attr("bucket", `${construct.id.toLowerCase()}-archive`),
|
|
3371
|
+
`lifecycle_rule {
|
|
3372
|
+
id = "archive-rule"
|
|
3373
|
+
enabled = true
|
|
3374
|
+
transition {
|
|
3375
|
+
days = 0
|
|
3376
|
+
storage_class = "DEEP_ARCHIVE"
|
|
3377
|
+
}
|
|
3378
|
+
${props.retentionDays ? ` expiration {
|
|
3379
|
+
days = ${props.retentionDays}
|
|
3380
|
+
}
|
|
3381
|
+
` : ""}}`,
|
|
3382
|
+
`object_lock_enabled = ${props.lockEnabled ?? false}`,
|
|
3383
|
+
"",
|
|
3384
|
+
tagsBlock(construct.id)
|
|
3385
|
+
].join("\n"));
|
|
3386
|
+
return block("resource", ["aws_s3_bucket", id], body);
|
|
3387
|
+
}
|
|
3388
|
+
// ── Network ───────────────────────────────────────────────────────────
|
|
3389
|
+
case "Network.VPC": {
|
|
3390
|
+
const body = indent([
|
|
3391
|
+
attr("cidr_block", props.cidr ?? "10.0.0.0/16"),
|
|
3392
|
+
attr("enable_dns_hostnames", true),
|
|
3393
|
+
attr("enable_dns_support", true),
|
|
3394
|
+
"",
|
|
3395
|
+
tagsBlock(construct.id)
|
|
3396
|
+
].join("\n"));
|
|
3397
|
+
return block("resource", ["aws_vpc", id], body);
|
|
3398
|
+
}
|
|
3399
|
+
case "Network.Subnet": {
|
|
3400
|
+
const isPublic = props.public ?? false;
|
|
3401
|
+
const lines = [
|
|
3402
|
+
`vpc_id = "${hclString(props.vpcId)}"`,
|
|
3403
|
+
attr("cidr_block", props.cidr),
|
|
3404
|
+
attr("map_public_ip_on_launch", isPublic),
|
|
3405
|
+
...props.availabilityZone ? [attr("availability_zone", props.availabilityZone)] : [],
|
|
3406
|
+
"",
|
|
3407
|
+
tagsBlock(construct.id)
|
|
3408
|
+
];
|
|
3409
|
+
return block("resource", ["aws_subnet", id], indent(lines.join("\n")));
|
|
3410
|
+
}
|
|
3411
|
+
case "Network.SecurityGroup": {
|
|
3412
|
+
const ingress = props.ingressRules ?? [];
|
|
3413
|
+
const egress = props.egressRules ?? [];
|
|
3414
|
+
const ingressBlocks = ingress.map((r, i) => {
|
|
3415
|
+
if (r.cidr === void 0) {
|
|
3416
|
+
console.warn(`[terraform] Security group rule sem CIDR; usando 0.0.0.0/0 \u2014 defina props.cidr explicitamente (${construct.id} ingress[${i}])`);
|
|
3417
|
+
}
|
|
3418
|
+
return indent([
|
|
3419
|
+
"ingress {",
|
|
3420
|
+
indent([
|
|
3421
|
+
attr("protocol", r.protocol),
|
|
3422
|
+
attr("from_port", r.fromPort),
|
|
3423
|
+
attr("to_port", r.toPort),
|
|
3424
|
+
`cidr_blocks = ["${hclString(r.cidr ?? "0.0.0.0/0")}"]`,
|
|
3425
|
+
r.description ? attr("description", r.description) : ""
|
|
3426
|
+
].filter(Boolean).join("\n")),
|
|
3427
|
+
"}"
|
|
3428
|
+
].join("\n"));
|
|
3429
|
+
}).join("\n");
|
|
3430
|
+
const egressList = egress.length > 0 ? egress : [{ protocol: "-1", fromPort: 0, toPort: 0, cidr: "0.0.0.0/0" }];
|
|
3431
|
+
const egressBlocks = egressList.map((r) => indent([
|
|
3432
|
+
"egress {",
|
|
3433
|
+
indent([
|
|
3434
|
+
attr("protocol", r.protocol),
|
|
3435
|
+
attr("from_port", r.fromPort),
|
|
3436
|
+
attr("to_port", r.toPort),
|
|
3437
|
+
`cidr_blocks = ["${hclString(r.cidr ?? "0.0.0.0/0")}"]`
|
|
3438
|
+
].join("\n")),
|
|
3439
|
+
"}"
|
|
3440
|
+
].join("\n"))).join("\n");
|
|
3441
|
+
const body = indent([
|
|
3442
|
+
`vpc_id = "${hclString(props.vpcId)}"`,
|
|
3443
|
+
attr("description", props.description ?? `Security group ${id}`),
|
|
3444
|
+
"",
|
|
3445
|
+
ingressBlocks,
|
|
3446
|
+
"",
|
|
3447
|
+
egressBlocks,
|
|
3448
|
+
"",
|
|
3449
|
+
tagsBlock(construct.id)
|
|
3450
|
+
].join("\n"));
|
|
3451
|
+
return block("resource", ["aws_security_group", id], body);
|
|
3452
|
+
}
|
|
3453
|
+
case "Network.WAF": {
|
|
3454
|
+
const rules = props.rules ?? [];
|
|
3455
|
+
const scope = props.scope ?? "REGIONAL";
|
|
3456
|
+
const defaultAction = props.defaultAction ?? "allow";
|
|
3457
|
+
const ruleBlocks = rules.map((r, i) => {
|
|
3458
|
+
const action = r.action ?? "allow";
|
|
3459
|
+
const ruleId = (r.name ?? `rule${i}`).replace(/[^a-zA-Z0-9_]/g, "_");
|
|
3460
|
+
const statement = r.managedGroup ? `statement {
|
|
3461
|
+
managed_rule_group_statement {
|
|
3462
|
+
name = "${hclString(r.managedGroup)}"
|
|
3463
|
+
vendor_name = "AWS"
|
|
3464
|
+
}
|
|
3465
|
+
}` : `statement {
|
|
3466
|
+
byte_match_statement {
|
|
3467
|
+
search_string = "${hclString((r.matchValues ?? ["BadBot"])[0])}"
|
|
3468
|
+
field_to_match { single_header { name = "user-agent" } }
|
|
3469
|
+
text_transformation { priority = 0 type = "NONE" }
|
|
3470
|
+
positional_constraint = "CONTAINS"
|
|
3471
|
+
}
|
|
3472
|
+
}`;
|
|
3473
|
+
return indent([
|
|
3474
|
+
"rule {",
|
|
3475
|
+
indent([
|
|
3476
|
+
attr("name", r.name ?? `rule-${i}`),
|
|
3477
|
+
attr("priority", r.priority ?? i + 1),
|
|
3478
|
+
`action {
|
|
3479
|
+
${action === "block" ? "block" : action === "count" ? "count" : "allow"} {}
|
|
3480
|
+
}`,
|
|
3481
|
+
statement,
|
|
3482
|
+
`visibility_config {
|
|
3483
|
+
cloudwatch_metrics_enabled = true
|
|
3484
|
+
metric_name = "${ruleId}"
|
|
3485
|
+
sampled_requests_enabled = true
|
|
3486
|
+
}`
|
|
3487
|
+
].join("\n")),
|
|
3488
|
+
"}"
|
|
3489
|
+
].join("\n"));
|
|
3490
|
+
}).join("\n");
|
|
3491
|
+
const body = indent([
|
|
3492
|
+
attr("name", id),
|
|
3493
|
+
attr("scope", scope),
|
|
3494
|
+
`default_action {
|
|
3495
|
+
${defaultAction === "block" ? "block" : "allow"} {}
|
|
3496
|
+
}`,
|
|
3497
|
+
ruleBlocks,
|
|
3498
|
+
`visibility_config {
|
|
3499
|
+
cloudwatch_metrics_enabled = true
|
|
3500
|
+
metric_name = "${id}"
|
|
3501
|
+
sampled_requests_enabled = true
|
|
3502
|
+
}`,
|
|
3503
|
+
"",
|
|
3504
|
+
tagsBlock(construct.id)
|
|
3505
|
+
].join("\n"));
|
|
3506
|
+
return block("resource", ["aws_wafv2_web_acl", id], body);
|
|
3507
|
+
}
|
|
3508
|
+
case "Network.LoadBalancer": {
|
|
3509
|
+
const lbType = props.type ?? "application";
|
|
3510
|
+
const listeners = props.listeners ?? [];
|
|
3511
|
+
const targetGroups = props.targetGroups ?? [];
|
|
3512
|
+
const lbBody = indent([
|
|
3513
|
+
attr("name", construct.id),
|
|
3514
|
+
attr("load_balancer_type", lbType),
|
|
3515
|
+
attr("internal", props.scheme === "internal"),
|
|
3516
|
+
`subnets = ${JSON.stringify(props.subnetIds ?? [])}`,
|
|
3517
|
+
...lbType === "application" && props.securityGroupIds ? [`security_groups = ${JSON.stringify(props.securityGroupIds)}`] : [],
|
|
3518
|
+
"",
|
|
3519
|
+
tagsBlock(construct.id)
|
|
3520
|
+
].join("\n"));
|
|
3521
|
+
const parts = [block("resource", ["aws_lb", id], lbBody)];
|
|
3522
|
+
for (const tg of targetGroups) {
|
|
3523
|
+
const tgId = `${id}_tg_${tg.name.replace(/[^a-zA-Z0-9_]/g, "_")}`;
|
|
3524
|
+
const tgBody = indent([
|
|
3525
|
+
attr("name", tg.name),
|
|
3526
|
+
attr("port", tg.port),
|
|
3527
|
+
attr("protocol", tg.protocol),
|
|
3528
|
+
`vpc_id = ""`,
|
|
3529
|
+
`target_type = "ip"`,
|
|
3530
|
+
`health_check {
|
|
3531
|
+
path = "${hclString(tg.healthCheckPath ?? "/")}"
|
|
3532
|
+
}`
|
|
3533
|
+
].join("\n"));
|
|
3534
|
+
parts.push(block("resource", ["aws_lb_target_group", tgId], tgBody));
|
|
3535
|
+
}
|
|
3536
|
+
for (let i = 0; i < listeners.length; i++) {
|
|
3537
|
+
const l = listeners[i];
|
|
3538
|
+
const listenerId = `${id}_listener_${i + 1}`;
|
|
3539
|
+
const redirectAction = `redirect {
|
|
3540
|
+
port = "443"
|
|
3541
|
+
protocol = "HTTPS"
|
|
3542
|
+
status_code = "HTTP_301"
|
|
3543
|
+
}`;
|
|
3544
|
+
const fixedAction = `fixed_response {
|
|
3545
|
+
content_type = "text/plain"
|
|
3546
|
+
message_body = "Not found"
|
|
3547
|
+
status_code = "404"
|
|
3548
|
+
}`;
|
|
3549
|
+
const listenerBody = indent([
|
|
3550
|
+
`load_balancer_arn = aws_lb.${id}.arn`,
|
|
3551
|
+
attr("port", l.port),
|
|
3552
|
+
attr("protocol", l.protocol),
|
|
3553
|
+
...l.certificateArn ? [attr("certificate_arn", l.certificateArn)] : [],
|
|
3554
|
+
`default_action {
|
|
3555
|
+
type = "${l.redirectToHttps ? "redirect" : "fixed-response"}"
|
|
3556
|
+
${l.redirectToHttps ? redirectAction : fixedAction}
|
|
3557
|
+
}`
|
|
3558
|
+
].join("\n"));
|
|
3559
|
+
parts.push(block("resource", ["aws_lb_listener", listenerId], listenerBody));
|
|
3560
|
+
}
|
|
3561
|
+
return parts.join("\n");
|
|
3562
|
+
}
|
|
3563
|
+
case "Network.CDN": {
|
|
3564
|
+
const origins = props.origins ?? [];
|
|
3565
|
+
const body = indent([
|
|
3566
|
+
`origin {
|
|
3567
|
+
domain_name = "${hclString(origins[0]?.domainName ?? "")}"
|
|
3568
|
+
origin_id = "${hclString(origins[0]?.id ?? "default")}"
|
|
3569
|
+
}`,
|
|
3570
|
+
`enabled = true`,
|
|
3571
|
+
attr("default_root_object", props.defaultRootObject ?? "index.html"),
|
|
3572
|
+
attr("price_class", props.priceClass ?? "PriceClass_100"),
|
|
3573
|
+
`default_cache_behavior {
|
|
3574
|
+
target_origin_id = "${hclString(origins[0]?.id ?? "default")}"
|
|
3575
|
+
viewer_protocol_policy = "redirect-to-https"
|
|
3576
|
+
allowed_methods = ["GET", "HEAD"]
|
|
3577
|
+
cached_methods = ["GET", "HEAD"]
|
|
3578
|
+
compress = true
|
|
3579
|
+
forwarded_values {
|
|
3580
|
+
query_string = false
|
|
3581
|
+
cookies { forward = "none" }
|
|
3582
|
+
}
|
|
3583
|
+
}`,
|
|
3584
|
+
...props.certificateArn ? [`viewer_certificate {
|
|
3585
|
+
acm_certificate_arn = "${hclString(props.certificateArn)}"
|
|
3586
|
+
ssl_support_method = "sni-only"
|
|
3587
|
+
}`] : [`viewer_certificate {
|
|
3588
|
+
cloudfront_default_certificate = true
|
|
3589
|
+
}`],
|
|
3590
|
+
`restrictions {
|
|
3591
|
+
geo_restriction {
|
|
3592
|
+
restriction_type = "none"
|
|
3593
|
+
}
|
|
3594
|
+
}`,
|
|
3595
|
+
"",
|
|
3596
|
+
tagsBlock(construct.id)
|
|
3597
|
+
].join("\n"));
|
|
3598
|
+
return block("resource", ["aws_cloudfront_distribution", id], body);
|
|
3599
|
+
}
|
|
3600
|
+
case "Network.Dns": {
|
|
3601
|
+
const records = props.records ?? [];
|
|
3602
|
+
const zoneBody = indent([
|
|
3603
|
+
attr("name", props.zoneName),
|
|
3604
|
+
tagsBlock(construct.id)
|
|
3605
|
+
].join("\n"));
|
|
3606
|
+
const parts = [block("resource", ["aws_route53_zone", id], zoneBody)];
|
|
3607
|
+
for (const r of records) {
|
|
3608
|
+
const recId = `${id}_${r.name.replace(/[^a-zA-Z0-9_]/g, "_")}_${r.type}`;
|
|
3609
|
+
const recBody = indent([
|
|
3610
|
+
`zone_id = aws_route53_zone.${id}.zone_id`,
|
|
3611
|
+
attr("name", r.name),
|
|
3612
|
+
attr("type", r.type),
|
|
3613
|
+
attr("ttl", r.ttl ?? 300),
|
|
3614
|
+
`records = ${JSON.stringify(r.values)}`
|
|
3615
|
+
].join("\n"));
|
|
3616
|
+
parts.push(block("resource", ["aws_route53_record", recId], recBody));
|
|
3617
|
+
}
|
|
3618
|
+
return parts.join("\n");
|
|
3619
|
+
}
|
|
3620
|
+
// ── Database ──────────────────────────────────────────────────────────
|
|
3621
|
+
case "Database.SQL": {
|
|
3622
|
+
const engine = props.engine ?? "mysql";
|
|
3623
|
+
const edition = props.edition ?? "";
|
|
3624
|
+
const licenseModel = props.licenseModel ?? "";
|
|
3625
|
+
const engineVersionMap = {
|
|
3626
|
+
mysql: "8.0.36",
|
|
3627
|
+
postgres: "15.4",
|
|
3628
|
+
mariadb: "10.11.6",
|
|
3629
|
+
oracle: "19.0.0.0.ru-2024-01.rur-2024-01.r1",
|
|
3630
|
+
sqlserver: "15.00.4365.2.v1"
|
|
3631
|
+
};
|
|
3632
|
+
const engineIdentMap = {
|
|
3633
|
+
mysql: "mysql",
|
|
3634
|
+
postgres: "postgres",
|
|
3635
|
+
mariadb: "mariadb",
|
|
3636
|
+
oracle: `oracle-${edition || "se2"}`,
|
|
3637
|
+
sqlserver: `sqlserver-${edition || "ex"}`
|
|
3638
|
+
};
|
|
3639
|
+
const defaultInstanceMap = {
|
|
3640
|
+
mysql: "db.t3.micro",
|
|
3641
|
+
postgres: "db.t3.micro",
|
|
3642
|
+
mariadb: "db.t3.micro",
|
|
3643
|
+
oracle: "db.t3.small",
|
|
3644
|
+
sqlserver: "db.t3.small"
|
|
3645
|
+
};
|
|
3646
|
+
const usernameMap = {
|
|
3647
|
+
mysql: "dbadmin",
|
|
3648
|
+
postgres: "dbadmin",
|
|
3649
|
+
mariadb: "dbadmin",
|
|
3650
|
+
oracle: "dbadmin",
|
|
3651
|
+
sqlserver: "sqladmin"
|
|
3652
|
+
};
|
|
3653
|
+
const attrs = [
|
|
3654
|
+
attr("identifier", construct.id.toLowerCase()),
|
|
3655
|
+
attr("engine", engineIdentMap[engine] ?? engine),
|
|
3656
|
+
attr("engine_version", engineVersionMap[engine] ?? "8.0.36"),
|
|
3657
|
+
attr("instance_class", props.instanceType ?? defaultInstanceMap[engine] ?? "db.t3.micro"),
|
|
3658
|
+
attr("allocated_storage", props.storageGb ?? 20),
|
|
3659
|
+
attr("username", usernameMap[engine] ?? "dbadmin"),
|
|
3660
|
+
`password = var.db_password`,
|
|
3661
|
+
attr("multi_az", props.multiAz ?? false),
|
|
3662
|
+
attr("skip_final_snapshot", false),
|
|
3663
|
+
attr("storage_encrypted", true),
|
|
3664
|
+
attr("backup_retention_period", props.backupRetentionDays ?? 7),
|
|
3665
|
+
attr("deletion_protection", props.deletionProtection ?? false)
|
|
3666
|
+
];
|
|
3667
|
+
if (licenseModel)
|
|
3668
|
+
attrs.push(attr("license_model", licenseModel));
|
|
3669
|
+
attrs.push("", tagsBlock(construct.id));
|
|
3670
|
+
return block("resource", ["aws_db_instance", id], indent(attrs.join("\n")));
|
|
3671
|
+
}
|
|
3672
|
+
case "Database.DocumentDB": {
|
|
3673
|
+
const instances = props.instances ?? 1;
|
|
3674
|
+
const clusterBody = indent([
|
|
3675
|
+
attr("cluster_identifier", construct.id.toLowerCase()),
|
|
3676
|
+
attr("engine", "docdb"),
|
|
3677
|
+
attr("master_username", "docdbadmin"),
|
|
3678
|
+
`master_password = var.db_password`,
|
|
3679
|
+
attr("backup_retention_period", 7),
|
|
3680
|
+
attr("skip_final_snapshot", false),
|
|
3681
|
+
attr("storage_encrypted", true),
|
|
3682
|
+
attr("deletion_protection", props.deletionProtection ?? false),
|
|
3683
|
+
"",
|
|
3684
|
+
tagsBlock(construct.id)
|
|
3685
|
+
].join("\n"));
|
|
3686
|
+
const instanceBlocks = Array.from({ length: instances }, (_, i) => block("resource", ["aws_docdb_cluster_instance", `${id}_instance_${i + 1}`], indent([
|
|
3687
|
+
attr("identifier", `${construct.id.toLowerCase()}-${i + 1}`),
|
|
3688
|
+
`cluster_id = aws_docdb_cluster.${id}.id`,
|
|
3689
|
+
attr("instance_class", props.instanceType ?? "db.t3.medium")
|
|
3690
|
+
].join("\n")))).join("\n");
|
|
3691
|
+
return [block("resource", ["aws_docdb_cluster", id], clusterBody), instanceBlocks].join("\n");
|
|
3692
|
+
}
|
|
3693
|
+
case "Database.DynamoDB": {
|
|
3694
|
+
const billingMode = props.billingMode ?? "PAY_PER_REQUEST";
|
|
3695
|
+
const gsis = props.globalSecondaryIndexes ?? [];
|
|
3696
|
+
const gsiBlocks = gsis.map((g) => indent([
|
|
3697
|
+
"global_secondary_index {",
|
|
3698
|
+
indent([
|
|
3699
|
+
attr("name", g.name),
|
|
3700
|
+
attr("hash_key", g.partitionKey),
|
|
3701
|
+
...g.sortKey ? [attr("range_key", g.sortKey)] : [],
|
|
3702
|
+
attr("projection_type", "ALL")
|
|
3703
|
+
].join("\n")),
|
|
3704
|
+
"}"
|
|
3705
|
+
].join("\n"))).join("\n");
|
|
3706
|
+
const body = indent([
|
|
3707
|
+
attr("name", construct.id),
|
|
3708
|
+
attr("billing_mode", billingMode),
|
|
3709
|
+
...billingMode === "PROVISIONED" ? [
|
|
3710
|
+
attr("read_capacity", props.readCapacity ?? 5),
|
|
3711
|
+
attr("write_capacity", props.writeCapacity ?? 5)
|
|
3712
|
+
] : [],
|
|
3713
|
+
attr("hash_key", props.partitionKey),
|
|
3714
|
+
...props.sortKey ? [attr("range_key", props.sortKey)] : [],
|
|
3715
|
+
`attribute {
|
|
3716
|
+
name = "${hclString(props.partitionKey)}"
|
|
3717
|
+
type = "S"
|
|
3718
|
+
}`,
|
|
3719
|
+
...props.sortKey ? [`attribute {
|
|
3720
|
+
name = "${hclString(props.sortKey)}"
|
|
3721
|
+
type = "S"
|
|
3722
|
+
}`] : [],
|
|
3723
|
+
...gsis.map((g) => `attribute {
|
|
3724
|
+
name = "${hclString(g.partitionKey)}"
|
|
3725
|
+
type = "S"
|
|
3726
|
+
}`),
|
|
3727
|
+
gsiBlocks,
|
|
3728
|
+
...props.ttlAttribute ? [`ttl {
|
|
3729
|
+
attribute_name = "${hclString(props.ttlAttribute)}"
|
|
3730
|
+
enabled = true
|
|
3731
|
+
}`] : [],
|
|
3732
|
+
`point_in_time_recovery {
|
|
3733
|
+
enabled = ${props.pointInTimeRecovery ?? true}
|
|
3734
|
+
}`,
|
|
3735
|
+
...props.streamEnabled ? [`stream_enabled = true`, `stream_view_type = "NEW_AND_OLD_IMAGES"`] : [],
|
|
3736
|
+
"",
|
|
3737
|
+
tagsBlock(construct.id)
|
|
3738
|
+
].join("\n"));
|
|
3739
|
+
return block("resource", ["aws_dynamodb_table", id], body);
|
|
3740
|
+
}
|
|
3741
|
+
// ── Cache ─────────────────────────────────────────────────────────────
|
|
3742
|
+
case "Cache.Redis": {
|
|
3743
|
+
const numNodes = props.numCacheNodes ?? 1;
|
|
3744
|
+
const autoFailover = props.automaticFailoverEnabled ?? false;
|
|
3745
|
+
const body = indent([
|
|
3746
|
+
attr("replication_group_id", construct.id.toLowerCase().replace(/[^a-z0-9-]/g, "-").slice(0, 40)),
|
|
3747
|
+
attr("description", `Redis ${construct.id}`),
|
|
3748
|
+
attr("node_type", CACHE_NODE_TYPE_MAP[props.nodeType ?? "small"] ?? "cache.t3.micro"),
|
|
3749
|
+
attr("num_cache_clusters", numNodes),
|
|
3750
|
+
attr("automatic_failover_enabled", autoFailover && numNodes > 1),
|
|
3751
|
+
attr("at_rest_encryption_enabled", props.atRestEncryptionEnabled ?? true),
|
|
3752
|
+
attr("transit_encryption_enabled", props.transitEncryptionEnabled ?? true),
|
|
3753
|
+
attr("engine_version", props.version ?? "7.0"),
|
|
3754
|
+
...props.subnetGroupName ? [attr("subnet_group_name", props.subnetGroupName)] : [],
|
|
3755
|
+
...props.securityGroupIds ? [`security_group_ids = ${JSON.stringify(props.securityGroupIds)}`] : [],
|
|
3756
|
+
"",
|
|
3757
|
+
tagsBlock(construct.id)
|
|
3758
|
+
].join("\n"));
|
|
3759
|
+
return block("resource", ["aws_elasticache_replication_group", id], body);
|
|
3760
|
+
}
|
|
3761
|
+
case "Cache.Memcached": {
|
|
3762
|
+
const body = indent([
|
|
3763
|
+
attr("cluster_id", construct.id.toLowerCase().replace(/[^a-z0-9-]/g, "-").slice(0, 40)),
|
|
3764
|
+
attr("engine", "memcached"),
|
|
3765
|
+
attr("node_type", CACHE_NODE_TYPE_MAP[props.nodeType ?? "small"] ?? "cache.t3.micro"),
|
|
3766
|
+
attr("num_cache_nodes", props.numCacheNodes ?? 2),
|
|
3767
|
+
...props.subnetGroupName ? [attr("subnet_group_name", props.subnetGroupName)] : [],
|
|
3768
|
+
"",
|
|
3769
|
+
tagsBlock(construct.id)
|
|
3770
|
+
].join("\n"));
|
|
3771
|
+
return block("resource", ["aws_elasticache_cluster", id], body);
|
|
3772
|
+
}
|
|
3773
|
+
// ── Function ──────────────────────────────────────────────────────────
|
|
3774
|
+
case "Function.Lambda": {
|
|
3775
|
+
const environment = props.environment;
|
|
3776
|
+
const runtimeMap = {
|
|
3777
|
+
"nodejs20": "nodejs20.x",
|
|
3778
|
+
"nodejs18": "nodejs18.x",
|
|
3779
|
+
"python3.12": "python3.12",
|
|
3780
|
+
"python3.11": "python3.11",
|
|
3781
|
+
"java21": "java21",
|
|
3782
|
+
"go1.x": "go1.x",
|
|
3783
|
+
"dotnet8": "dotnet8"
|
|
3784
|
+
};
|
|
3785
|
+
const envBlock = environment && Object.keys(environment).length > 0 ? "\n" + indent([
|
|
3786
|
+
"environment {",
|
|
3787
|
+
indent(`variables = {
|
|
3788
|
+
${Object.entries(environment).map(([k, v]) => ` ${k} = "${hclString(v)}"`).join("\n")}
|
|
3789
|
+
}`),
|
|
3790
|
+
"}"
|
|
3791
|
+
].join("\n")) : "";
|
|
3792
|
+
const body = indent([
|
|
3793
|
+
attr("function_name", construct.id),
|
|
3794
|
+
attr("runtime", runtimeMap[props.runtime ?? "nodejs20"] ?? "nodejs20.x"),
|
|
3795
|
+
attr("handler", props.handler ?? "index.handler"),
|
|
3796
|
+
attr("role", "arn:aws:iam::ACCOUNT_ID:role/lambda-role"),
|
|
3797
|
+
`filename = "function.zip"`,
|
|
3798
|
+
`source_code_hash = filebase64sha256("function.zip")`,
|
|
3799
|
+
...props.memory ? [attr("memory_size", props.memory)] : [],
|
|
3800
|
+
...props.timeout ? [attr("timeout", props.timeout)] : [],
|
|
3801
|
+
...props.reservedConcurrency !== void 0 ? [attr("reserved_concurrent_executions", props.reservedConcurrency)] : [],
|
|
3802
|
+
envBlock,
|
|
3803
|
+
...props.vpcId ? [`vpc_config {
|
|
3804
|
+
subnet_ids = ${JSON.stringify(props.subnetIds ?? [])}
|
|
3805
|
+
security_group_ids = ${JSON.stringify(props.securityGroupIds ?? [])}
|
|
3806
|
+
}`] : [],
|
|
3807
|
+
"",
|
|
3808
|
+
tagsBlock(construct.id)
|
|
3809
|
+
].filter((l) => l !== "").join("\n"));
|
|
3810
|
+
return block("resource", ["aws_lambda_function", id], body);
|
|
3811
|
+
}
|
|
3812
|
+
case "Function.ApiGateway": {
|
|
3813
|
+
const apigwType = props.type ?? "HTTP";
|
|
3814
|
+
const routes = props.routes ?? [];
|
|
3815
|
+
const stageName = props.stageName ?? "$default";
|
|
3816
|
+
const apiBody = indent([
|
|
3817
|
+
attr("name", props.name),
|
|
3818
|
+
attr("protocol_type", apigwType),
|
|
3819
|
+
...props.cors ? [`cors_configuration {
|
|
3820
|
+
allow_origins = ["*"]
|
|
3821
|
+
allow_methods = ["*"]
|
|
3822
|
+
allow_headers = ["*"]
|
|
3823
|
+
}`] : [],
|
|
3824
|
+
"",
|
|
3825
|
+
tagsBlock(construct.id)
|
|
3826
|
+
].join("\n"));
|
|
3827
|
+
const stageBody = indent([
|
|
3828
|
+
`api_id = aws_apigatewayv2_api.${id}.id`,
|
|
3829
|
+
attr("name", stageName),
|
|
3830
|
+
attr("auto_deploy", true)
|
|
3831
|
+
].join("\n"));
|
|
3832
|
+
const parts = [
|
|
3833
|
+
block("resource", ["aws_apigatewayv2_api", id], apiBody),
|
|
3834
|
+
block("resource", ["aws_apigatewayv2_stage", `${id}_stage`], stageBody)
|
|
3835
|
+
];
|
|
3836
|
+
const authorizerLambdaId = props.authorizerLambdaId;
|
|
3837
|
+
if (authorizerLambdaId) {
|
|
3838
|
+
const authorizerBody = indent([
|
|
3839
|
+
`api_id = aws_apigatewayv2_api.${id}.id`,
|
|
3840
|
+
attr("authorizer_type", "REQUEST"),
|
|
3841
|
+
attr("name", `${props.name}-authorizer`),
|
|
3842
|
+
`authorizer_uri = aws_lambda_function.${authorizerLambdaId}.invoke_arn`,
|
|
3843
|
+
attr("authorizer_payload_format_version", "2.0"),
|
|
3844
|
+
`identity_sources = ["$request.header.Authorization"]`
|
|
3845
|
+
].join("\n"));
|
|
3846
|
+
parts.push(block("resource", ["aws_apigatewayv2_authorizer", `${id}_authorizer`], authorizerBody));
|
|
3847
|
+
}
|
|
3848
|
+
for (const r of routes) {
|
|
3849
|
+
const routeId = `${id}_${r.method.toLowerCase()}_${r.path.replace(/[^a-zA-Z0-9]/g, "_")}`;
|
|
3850
|
+
const routeBody = indent([
|
|
3851
|
+
`api_id = aws_apigatewayv2_api.${id}.id`,
|
|
3852
|
+
attr("route_key", `${r.method} ${r.path}`),
|
|
3853
|
+
...r.lambdaId ? [`target = "integrations/\${aws_apigatewayv2_integration.${routeId}_integ.id}"`] : [],
|
|
3854
|
+
...authorizerLambdaId ? [
|
|
3855
|
+
attr("authorization_type", "CUSTOM"),
|
|
3856
|
+
`authorizer_id = aws_apigatewayv2_authorizer.${id}_authorizer.id`
|
|
3857
|
+
] : []
|
|
3858
|
+
].join("\n"));
|
|
3859
|
+
parts.push(block("resource", ["aws_apigatewayv2_route", routeId], routeBody));
|
|
3860
|
+
if (r.lambdaId) {
|
|
3861
|
+
const integBody = indent([
|
|
3862
|
+
`api_id = aws_apigatewayv2_api.${id}.id`,
|
|
3863
|
+
attr("integration_type", "AWS_PROXY"),
|
|
3864
|
+
`integration_uri = aws_lambda_function.${r.lambdaId}.invoke_arn`,
|
|
3865
|
+
attr("payload_format_version", "2.0")
|
|
3866
|
+
].join("\n"));
|
|
3867
|
+
parts.push(block("resource", ["aws_apigatewayv2_integration", `${routeId}_integ`], integBody));
|
|
3868
|
+
}
|
|
3869
|
+
}
|
|
3870
|
+
return parts.join("\n");
|
|
3871
|
+
}
|
|
3872
|
+
// ── Policy ────────────────────────────────────────────────────────────
|
|
3873
|
+
case "Policy.IAM": {
|
|
3874
|
+
const statements = props.statements ?? [];
|
|
3875
|
+
const attachType = props.attachType;
|
|
3876
|
+
const attachTo = props.attachTo.replace(/[^a-zA-Z0-9_]/g, "_");
|
|
3877
|
+
const principalService = attachType === "lambda" ? "lambda.amazonaws.com" : "ec2.amazonaws.com";
|
|
3878
|
+
const policyStmts = statements.map((s) => {
|
|
3879
|
+
const actions = s.actions.map((a) => `"${hclString(a)}"`).join(", ");
|
|
3880
|
+
const resources = (s.resources ?? ["*"]).map((r) => `"${hclString(r)}"`).join(", ");
|
|
3881
|
+
return ` {
|
|
3882
|
+
"Effect": "${hclString(s.effect)}",
|
|
3883
|
+
"Action": [${actions}],
|
|
3884
|
+
"Resource": [${resources}]
|
|
3885
|
+
}`;
|
|
3886
|
+
}).join(",\n");
|
|
3887
|
+
const roleBody = indent([
|
|
3888
|
+
attr("name", `${attachTo}-role`),
|
|
3889
|
+
`assume_role_policy = jsonencode({
|
|
3890
|
+
Version = "2012-10-17"
|
|
3891
|
+
Statement = [{
|
|
3892
|
+
Effect = "Allow"
|
|
3893
|
+
Principal = { Service = "${principalService}" }
|
|
3894
|
+
Action = "sts:AssumeRole"
|
|
3895
|
+
}]
|
|
3896
|
+
})`,
|
|
3897
|
+
"",
|
|
3898
|
+
tagsBlock(`${attachTo}-role`)
|
|
3899
|
+
].join("\n"));
|
|
3900
|
+
const policyBody = indent([
|
|
3901
|
+
attr("name", `${id}-policy`),
|
|
3902
|
+
`policy = jsonencode({
|
|
3903
|
+
Version = "2012-10-17"
|
|
3904
|
+
Statement = [
|
|
3905
|
+
${policyStmts}
|
|
3906
|
+
]
|
|
3907
|
+
})`
|
|
3908
|
+
].join("\n"));
|
|
3909
|
+
const attachBody = indent([
|
|
3910
|
+
`role = aws_iam_role.${id}_role.name`,
|
|
3911
|
+
`policy_arn = aws_iam_policy.${id}_policy.arn`
|
|
3912
|
+
].join("\n"));
|
|
3913
|
+
const parts = [
|
|
3914
|
+
block("resource", ["aws_iam_role", `${id}_role`], roleBody),
|
|
3915
|
+
block("resource", ["aws_iam_policy", `${id}_policy`], policyBody),
|
|
3916
|
+
block("resource", ["aws_iam_role_policy_attachment", `${id}_attach`], attachBody)
|
|
3917
|
+
];
|
|
3918
|
+
if (attachType === "compute") {
|
|
3919
|
+
parts.push(block("resource", ["aws_iam_instance_profile", `${id}_profile`], indent([
|
|
3920
|
+
attr("name", `${attachTo}-profile`),
|
|
3921
|
+
`role = aws_iam_role.${id}_role.name`
|
|
3922
|
+
].join("\n"))));
|
|
3923
|
+
}
|
|
3924
|
+
return parts.join("\n");
|
|
3925
|
+
}
|
|
3926
|
+
// ── Events ────────────────────────────────────────────────────────────
|
|
3927
|
+
case "Events.EventBridge": {
|
|
3928
|
+
const rules = props.rules ?? [];
|
|
3929
|
+
const busName = props.busName ?? "default";
|
|
3930
|
+
const parts = [];
|
|
3931
|
+
if (busName !== "default") {
|
|
3932
|
+
parts.push(block("resource", ["aws_cloudwatch_event_bus", id], indent(attr("name", busName))));
|
|
3933
|
+
}
|
|
3934
|
+
for (const r of rules) {
|
|
3935
|
+
const ruleId = (r.name ?? "rule").replace(/[^a-zA-Z0-9_]/g, "_");
|
|
3936
|
+
const pattern = {};
|
|
3937
|
+
if (r.source)
|
|
3938
|
+
pattern["source"] = r.source;
|
|
3939
|
+
if (r.detailTypes)
|
|
3940
|
+
pattern["detail-type"] = r.detailTypes;
|
|
3941
|
+
const ruleBody = indent([
|
|
3942
|
+
attr("name", r.name),
|
|
3943
|
+
`event_bus_name = "${hclString(busName)}"`,
|
|
3944
|
+
`event_pattern = jsonencode(${JSON.stringify(pattern)})`,
|
|
3945
|
+
attr("state", "ENABLED")
|
|
3946
|
+
].join("\n"));
|
|
3947
|
+
parts.push(block("resource", ["aws_cloudwatch_event_rule", `${id}_${ruleId}`], ruleBody));
|
|
3948
|
+
if (r.targetArn) {
|
|
3949
|
+
const targetBody = indent([
|
|
3950
|
+
`rule = aws_cloudwatch_event_rule.${id}_${ruleId}.name`,
|
|
3951
|
+
attr("target_id", `${ruleId}Target`),
|
|
3952
|
+
attr("arn", r.targetArn)
|
|
3953
|
+
].join("\n"));
|
|
3954
|
+
parts.push(block("resource", ["aws_cloudwatch_event_target", `${id}_${ruleId}_target`], targetBody));
|
|
3955
|
+
}
|
|
3956
|
+
}
|
|
3957
|
+
return parts.join("\n");
|
|
3958
|
+
}
|
|
3959
|
+
// ── Workflow ──────────────────────────────────────────────────────────
|
|
3960
|
+
case "Workflow.StepFunctions": {
|
|
3961
|
+
const steps = props.steps ?? [];
|
|
3962
|
+
const definition = {
|
|
3963
|
+
Comment: props.description ?? `Workflow ${construct.id}`,
|
|
3964
|
+
StartAt: steps[0]?.name ?? "Start",
|
|
3965
|
+
States: Object.fromEntries(steps.map((s, i) => [s.name, {
|
|
3966
|
+
Type: s.type ?? "Task",
|
|
3967
|
+
Resource: s.resource ?? "",
|
|
3968
|
+
...i < steps.length - 1 ? { Next: steps[i + 1].name } : { End: true }
|
|
3969
|
+
}]))
|
|
3970
|
+
};
|
|
3971
|
+
const body = indent([
|
|
3972
|
+
attr("name", construct.id),
|
|
3973
|
+
attr("type", props.type ?? "STANDARD"),
|
|
3974
|
+
`definition = jsonencode(${JSON.stringify(definition)})`,
|
|
3975
|
+
attr("role_arn", "arn:aws:iam::ACCOUNT_ID:role/StepFunctionsExecutionRole"),
|
|
3976
|
+
"",
|
|
3977
|
+
tagsBlock(construct.id)
|
|
3978
|
+
].join("\n"));
|
|
3979
|
+
return block("resource", ["aws_sfn_state_machine", id], body);
|
|
3980
|
+
}
|
|
3981
|
+
// ── Messaging ─────────────────────────────────────────────────────────
|
|
3982
|
+
case "Messaging.Queue": {
|
|
3983
|
+
const fifo = props.fifo ?? false;
|
|
3984
|
+
const body = indent([
|
|
3985
|
+
attr("name", fifo ? `${construct.id}.fifo` : construct.id),
|
|
3986
|
+
attr("visibility_timeout_seconds", props.visibilityTimeoutSeconds ?? 30),
|
|
3987
|
+
attr("message_retention_seconds", props.messageRetentionSeconds ?? 345600),
|
|
3988
|
+
attr("delay_seconds", props.delaySeconds ?? 0),
|
|
3989
|
+
attr("fifo_queue", fifo),
|
|
3990
|
+
attr("sqs_managed_sse_enabled", props.encrypted ?? true),
|
|
3991
|
+
...props.dlqArn ? [`redrive_policy = jsonencode({
|
|
3992
|
+
deadLetterTargetArn = "${hclString(props.dlqArn)}"
|
|
3993
|
+
maxReceiveCount = ${props.maxReceiveCount ?? 3}
|
|
3994
|
+
})`] : [],
|
|
3995
|
+
"",
|
|
3996
|
+
tagsBlock(construct.id)
|
|
3997
|
+
].join("\n"));
|
|
3998
|
+
return block("resource", ["aws_sqs_queue", id], body);
|
|
3999
|
+
}
|
|
4000
|
+
case "Messaging.Topic": {
|
|
4001
|
+
const fifo = props.fifo ?? false;
|
|
4002
|
+
const subscriptions = props.subscriptions ?? [];
|
|
4003
|
+
const topicBody = indent([
|
|
4004
|
+
attr("name", fifo ? `${construct.id}.fifo` : construct.id),
|
|
4005
|
+
attr("display_name", props.displayName ?? construct.id),
|
|
4006
|
+
attr("fifo_topic", fifo),
|
|
4007
|
+
...props.encrypted ? [attr("kms_master_key_id", "alias/aws/sns")] : [],
|
|
4008
|
+
"",
|
|
4009
|
+
tagsBlock(construct.id)
|
|
4010
|
+
].join("\n"));
|
|
4011
|
+
const subBlocks = subscriptions.map((s, i) => block("resource", ["aws_sns_topic_subscription", `${id}_sub_${i}`], indent([
|
|
4012
|
+
`topic_arn = aws_sns_topic.${id}.arn`,
|
|
4013
|
+
attr("protocol", s.protocol),
|
|
4014
|
+
attr("endpoint", s.endpoint)
|
|
4015
|
+
].join("\n")))).join("\n");
|
|
4016
|
+
return [block("resource", ["aws_sns_topic", id], topicBody), subBlocks].filter(Boolean).join("\n");
|
|
4017
|
+
}
|
|
4018
|
+
// ── Secret / Certificate ──────────────────────────────────────────────
|
|
4019
|
+
case "Secret.Vault": {
|
|
4020
|
+
const body = indent([
|
|
4021
|
+
attr("name", construct.id),
|
|
4022
|
+
attr("description", props.description ?? `Secret ${construct.id}`),
|
|
4023
|
+
...props.kmsKeyId ? [attr("kms_key_id", props.kmsKeyId)] : [],
|
|
4024
|
+
...props.rotationDays ? [`rotation_rules {
|
|
4025
|
+
automatically_after_days = ${props.rotationDays}
|
|
4026
|
+
}`] : [],
|
|
4027
|
+
"",
|
|
4028
|
+
tagsBlock(construct.id)
|
|
4029
|
+
].join("\n"));
|
|
4030
|
+
return block("resource", ["aws_secretsmanager_secret", id], body);
|
|
4031
|
+
}
|
|
4032
|
+
case "Certificate.TLS": {
|
|
4033
|
+
const sans = props.subjectAlternativeNames ?? [];
|
|
4034
|
+
const body = indent([
|
|
4035
|
+
attr("domain_name", props.domainName),
|
|
4036
|
+
attr("validation_method", props.validationMethod ?? "DNS"),
|
|
4037
|
+
...sans.length > 0 ? [`subject_alternative_names = ${JSON.stringify(sans)}`] : [],
|
|
4038
|
+
`lifecycle {
|
|
4039
|
+
create_before_destroy = true
|
|
4040
|
+
}`,
|
|
4041
|
+
"",
|
|
4042
|
+
tagsBlock(construct.id)
|
|
4043
|
+
].join("\n"));
|
|
4044
|
+
return block("resource", ["aws_acm_certificate", id], body);
|
|
4045
|
+
}
|
|
4046
|
+
// ── Monitoring ────────────────────────────────────────────────────────
|
|
4047
|
+
case "Monitoring.Alarm": {
|
|
4048
|
+
const dimensions = props.dimensions;
|
|
4049
|
+
const dimBlock = dimensions ? `dimensions = {
|
|
4050
|
+
${Object.entries(dimensions).map(([k, v]) => ` ${k} = "${hclString(v)}"`).join("\n")}
|
|
4051
|
+
}` : "";
|
|
4052
|
+
const body = indent([
|
|
4053
|
+
attr("alarm_name", construct.id),
|
|
4054
|
+
attr("metric_name", props.metricName),
|
|
4055
|
+
attr("namespace", props.namespace ?? "AWS/Lambda"),
|
|
4056
|
+
attr("threshold", props.threshold),
|
|
4057
|
+
attr("evaluation_periods", props.evaluationPeriods ?? 2),
|
|
4058
|
+
attr("period", props.periodSeconds ?? 60),
|
|
4059
|
+
attr("comparison_operator", props.comparisonOperator ?? "GreaterThanThreshold"),
|
|
4060
|
+
attr("statistic", props.statistic ?? "Average"),
|
|
4061
|
+
attr("treat_missing_data", props.treatMissingData ?? "notBreaching"),
|
|
4062
|
+
...props.alarmActions ? [`alarm_actions = ${JSON.stringify(props.alarmActions)}`] : [],
|
|
4063
|
+
...props.okActions ? [`ok_actions = ${JSON.stringify(props.okActions)}`] : [],
|
|
4064
|
+
dimBlock
|
|
4065
|
+
].filter(Boolean).join("\n"));
|
|
4066
|
+
return block("resource", ["aws_cloudwatch_metric_alarm", id], body);
|
|
4067
|
+
}
|
|
4068
|
+
case "Monitoring.Dashboard": {
|
|
4069
|
+
const widgets = props.widgets ?? [];
|
|
4070
|
+
const dashBody = {
|
|
4071
|
+
widgets: widgets.map((w, i) => ({
|
|
4072
|
+
type: w.type === "text" ? "text" : "metric",
|
|
4073
|
+
x: i % 3 * 8,
|
|
4074
|
+
y: Math.floor(i / 3) * 6,
|
|
4075
|
+
width: 8,
|
|
4076
|
+
height: 6,
|
|
4077
|
+
properties: w.type === "text" ? { markdown: w.markdown ?? w.title } : { title: w.title, metrics: [[w.namespace ?? "AWS/Lambda", w.metricName]], period: w.period ?? 60, stat: w.stat ?? "Average", view: "timeSeries" }
|
|
4078
|
+
}))
|
|
4079
|
+
};
|
|
4080
|
+
const body = indent([
|
|
4081
|
+
attr("dashboard_name", construct.id),
|
|
4082
|
+
`dashboard_body = jsonencode(${JSON.stringify(dashBody)})`
|
|
4083
|
+
].join("\n"));
|
|
4084
|
+
return block("resource", ["aws_cloudwatch_dashboard", id], body);
|
|
4085
|
+
}
|
|
4086
|
+
case "Logging.Stream": {
|
|
4087
|
+
const filters = props.subscriptionFilters ?? [];
|
|
4088
|
+
const lgBody = indent([
|
|
4089
|
+
attr("name", `/iacmp/${construct.id}`),
|
|
4090
|
+
attr("retention_in_days", props.retentionDays ?? 30),
|
|
4091
|
+
...props.kmsKeyId ? [attr("kms_key_id", props.kmsKeyId)] : [],
|
|
4092
|
+
"",
|
|
4093
|
+
tagsBlock(construct.id)
|
|
4094
|
+
].join("\n"));
|
|
4095
|
+
const parts = [block("resource", ["aws_cloudwatch_log_group", id], lgBody)];
|
|
4096
|
+
for (const f of filters) {
|
|
4097
|
+
const filterId = `${id}_${f.name.replace(/[^a-zA-Z0-9_]/g, "_")}_filter`;
|
|
4098
|
+
const filterBody = indent([
|
|
4099
|
+
`log_group_name = aws_cloudwatch_log_group.${id}.name`,
|
|
4100
|
+
attr("name", f.name),
|
|
4101
|
+
attr("filter_pattern", f.filterPattern),
|
|
4102
|
+
attr("destination_arn", f.destinationArn)
|
|
4103
|
+
].join("\n"));
|
|
4104
|
+
parts.push(block("resource", ["aws_cloudwatch_log_subscription_filter", filterId], filterBody));
|
|
4105
|
+
}
|
|
4106
|
+
return parts.join("\n");
|
|
4107
|
+
}
|
|
4108
|
+
case "Custom.Resource": {
|
|
4109
|
+
const tf = props.terraform;
|
|
4110
|
+
if (!tf)
|
|
4111
|
+
return "";
|
|
4112
|
+
const body = indent(Object.entries(tf.body).map(([k, v]) => `${k} = ${hclValue(v)}`).join("\n"));
|
|
4113
|
+
return block("resource", [tf.type, id], body);
|
|
4114
|
+
}
|
|
4115
|
+
default:
|
|
4116
|
+
console.warn(`[terraform] Construct type '${construct.type}' nao suportado \u2014 descartado.`);
|
|
4117
|
+
return "";
|
|
4118
|
+
}
|
|
4119
|
+
}
|
|
4120
|
+
function synthesize(stack) {
|
|
4121
|
+
const awsBlock = block("aws", [], indent(`source = "hashicorp/aws"
|
|
4122
|
+
version = "~> 5.0"`));
|
|
4123
|
+
const requiredProvidersBlock = block("required_providers", [], indent(awsBlock));
|
|
4124
|
+
const terraformBlock = block("terraform", [], indent(requiredProvidersBlock));
|
|
4125
|
+
const providerBlock = block("provider", ["aws"], indent(attr("region", "us-east-1")));
|
|
4126
|
+
const needsDbPassword = stack.constructs.some((c) => c.type === "Database.SQL" || c.type === "Database.DocumentDB");
|
|
4127
|
+
const dbPasswordVar = needsDbPassword ? block("variable", ["db_password"], indent([
|
|
4128
|
+
"type = string",
|
|
4129
|
+
"sensitive = true",
|
|
4130
|
+
attr("description", "Senha do administrador do banco de dados (Database.SQL/DocumentDB). Forneca via TF_VAR_db_password ou tfvars.")
|
|
4131
|
+
].join("\n"))) : "";
|
|
4132
|
+
const header = [terraformBlock, providerBlock, dbPasswordVar].filter(Boolean).join("\n");
|
|
4133
|
+
const resources = stack.constructs.map((c) => synthesizeConstruct(c)).filter(Boolean).join("\n");
|
|
4134
|
+
return [header, resources].filter(Boolean).join("\n");
|
|
4135
|
+
}
|
|
4136
|
+
}
|
|
4137
|
+
});
|
|
4138
|
+
|
|
4139
|
+
// ../providers/terraform/dist/provider.js
|
|
4140
|
+
var require_provider4 = __commonJS({
|
|
4141
|
+
"../providers/terraform/dist/provider.js"(exports2) {
|
|
4142
|
+
"use strict";
|
|
4143
|
+
Object.defineProperty(exports2, "__esModule", { value: true });
|
|
4144
|
+
exports2.TerraformProvider = void 0;
|
|
4145
|
+
var hcl_1 = require_hcl();
|
|
4146
|
+
var TerraformProvider2 = class {
|
|
4147
|
+
name = "terraform";
|
|
4148
|
+
synthesize(stack) {
|
|
4149
|
+
return (0, hcl_1.synthesize)(stack);
|
|
4150
|
+
}
|
|
4151
|
+
};
|
|
4152
|
+
exports2.TerraformProvider = TerraformProvider2;
|
|
4153
|
+
}
|
|
4154
|
+
});
|
|
4155
|
+
|
|
4156
|
+
// ../providers/terraform/dist/index.js
|
|
4157
|
+
var require_dist4 = __commonJS({
|
|
4158
|
+
"../providers/terraform/dist/index.js"(exports2) {
|
|
4159
|
+
"use strict";
|
|
4160
|
+
Object.defineProperty(exports2, "__esModule", { value: true });
|
|
4161
|
+
exports2.synthesize = exports2.TerraformProvider = void 0;
|
|
4162
|
+
var provider_1 = require_provider4();
|
|
4163
|
+
Object.defineProperty(exports2, "TerraformProvider", { enumerable: true, get: function() {
|
|
4164
|
+
return provider_1.TerraformProvider;
|
|
4165
|
+
} });
|
|
4166
|
+
var hcl_1 = require_hcl();
|
|
4167
|
+
Object.defineProperty(exports2, "synthesize", { enumerable: true, get: function() {
|
|
4168
|
+
return hcl_1.synthesize;
|
|
4169
|
+
} });
|
|
4170
|
+
}
|
|
4171
|
+
});
|
|
4172
|
+
|
|
4173
|
+
// src/commands/diff.ts
|
|
4174
|
+
var diff_exports = {};
|
|
4175
|
+
__export(diff_exports, {
|
|
4176
|
+
default: () => Diff
|
|
4177
|
+
});
|
|
4178
|
+
module.exports = __toCommonJS(diff_exports);
|
|
4179
|
+
var import_core = require("@oclif/core");
|
|
4180
|
+
var fs4 = __toESM(require("fs"));
|
|
4181
|
+
var path3 = __toESM(require("path"));
|
|
4182
|
+
var import_chalk = __toESM(require("chalk"));
|
|
4183
|
+
var DiffLib = __toESM(require("diff"));
|
|
4184
|
+
var import_provider_aws = __toESM(require_dist());
|
|
4185
|
+
var import_provider_azure = __toESM(require_dist2());
|
|
4186
|
+
var import_provider_gcp = __toESM(require_dist3());
|
|
4187
|
+
var import_provider_terraform = __toESM(require_dist4());
|
|
4188
|
+
|
|
4189
|
+
// src/synth-out.ts
|
|
4190
|
+
var fs = __toESM(require("fs"));
|
|
4191
|
+
var path = __toESM(require("path"));
|
|
4192
|
+
function templateExt(provider) {
|
|
4193
|
+
return provider === "terraform" ? ".tf" : ".json";
|
|
4194
|
+
}
|
|
4195
|
+
function synthRoot(cwd) {
|
|
4196
|
+
return path.join(cwd, "synth-out");
|
|
4197
|
+
}
|
|
4198
|
+
function providerOutDir(cwd, provider) {
|
|
4199
|
+
return path.join(synthRoot(cwd), provider);
|
|
4200
|
+
}
|
|
4201
|
+
function hasTemplates(dir, provider) {
|
|
4202
|
+
const ext = templateExt(provider);
|
|
4203
|
+
try {
|
|
4204
|
+
return fs.readdirSync(dir).some((f) => f.endsWith(ext) && fs.statSync(path.join(dir, f)).isFile());
|
|
4205
|
+
} catch {
|
|
4206
|
+
return false;
|
|
4207
|
+
}
|
|
4208
|
+
}
|
|
4209
|
+
function resolveTemplateDir(cwd, provider) {
|
|
4210
|
+
const dir = providerOutDir(cwd, provider);
|
|
4211
|
+
if (fs.existsSync(dir)) return dir;
|
|
4212
|
+
const root = synthRoot(cwd);
|
|
4213
|
+
if (fs.existsSync(root) && hasTemplates(root, provider)) return root;
|
|
4214
|
+
return null;
|
|
4215
|
+
}
|
|
4216
|
+
|
|
4217
|
+
// src/load-stacks.ts
|
|
4218
|
+
var fs2 = __toESM(require("fs"));
|
|
4219
|
+
var path2 = __toESM(require("path"));
|
|
4220
|
+
function findStackFiles(dir) {
|
|
4221
|
+
const entries = fs2.readdirSync(dir, { withFileTypes: true });
|
|
4222
|
+
const files = [];
|
|
4223
|
+
for (const entry of entries) {
|
|
4224
|
+
const full = path2.join(dir, entry.name);
|
|
4225
|
+
if (entry.isDirectory()) {
|
|
4226
|
+
files.push(...findStackFiles(full));
|
|
4227
|
+
} else if (entry.name.endsWith(".ts") || entry.name.endsWith(".js")) {
|
|
4228
|
+
files.push(full);
|
|
4229
|
+
}
|
|
4230
|
+
}
|
|
4231
|
+
return files;
|
|
4232
|
+
}
|
|
4233
|
+
|
|
4234
|
+
// src/utils.ts
|
|
4235
|
+
var fs3 = __toESM(require("fs"));
|
|
4236
|
+
function readJsonFile(filePath) {
|
|
4237
|
+
let content;
|
|
4238
|
+
try {
|
|
4239
|
+
content = fs3.readFileSync(filePath, "utf-8");
|
|
4240
|
+
} catch (e) {
|
|
4241
|
+
throw new Error(`Falha ao ler '${filePath}': ${errMessage(e)}`);
|
|
4242
|
+
}
|
|
4243
|
+
try {
|
|
4244
|
+
return JSON.parse(content);
|
|
4245
|
+
} catch (e) {
|
|
4246
|
+
throw new Error(`JSON inv\xE1lido em '${filePath}': ${errMessage(e)}`);
|
|
4247
|
+
}
|
|
4248
|
+
}
|
|
4249
|
+
function errMessage(e) {
|
|
4250
|
+
if (e instanceof Error) return e.message;
|
|
4251
|
+
if (typeof e === "string") return e;
|
|
4252
|
+
try {
|
|
4253
|
+
return JSON.stringify(e);
|
|
4254
|
+
} catch {
|
|
4255
|
+
return String(e);
|
|
4256
|
+
}
|
|
4257
|
+
}
|
|
4258
|
+
|
|
4259
|
+
// src/commands/diff.ts
|
|
4260
|
+
var CONTEXT_LINES = 2;
|
|
4261
|
+
function renderDiff(oldText, newText) {
|
|
4262
|
+
const changes = DiffLib.diffLines(oldText, newText);
|
|
4263
|
+
let hasChanges = false;
|
|
4264
|
+
const buffer = [];
|
|
4265
|
+
for (const change of changes) {
|
|
4266
|
+
const lines = change.value.split("\n");
|
|
4267
|
+
if (lines[lines.length - 1] === "") lines.pop();
|
|
4268
|
+
for (const line of lines) {
|
|
4269
|
+
if (change.added) {
|
|
4270
|
+
buffer.push({ type: "add", line });
|
|
4271
|
+
hasChanges = true;
|
|
4272
|
+
} else if (change.removed) {
|
|
4273
|
+
buffer.push({ type: "remove", line });
|
|
4274
|
+
hasChanges = true;
|
|
4275
|
+
} else {
|
|
4276
|
+
buffer.push({ type: "same", line });
|
|
4277
|
+
}
|
|
4278
|
+
}
|
|
4279
|
+
}
|
|
4280
|
+
if (!hasChanges) return false;
|
|
4281
|
+
const changedIndexes = /* @__PURE__ */ new Set();
|
|
4282
|
+
buffer.forEach((entry, i) => {
|
|
4283
|
+
if (entry.type !== "same") {
|
|
4284
|
+
for (let c = Math.max(0, i - CONTEXT_LINES); c <= Math.min(buffer.length - 1, i + CONTEXT_LINES); c++) {
|
|
4285
|
+
changedIndexes.add(c);
|
|
4286
|
+
}
|
|
4287
|
+
}
|
|
4288
|
+
});
|
|
4289
|
+
let lastPrinted = -1;
|
|
4290
|
+
for (let i = 0; i < buffer.length; i++) {
|
|
4291
|
+
if (!changedIndexes.has(i)) continue;
|
|
4292
|
+
if (lastPrinted !== -1 && i > lastPrinted + 1) {
|
|
4293
|
+
process.stdout.write(import_chalk.default.dim("...\n"));
|
|
4294
|
+
}
|
|
4295
|
+
const { type, line } = buffer[i];
|
|
4296
|
+
if (type === "add") {
|
|
4297
|
+
process.stdout.write(import_chalk.default.green(`+ ${line}
|
|
4298
|
+
`));
|
|
4299
|
+
} else if (type === "remove") {
|
|
4300
|
+
process.stdout.write(import_chalk.default.red(`- ${line}
|
|
4301
|
+
`));
|
|
4302
|
+
} else {
|
|
4303
|
+
process.stdout.write(` ${line}
|
|
4304
|
+
`);
|
|
4305
|
+
}
|
|
4306
|
+
lastPrinted = i;
|
|
4307
|
+
}
|
|
4308
|
+
return true;
|
|
4309
|
+
}
|
|
4310
|
+
function synthStack(stack, provider) {
|
|
4311
|
+
switch (provider) {
|
|
4312
|
+
case "aws": {
|
|
4313
|
+
const p = new import_provider_aws.AWSProvider();
|
|
4314
|
+
return JSON.stringify(p.synthesize(stack), null, 2) + "\n";
|
|
4315
|
+
}
|
|
4316
|
+
case "azure": {
|
|
4317
|
+
const p = new import_provider_azure.AzureProvider();
|
|
4318
|
+
return JSON.stringify(p.synthesize(stack), null, 2) + "\n";
|
|
4319
|
+
}
|
|
4320
|
+
case "gcp": {
|
|
4321
|
+
const p = new import_provider_gcp.GCPProvider();
|
|
4322
|
+
return JSON.stringify(p.synthesize(stack), null, 2) + "\n";
|
|
4323
|
+
}
|
|
4324
|
+
case "terraform": {
|
|
4325
|
+
const p = new import_provider_terraform.TerraformProvider();
|
|
4326
|
+
return p.synthesize(stack);
|
|
4327
|
+
}
|
|
4328
|
+
default:
|
|
4329
|
+
throw new Error(`Provider '${provider}' n\xE3o suportado.`);
|
|
4330
|
+
}
|
|
4331
|
+
}
|
|
4332
|
+
var Diff = class _Diff extends import_core.Command {
|
|
4333
|
+
static description = "Compara o \xFAltimo synth salvo com o synth atual";
|
|
4334
|
+
static flags = {
|
|
4335
|
+
provider: import_core.Flags.string({ char: "p", description: "Provider alvo (aws, azure, gcp, terraform)" }),
|
|
4336
|
+
stack: import_core.Flags.string({ char: "s", description: "Stack espec\xEDfica" })
|
|
4337
|
+
};
|
|
4338
|
+
static examples = [
|
|
4339
|
+
"$ iacmp diff",
|
|
4340
|
+
"$ iacmp diff --provider aws",
|
|
4341
|
+
"$ iacmp diff --stack minha-stack"
|
|
4342
|
+
];
|
|
4343
|
+
async run() {
|
|
4344
|
+
const { flags } = await this.parse(_Diff);
|
|
4345
|
+
const cwd = process.cwd();
|
|
4346
|
+
const configPath = path3.join(cwd, "iacmp.json");
|
|
4347
|
+
if (!fs4.existsSync(configPath)) {
|
|
4348
|
+
this.error("Projeto n\xE3o inicializado. Rode: iacmp init");
|
|
4349
|
+
}
|
|
4350
|
+
let config;
|
|
4351
|
+
try {
|
|
4352
|
+
config = readJsonFile(configPath);
|
|
4353
|
+
} catch (err) {
|
|
4354
|
+
this.error(errMessage(err));
|
|
4355
|
+
}
|
|
4356
|
+
const provider = flags.provider ?? config.provider ?? "aws";
|
|
4357
|
+
const outDir = resolveTemplateDir(cwd, provider);
|
|
4358
|
+
if (!outDir) {
|
|
4359
|
+
this.log("Nenhum synth anterior encontrado. Rode: iacmp synth");
|
|
4360
|
+
return;
|
|
4361
|
+
}
|
|
4362
|
+
const ext = templateExt(provider);
|
|
4363
|
+
const existingFiles = fs4.readdirSync(outDir).filter((f) => f.endsWith(ext));
|
|
4364
|
+
if (existingFiles.length === 0) {
|
|
4365
|
+
this.log("Nenhum synth anterior encontrado. Rode: iacmp synth");
|
|
4366
|
+
return;
|
|
4367
|
+
}
|
|
4368
|
+
const stacksDir = path3.join(cwd, "stacks");
|
|
4369
|
+
if (!fs4.existsSync(stacksDir)) {
|
|
4370
|
+
this.error("Diret\xF3rio stacks/ n\xE3o encontrado.");
|
|
4371
|
+
}
|
|
4372
|
+
const stackFiles = findStackFiles(stacksDir).filter((f) => !flags.stack || path3.basename(f).replace(/\.(ts|js)$/, "") === flags.stack);
|
|
4373
|
+
if (stackFiles.length === 0) {
|
|
4374
|
+
this.error("Nenhuma stack encontrada em stacks/");
|
|
4375
|
+
}
|
|
4376
|
+
let anyDiff = false;
|
|
4377
|
+
for (const stackPath of stackFiles) {
|
|
4378
|
+
const file = path3.basename(stackPath);
|
|
4379
|
+
const stackName = file.replace(/\.(ts|js)$/, "");
|
|
4380
|
+
const savedPath = path3.join(outDir, `${stackName}${ext}`);
|
|
4381
|
+
if (!fs4.existsSync(savedPath)) {
|
|
4382
|
+
this.log(`Stack nova (sem synth anterior): ${stackName}`);
|
|
4383
|
+
anyDiff = true;
|
|
4384
|
+
continue;
|
|
4385
|
+
}
|
|
4386
|
+
let stackModule;
|
|
4387
|
+
try {
|
|
4388
|
+
stackModule = require(stackPath);
|
|
4389
|
+
} catch (err) {
|
|
4390
|
+
this.warn(`N\xE3o foi poss\xEDvel carregar ${file}: ${errMessage(err)}`);
|
|
4391
|
+
continue;
|
|
4392
|
+
}
|
|
4393
|
+
const stack = stackModule.default ?? stackModule.stack ?? stackModule;
|
|
4394
|
+
if (!stack || typeof stack !== "object" || !("constructs" in stack)) {
|
|
4395
|
+
this.warn(`${file} n\xE3o exporta uma Stack v\xE1lida.`);
|
|
4396
|
+
continue;
|
|
4397
|
+
}
|
|
4398
|
+
const oldText = fs4.readFileSync(savedPath, "utf-8");
|
|
4399
|
+
let newText;
|
|
4400
|
+
try {
|
|
4401
|
+
newText = synthStack(stack, provider);
|
|
4402
|
+
} catch (err) {
|
|
4403
|
+
this.warn(`Erro ao sintetizar ${stackName}: ${errMessage(err)}`);
|
|
4404
|
+
continue;
|
|
4405
|
+
}
|
|
4406
|
+
this.log(`
|
|
4407
|
+
${import_chalk.default.bold(stackName)}${ext}`);
|
|
4408
|
+
this.log(import_chalk.default.dim("\u2500".repeat(50)));
|
|
4409
|
+
const hasDiff = renderDiff(oldText, newText);
|
|
4410
|
+
if (hasDiff) {
|
|
4411
|
+
anyDiff = true;
|
|
4412
|
+
} else {
|
|
4413
|
+
this.log(import_chalk.default.dim(" (sem altera\xE7\xF5es)"));
|
|
4414
|
+
}
|
|
4415
|
+
}
|
|
4416
|
+
if (!anyDiff) {
|
|
4417
|
+
this.log("\nNenhuma altera\xE7\xE3o detectada.");
|
|
4418
|
+
}
|
|
4419
|
+
}
|
|
4420
|
+
};
|