@aws/nx-plugin 1.0.0-rc.84 → 1.0.0-rc.85
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/generators.json +7 -0
- package/migrations.json +6 -1
- package/package.json +1 -1
- package/src/internal/test-matrix/generator.js +16 -0
- package/src/internal/test-matrix/generator.js.map +1 -1
- package/src/migrations/latest/terraform-project-checkov-target-and-bootstrap-destroy/metadata.json +3 -0
- package/src/migrations/latest/terraform-project-checkov-target-and-bootstrap-destroy/migration.d.ts +6 -0
- package/src/migrations/latest/terraform-project-checkov-target-and-bootstrap-destroy/migration.js +146 -0
- package/src/migrations/latest/terraform-project-checkov-target-and-bootstrap-destroy/migration.js.map +1 -0
- package/src/open-api/json-metadata/generator.d.ts +24 -0
- package/src/open-api/json-metadata/generator.js +37 -0
- package/src/open-api/json-metadata/generator.js.map +1 -0
- package/src/open-api/json-metadata/schema.d.js +6 -0
- package/src/open-api/json-metadata/schema.d.js.map +1 -0
- package/src/open-api/json-metadata/schema.d.ts +9 -0
- package/src/open-api/json-metadata/schema.json +20 -0
- package/src/py/fast-api/__snapshots__/generator.terraform.spec.ts.snap +1604 -559
- package/src/py/fast-api/generator.js +2 -1
- package/src/py/fast-api/generator.js.map +1 -1
- package/src/sdk/open-api.d.ts +2 -0
- package/src/sdk/open-api.js +2 -1
- package/src/sdk/open-api.js.map +1 -1
- package/src/smithy/ts/api/__snapshots__/generator.spec.ts.snap +1548 -440
- package/src/smithy/ts/api/generator.js +2 -1
- package/src/smithy/ts/api/generator.js.map +1 -1
- package/src/terraform/project/files/application/bootstrap/providers.tf.template +2 -0
- package/src/terraform/project/files/application/scripts/bootstrap-destroy.ts.template +111 -0
- package/src/terraform/project/files/application/src/providers.tf.template +2 -0
- package/src/terraform/project/files/checkov/checkov.yml.template +8 -0
- package/src/terraform/project/generator.js +47 -4
- package/src/terraform/project/generator.js.map +1 -1
- package/src/trpc/backend/__snapshots__/generator.spec.ts.snap +1552 -525
- package/src/trpc/backend/files-operations/scripts/generate-operations.ts.template +40 -0
- package/src/trpc/backend/generator.js +10 -0
- package/src/trpc/backend/generator.js.map +1 -1
- package/src/utils/api-constructs/api-constructs.js +11 -0
- package/src/utils/api-constructs/api-constructs.js.map +1 -1
- package/src/utils/api-constructs/files/terraform/app/apis/http/__apiNameKebabCase__/__apiNameKebabCase__.tf.template +243 -3
- package/src/utils/api-constructs/files/terraform/app/apis/rest/__apiNameKebabCase__/__apiNameKebabCase__.tf.template +428 -1
- package/src/utils/api-constructs/open-api-metadata.d.ts +20 -3
- package/src/utils/api-constructs/open-api-metadata.js +64 -8
- package/src/utils/api-constructs/open-api-metadata.js.map +1 -1
- package/src/utils/api-constructs/trpc-operations-metadata.d.ts +21 -0
- package/src/utils/api-constructs/trpc-operations-metadata.js +55 -0
- package/src/utils/api-constructs/trpc-operations-metadata.js.map +1 -0
|
@@ -131,6 +131,95 @@ resource "random_string" "suffix" {
|
|
|
131
131
|
upper = false
|
|
132
132
|
}
|
|
133
133
|
|
|
134
|
+
<%_ if (backend.integrationPattern === 'isolated') { _%>
|
|
135
|
+
locals {
|
|
136
|
+
# Generated at build time from the API definition
|
|
137
|
+
operations_file = "${path.module}/../../../generated/<%- apiNameKebabCase %>/operations.json"
|
|
138
|
+
operations = fileexists(local.operations_file) ? jsondecode(file(local.operations_file)) : {}
|
|
139
|
+
|
|
140
|
+
operation_slug = { for op in keys(local.operations) : op => replace(op, "/[^a-zA-Z0-9-_]/", "-") }
|
|
141
|
+
operation_hash = { for op in keys(local.operations) : op => substr(sha256(op), 0, 8) }
|
|
142
|
+
|
|
143
|
+
function_name = { for op in keys(local.operations) : op =>
|
|
144
|
+
"${substr("<%- apiNameClassName %>-${local.operation_slug[op]}", 0, 64 - length(local.operation_hash[op]) - length(random_string.suffix.result) - 2)}-${local.operation_hash[op]}-${random_string.suffix.result}"
|
|
145
|
+
}
|
|
146
|
+
role_name = { for op in keys(local.operations) : op =>
|
|
147
|
+
"${substr("<%- apiNameClassName %>-${local.operation_slug[op]}-role", 0, 64 - length(local.operation_hash[op]) - length(random_string.suffix.result) - 2)}-${local.operation_hash[op]}-${random_string.suffix.result}"
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
# A REST API needs a resource per path segment, so each operation's path is
|
|
151
|
+
# split into the distinct path prefixes the resource tree is built from below
|
|
152
|
+
operation_segments = { for op, details in local.operations : op => compact(split("/", details.path)) }
|
|
153
|
+
|
|
154
|
+
path_nodes = merge([
|
|
155
|
+
for op, segments in local.operation_segments : {
|
|
156
|
+
for i, segment in segments : join("/", slice(segments, 0, i + 1)) => {
|
|
157
|
+
depth = i
|
|
158
|
+
parent = i == 0 ? "" : join("/", slice(segments, 0, i))
|
|
159
|
+
part = segment
|
|
160
|
+
}
|
|
161
|
+
}
|
|
162
|
+
]...)
|
|
163
|
+
|
|
164
|
+
# Instances of a resource cannot reference one another, so each depth is a
|
|
165
|
+
# separate resource. Deeper paths are reported by the precondition below.
|
|
166
|
+
max_path_depth = <%- maxRestPathDepth %>
|
|
167
|
+
path_nodes_by_depth = { for depth in range(local.max_path_depth) : depth => {
|
|
168
|
+
for path, node in local.path_nodes : path => node if node.depth == depth
|
|
169
|
+
} }
|
|
170
|
+
paths_too_deep = [for path, node in local.path_nodes : path if node.depth >= local.max_path_depth]
|
|
171
|
+
|
|
172
|
+
resource_ids = merge(
|
|
173
|
+
<%_ for (let depth = 0; depth < maxRestPathDepth; depth++) { _%>
|
|
174
|
+
{ for path, resource in aws_api_gateway_resource.path_depth_<%- depth %> : path => resource.id },
|
|
175
|
+
<%_ } _%>
|
|
176
|
+
)
|
|
177
|
+
|
|
178
|
+
operation_resource_id = { for op, segments in local.operation_segments : op =>
|
|
179
|
+
local.resource_ids[join("/", segments)]
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
permission_source_suffix = { for op, details in local.operations : op =>
|
|
183
|
+
"${upper(details.method)}/${replace(join("/", local.operation_segments[op]), "/{[^}]*}/", "*")}"
|
|
184
|
+
}
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
resource "terraform_data" "operations_metadata" {
|
|
188
|
+
# Fails the plan when the operations metadata has not been generated
|
|
189
|
+
input = local.operations_file
|
|
190
|
+
|
|
191
|
+
lifecycle {
|
|
192
|
+
precondition {
|
|
193
|
+
condition = fileexists(local.operations_file)
|
|
194
|
+
error_message = "Operations metadata not found at ${local.operations_file}. Build the <%- apiNameClassName %> project to generate it."
|
|
195
|
+
}
|
|
196
|
+
precondition {
|
|
197
|
+
condition = length(local.operations) > 0
|
|
198
|
+
error_message = "No operations found in ${local.operations_file}. The <%- apiNameClassName %> API must define at least one operation."
|
|
199
|
+
}
|
|
200
|
+
precondition {
|
|
201
|
+
condition = length(local.paths_too_deep) == 0
|
|
202
|
+
error_message = "Operation paths exceed the supported depth of ${local.max_path_depth} segments: ${join(", ", local.paths_too_deep)}. Add further aws_api_gateway_resource levels to this module to support them."
|
|
203
|
+
}
|
|
204
|
+
}
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
<%_ for (let depth = 0; depth < maxRestPathDepth; depth++) { _%>
|
|
208
|
+
# API Gateway resources at path depth <%- depth %>
|
|
209
|
+
resource "aws_api_gateway_resource" "path_depth_<%- depth %>" {
|
|
210
|
+
for_each = local.path_nodes_by_depth[<%- depth %>]
|
|
211
|
+
|
|
212
|
+
rest_api_id = module.rest_api.api_id
|
|
213
|
+
<%_ if (depth === 0) { _%>
|
|
214
|
+
parent_id = module.rest_api.api_root_resource_id
|
|
215
|
+
<%_ } else { _%>
|
|
216
|
+
parent_id = aws_api_gateway_resource.path_depth_<%- depth - 1 %>[each.value.parent].id
|
|
217
|
+
<%_ } _%>
|
|
218
|
+
path_part = each.value.part
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
<%_ } _%>
|
|
222
|
+
<%_ } _%>
|
|
134
223
|
# Resources
|
|
135
224
|
|
|
136
225
|
# Create Lambda ZIP file from the bundle directory
|
|
@@ -205,8 +294,14 @@ resource "aws_vpc_security_group_egress_rule" "api_lambda_https" {
|
|
|
205
294
|
description = "Allow outbound HTTPS to AWS service endpoints"
|
|
206
295
|
}
|
|
207
296
|
|
|
208
|
-
# Lambda function
|
|
209
297
|
resource "aws_lambda_function" "api_lambda" {
|
|
298
|
+
<%_ if (backend.integrationPattern === 'isolated') { _%>
|
|
299
|
+
# One lambda function per operation, each serving just that operation
|
|
300
|
+
for_each = local.operations
|
|
301
|
+
|
|
302
|
+
<%_ } else { _%>
|
|
303
|
+
# A single "router" lambda serving all requests
|
|
304
|
+
<%_ } _%>
|
|
210
305
|
#checkov:skip=CKV_AWS_117:Lambda function is optionally deployed into a VPC via vpc_id/subnet_ids; not required for this use case
|
|
211
306
|
#checkov:skip=CKV_AWS_116:Dead Letter Queue not required for this simple API use case
|
|
212
307
|
#checkov:skip=CKV_AWS_272:Code signing not required for this use case
|
|
@@ -215,8 +310,13 @@ resource "aws_lambda_function" "api_lambda" {
|
|
|
215
310
|
s3_bucket = aws_s3_object.lambda_zip.bucket
|
|
216
311
|
s3_key = aws_s3_object.lambda_zip.key
|
|
217
312
|
s3_object_version = aws_s3_object.lambda_zip.version_id
|
|
313
|
+
<%_ if (backend.integrationPattern === 'isolated') { _%>
|
|
314
|
+
function_name = local.function_name[each.key]
|
|
315
|
+
role = aws_iam_role.lambda_execution_role[each.key].arn
|
|
316
|
+
<%_ } else { _%>
|
|
218
317
|
function_name = "<%- apiNameClassName %>Handler-${random_string.suffix.result}"
|
|
219
318
|
role = aws_iam_role.lambda_execution_role.arn
|
|
319
|
+
<%_ } _%>
|
|
220
320
|
<%_ if (['trpc', 'smithy'].includes(backend.type)) { _%>
|
|
221
321
|
handler = "index.handler"
|
|
222
322
|
runtime = "nodejs22.x"
|
|
@@ -272,7 +372,13 @@ resource "aws_lambda_function" "api_lambda" {
|
|
|
272
372
|
|
|
273
373
|
# IAM role for Lambda execution
|
|
274
374
|
resource "aws_iam_role" "lambda_execution_role" {
|
|
375
|
+
<%_ if (backend.integrationPattern === 'isolated') { _%>
|
|
376
|
+
for_each = local.operations
|
|
377
|
+
|
|
378
|
+
name = local.role_name[each.key]
|
|
379
|
+
<%_ } else { _%>
|
|
275
380
|
name = "<%- apiNameClassName %>Handler-execution-role-${random_string.suffix.result}"
|
|
381
|
+
<%_ } _%>
|
|
276
382
|
|
|
277
383
|
assume_role_policy = jsonencode({
|
|
278
384
|
Version = "2012-10-17"
|
|
@@ -292,25 +398,57 @@ resource "aws_iam_role" "lambda_execution_role" {
|
|
|
292
398
|
|
|
293
399
|
# Attach basic execution policy to Lambda role
|
|
294
400
|
resource "aws_iam_role_policy_attachment" "lambda_basic_execution" {
|
|
401
|
+
<%_ if (backend.integrationPattern === 'isolated') { _%>
|
|
402
|
+
for_each = local.operations
|
|
403
|
+
|
|
404
|
+
policy_arn = "arn:aws:iam::aws:policy/service-role/AWSLambdaBasicExecutionRole"
|
|
405
|
+
role = aws_iam_role.lambda_execution_role[each.key].name
|
|
406
|
+
<%_ } else { _%>
|
|
295
407
|
policy_arn = "arn:aws:iam::aws:policy/service-role/AWSLambdaBasicExecutionRole"
|
|
296
408
|
role = aws_iam_role.lambda_execution_role.name
|
|
409
|
+
<%_ } _%>
|
|
297
410
|
}
|
|
298
411
|
|
|
299
412
|
# Attach X-Ray tracing policy to Lambda role
|
|
300
413
|
resource "aws_iam_role_policy_attachment" "lambda_xray_execution" {
|
|
414
|
+
<%_ if (backend.integrationPattern === 'isolated') { _%>
|
|
415
|
+
for_each = local.operations
|
|
416
|
+
|
|
417
|
+
policy_arn = "arn:aws:iam::aws:policy/AWSXRayDaemonWriteAccess"
|
|
418
|
+
role = aws_iam_role.lambda_execution_role[each.key].name
|
|
419
|
+
<%_ } else { _%>
|
|
301
420
|
policy_arn = "arn:aws:iam::aws:policy/AWSXRayDaemonWriteAccess"
|
|
302
421
|
role = aws_iam_role.lambda_execution_role.name
|
|
422
|
+
<%_ } _%>
|
|
303
423
|
}
|
|
304
424
|
|
|
305
425
|
# Attach VPC access policy to Lambda role when deployed into a VPC
|
|
306
426
|
resource "aws_iam_role_policy_attachment" "lambda_vpc_access" {
|
|
427
|
+
<%_ if (backend.integrationPattern === 'isolated') { _%>
|
|
428
|
+
for_each = var.enable_vpc ? local.operations : {}
|
|
429
|
+
|
|
430
|
+
policy_arn = "arn:aws:iam::aws:policy/service-role/AWSLambdaVPCAccessExecutionRole"
|
|
431
|
+
role = aws_iam_role.lambda_execution_role[each.key].name
|
|
432
|
+
<%_ } else { _%>
|
|
307
433
|
count = var.enable_vpc ? 1 : 0
|
|
308
434
|
policy_arn = "arn:aws:iam::aws:policy/service-role/AWSLambdaVPCAccessExecutionRole"
|
|
309
435
|
role = aws_iam_role.lambda_execution_role.name
|
|
436
|
+
<%_ } _%>
|
|
310
437
|
}
|
|
311
438
|
|
|
312
439
|
# Additional IAM policies for Lambda (if provided)
|
|
313
440
|
resource "aws_iam_role_policy" "lambda_additional_policies" {
|
|
441
|
+
<%_ if (backend.integrationPattern === 'isolated') { _%>
|
|
442
|
+
for_each = local.operations
|
|
443
|
+
|
|
444
|
+
name = "${substr(local.function_name[each.key], 0, 55)}-policies"
|
|
445
|
+
role = aws_iam_role.lambda_execution_role[each.key].id
|
|
446
|
+
|
|
447
|
+
policy = jsonencode({
|
|
448
|
+
Version = "2012-10-17"
|
|
449
|
+
Statement = local.lambda_policy_document_statements
|
|
450
|
+
})
|
|
451
|
+
<%_ } else { _%>
|
|
314
452
|
count = length(local.lambda_policy_statements) > 0 ? 1 : 0
|
|
315
453
|
name = "<%- apiNameClassName %>Handler-additional-policies-${random_string.suffix.result}"
|
|
316
454
|
role = aws_iam_role.lambda_execution_role.id
|
|
@@ -319,6 +457,7 @@ resource "aws_iam_role_policy" "lambda_additional_policies" {
|
|
|
319
457
|
Version = "2012-10-17"
|
|
320
458
|
Statement = local.lambda_policy_statements
|
|
321
459
|
})
|
|
460
|
+
<%_ } _%>
|
|
322
461
|
}
|
|
323
462
|
|
|
324
463
|
locals {
|
|
@@ -333,6 +472,17 @@ locals {
|
|
|
333
472
|
Resource = ["${var.appconfig_application_arn}/*"]
|
|
334
473
|
}
|
|
335
474
|
] : [], var.additional_iam_policy_statements)
|
|
475
|
+
<%_ if (backend.integrationPattern === 'isolated') { _%>
|
|
476
|
+
|
|
477
|
+
# IAM rejects an empty statement list, so fall back to a no-op deny
|
|
478
|
+
lambda_policy_document_statements = length(local.lambda_policy_statements) > 0 ? local.lambda_policy_statements : [
|
|
479
|
+
{
|
|
480
|
+
Effect = "Deny"
|
|
481
|
+
Action = ["appconfig:GetLatestConfiguration"]
|
|
482
|
+
Resource = ["arn:aws:appconfig:${data.aws_region.current.region}:${data.aws_caller_identity.current.account_id}:application/none"]
|
|
483
|
+
}
|
|
484
|
+
]
|
|
485
|
+
<%_ } _%>
|
|
336
486
|
}
|
|
337
487
|
|
|
338
488
|
# CloudWatch Log Group for Lambda
|
|
@@ -340,16 +490,31 @@ resource "aws_cloudwatch_log_group" "lambda_logs" {
|
|
|
340
490
|
#checkov:skip=CKV_AWS_158:Using default CloudWatch log encryption
|
|
341
491
|
#checkov:skip=CKV_AWS_338:Log retention set to forever
|
|
342
492
|
#checkov:skip=CKV_AWS_66:Log retention set to forever
|
|
493
|
+
<%_ if (backend.integrationPattern === 'isolated') { _%>
|
|
494
|
+
for_each = local.operations
|
|
495
|
+
|
|
496
|
+
name = "/aws/lambda/${local.function_name[each.key]}"
|
|
497
|
+
tags = var.tags
|
|
498
|
+
<%_ } else { _%>
|
|
343
499
|
name = "/aws/lambda/<%- apiNameClassName %>Handler-${random_string.suffix.result}"
|
|
344
500
|
tags = var.tags
|
|
501
|
+
<%_ } _%>
|
|
345
502
|
}
|
|
346
503
|
|
|
347
504
|
<%_ if (backend.type === 'fastapi') { _%>
|
|
348
505
|
# Lambda alias pointing to the latest published version for SnapStart
|
|
349
506
|
resource "aws_lambda_alias" "live" {
|
|
507
|
+
<%_ if (backend.integrationPattern === 'isolated') { _%>
|
|
508
|
+
for_each = local.operations
|
|
509
|
+
|
|
510
|
+
name = "live"
|
|
511
|
+
function_name = aws_lambda_function.api_lambda[each.key].function_name
|
|
512
|
+
function_version = aws_lambda_function.api_lambda[each.key].version
|
|
513
|
+
<%_ } else { _%>
|
|
350
514
|
name = "live"
|
|
351
515
|
function_name = aws_lambda_function.api_lambda.function_name
|
|
352
516
|
function_version = aws_lambda_function.api_lambda.version
|
|
517
|
+
<%_ } _%>
|
|
353
518
|
|
|
354
519
|
depends_on = [aws_lambda_function.api_lambda]
|
|
355
520
|
}
|
|
@@ -472,6 +637,161 @@ resource "aws_lambda_permission" "authorizer_invoke" {
|
|
|
472
637
|
}
|
|
473
638
|
<%_ } _%>
|
|
474
639
|
|
|
640
|
+
<%_ if (backend.integrationPattern === 'isolated') { _%>
|
|
641
|
+
# Method per operation
|
|
642
|
+
resource "aws_api_gateway_method" "operation_methods" {
|
|
643
|
+
#checkov:skip=CKV2_AWS_53:Request validation not required for proxy integration as Lambda handles validation
|
|
644
|
+
for_each = local.operations
|
|
645
|
+
|
|
646
|
+
rest_api_id = module.rest_api.api_id
|
|
647
|
+
resource_id = local.operation_resource_id[each.key]
|
|
648
|
+
http_method = upper(each.value.method)
|
|
649
|
+
|
|
650
|
+
<%_ if (auth === 'iam') { _%>
|
|
651
|
+
authorization = "AWS_IAM"
|
|
652
|
+
<%_ } else if (auth === 'cognito') { _%>
|
|
653
|
+
authorization = "COGNITO_USER_POOLS"
|
|
654
|
+
authorizer_id = aws_api_gateway_authorizer.cognito_authorizer.id
|
|
655
|
+
# Accept access tokens from both sign-in flows: 'openid' (Cognito hosted UI)
|
|
656
|
+
# and 'aws.cognito.signin.user.admin' (Cognito admin/SRP auth APIs).
|
|
657
|
+
authorization_scopes = ["openid", "aws.cognito.signin.user.admin"]
|
|
658
|
+
<%_ } else if (auth === 'custom') { _%>
|
|
659
|
+
authorization = "CUSTOM"
|
|
660
|
+
authorizer_id = aws_api_gateway_authorizer.custom_authorizer.id
|
|
661
|
+
<%_ } _%>
|
|
662
|
+
|
|
663
|
+
# Path parameters must be declared on the method
|
|
664
|
+
request_parameters = {
|
|
665
|
+
for segment in local.operation_segments[each.key] :
|
|
666
|
+
"method.request.path.${trimsuffix(trimprefix(segment, "{"), "}")}" => true
|
|
667
|
+
if startswith(segment, "{")
|
|
668
|
+
}
|
|
669
|
+
|
|
670
|
+
depends_on = [<% if (auth === 'cognito') { %>aws_api_gateway_authorizer.cognito_authorizer<% } else if (auth === 'custom') { %>aws_api_gateway_authorizer.custom_authorizer<% } %>]
|
|
671
|
+
}
|
|
672
|
+
|
|
673
|
+
# Lambda integration per operation
|
|
674
|
+
resource "aws_api_gateway_integration" "lambda_integration" {
|
|
675
|
+
for_each = local.operations
|
|
676
|
+
|
|
677
|
+
rest_api_id = module.rest_api.api_id
|
|
678
|
+
resource_id = local.operation_resource_id[each.key]
|
|
679
|
+
http_method = aws_api_gateway_method.operation_methods[each.key].http_method
|
|
680
|
+
|
|
681
|
+
integration_http_method = "POST"
|
|
682
|
+
type = "AWS_PROXY"
|
|
683
|
+
<%_ if (backend.type === 'fastapi') { _%>
|
|
684
|
+
# Use the response streaming invocation path with the alias ARN for SnapStart
|
|
685
|
+
uri = "arn:aws:apigateway:${data.aws_region.current.region}:lambda:path/2021-11-15/functions/${aws_lambda_alias.live[each.key].arn}/response-streaming-invocations"
|
|
686
|
+
response_transfer_mode = "STREAM"
|
|
687
|
+
<%_ } else if (backend.type === 'trpc') { _%>
|
|
688
|
+
uri = aws_lambda_function.api_lambda[each.key].response_streaming_invoke_arn
|
|
689
|
+
response_transfer_mode = "STREAM"
|
|
690
|
+
<%_ } else { _%>
|
|
691
|
+
uri = aws_lambda_function.api_lambda[each.key].invoke_arn
|
|
692
|
+
<%_ } _%>
|
|
693
|
+
|
|
694
|
+
<%_ if (backend.type === 'fastapi') { _%>
|
|
695
|
+
depends_on = [aws_lambda_alias.live]
|
|
696
|
+
<%_ } else { _%>
|
|
697
|
+
depends_on = [aws_lambda_function.api_lambda]
|
|
698
|
+
<%_ } _%>
|
|
699
|
+
}
|
|
700
|
+
|
|
701
|
+
# OPTIONS method for CORS preflight, on every resource an operation is served from
|
|
702
|
+
resource "aws_api_gateway_method" "options_method" {
|
|
703
|
+
#checkov:skip=CKV2_AWS_70:OPTIONS method must be unauthenticated for CORS preflight requests
|
|
704
|
+
#checkov:skip=CKV2_AWS_53:Request validation not required for OPTIONS CORS preflight method
|
|
705
|
+
for_each = local.resource_ids
|
|
706
|
+
|
|
707
|
+
rest_api_id = module.rest_api.api_id
|
|
708
|
+
resource_id = each.value
|
|
709
|
+
http_method = "OPTIONS"
|
|
710
|
+
authorization = "NONE"
|
|
711
|
+
}
|
|
712
|
+
|
|
713
|
+
# CORS integration for OPTIONS methods
|
|
714
|
+
resource "aws_api_gateway_integration" "options_integration" {
|
|
715
|
+
for_each = local.resource_ids
|
|
716
|
+
|
|
717
|
+
rest_api_id = module.rest_api.api_id
|
|
718
|
+
resource_id = each.value
|
|
719
|
+
http_method = aws_api_gateway_method.options_method[each.key].http_method
|
|
720
|
+
|
|
721
|
+
type = "MOCK"
|
|
722
|
+
request_templates = {
|
|
723
|
+
"application/json" = "{\"statusCode\": 204}"
|
|
724
|
+
}
|
|
725
|
+
}
|
|
726
|
+
|
|
727
|
+
# OPTIONS method responses
|
|
728
|
+
resource "aws_api_gateway_method_response" "options_response" {
|
|
729
|
+
for_each = local.resource_ids
|
|
730
|
+
|
|
731
|
+
rest_api_id = module.rest_api.api_id
|
|
732
|
+
resource_id = each.value
|
|
733
|
+
http_method = aws_api_gateway_method.options_method[each.key].http_method
|
|
734
|
+
status_code = "204"
|
|
735
|
+
|
|
736
|
+
response_parameters = {
|
|
737
|
+
"method.response.header.Access-Control-Allow-Headers" = true
|
|
738
|
+
"method.response.header.Access-Control-Allow-Methods" = true
|
|
739
|
+
"method.response.header.Access-Control-Allow-Origin" = true
|
|
740
|
+
}
|
|
741
|
+
}
|
|
742
|
+
|
|
743
|
+
# OPTIONS integration responses
|
|
744
|
+
resource "aws_api_gateway_integration_response" "options_integration_response" {
|
|
745
|
+
for_each = local.resource_ids
|
|
746
|
+
|
|
747
|
+
rest_api_id = module.rest_api.api_id
|
|
748
|
+
resource_id = each.value
|
|
749
|
+
http_method = aws_api_gateway_method.options_method[each.key].http_method
|
|
750
|
+
status_code = aws_api_gateway_method_response.options_response[each.key].status_code
|
|
751
|
+
|
|
752
|
+
response_parameters = {
|
|
753
|
+
"method.response.header.Access-Control-Allow-Headers" = "'${join(",", var.cors_allow_headers)}'"
|
|
754
|
+
"method.response.header.Access-Control-Allow-Methods" = "'${join(",", var.cors_allow_methods)}'"
|
|
755
|
+
"method.response.header.Access-Control-Allow-Origin" = "'${join(",", var.cors_allow_origins)}'"
|
|
756
|
+
}
|
|
757
|
+
|
|
758
|
+
depends_on = [aws_api_gateway_integration.options_integration]
|
|
759
|
+
}
|
|
760
|
+
|
|
761
|
+
# API Gateway deployment
|
|
762
|
+
resource "aws_api_gateway_deployment" "api_deployment" {
|
|
763
|
+
rest_api_id = module.rest_api.api_id
|
|
764
|
+
|
|
765
|
+
triggers = {
|
|
766
|
+
redeployment = sha1(jsonencode(concat(
|
|
767
|
+
[for r in aws_api_gateway_resource.path_depth_0 : r.id],
|
|
768
|
+
<%_ for (let depth = 1; depth < maxRestPathDepth; depth++) { _%>
|
|
769
|
+
[for r in aws_api_gateway_resource.path_depth_<%- depth %> : r.id],
|
|
770
|
+
<%_ } _%>
|
|
771
|
+
[for m in aws_api_gateway_method.operation_methods : m.id],
|
|
772
|
+
[for i in aws_api_gateway_integration.lambda_integration : i.id],
|
|
773
|
+
[for m in aws_api_gateway_method.options_method : m.id],
|
|
774
|
+
[for i in aws_api_gateway_integration.options_integration : i.id],
|
|
775
|
+
)))
|
|
776
|
+
}
|
|
777
|
+
|
|
778
|
+
lifecycle {
|
|
779
|
+
create_before_destroy = true
|
|
780
|
+
}
|
|
781
|
+
|
|
782
|
+
depends_on = [
|
|
783
|
+
aws_api_gateway_method.operation_methods,
|
|
784
|
+
aws_api_gateway_integration.lambda_integration,
|
|
785
|
+
aws_api_gateway_method.options_method,
|
|
786
|
+
aws_api_gateway_integration.options_integration,
|
|
787
|
+
aws_api_gateway_method_response.options_response,
|
|
788
|
+
aws_api_gateway_integration_response.options_integration_response,
|
|
789
|
+
<%_ if (auth === 'custom') { _%>
|
|
790
|
+
aws_api_gateway_authorizer.custom_authorizer,
|
|
791
|
+
<%_ } _%>
|
|
792
|
+
]
|
|
793
|
+
}
|
|
794
|
+
<%_ } else { _%>
|
|
475
795
|
# Create proxy resource (captures all paths)
|
|
476
796
|
resource "aws_api_gateway_resource" "proxy_resource" {
|
|
477
797
|
rest_api_id = module.rest_api.api_id
|
|
@@ -613,6 +933,8 @@ resource "aws_api_gateway_deployment" "api_deployment" {
|
|
|
613
933
|
]
|
|
614
934
|
}
|
|
615
935
|
|
|
936
|
+
<%_ } _%>
|
|
937
|
+
|
|
616
938
|
# KMS key for encrypting access logs at rest
|
|
617
939
|
resource "aws_kms_key" "access_logs" {
|
|
618
940
|
description = "<%- apiNameClassName %> API access log encryption"
|
|
@@ -757,6 +1079,18 @@ resource "aws_api_gateway_rest_api_policy" "api_policy" {
|
|
|
757
1079
|
# Lambda permission for API Gateway to invoke the function
|
|
758
1080
|
<%_ } _%>
|
|
759
1081
|
resource "aws_lambda_permission" "api_gateway_invoke" {
|
|
1082
|
+
<%_ if (backend.integrationPattern === 'isolated') { _%>
|
|
1083
|
+
for_each = local.operations
|
|
1084
|
+
|
|
1085
|
+
statement_id = "AllowExecutionFromAPIGateway"
|
|
1086
|
+
action = "lambda:InvokeFunction"
|
|
1087
|
+
function_name = aws_lambda_function.api_lambda[each.key].function_name
|
|
1088
|
+
<%_ if (backend.type === 'fastapi') { _%>
|
|
1089
|
+
qualifier = aws_lambda_alias.live[each.key].name
|
|
1090
|
+
<%_ } _%>
|
|
1091
|
+
principal = "apigateway.amazonaws.com"
|
|
1092
|
+
source_arn = "${module.rest_api.api_execution_arn}/*/${local.permission_source_suffix[each.key]}"
|
|
1093
|
+
<%_ } else { _%>
|
|
760
1094
|
statement_id = "AllowExecutionFromAPIGateway"
|
|
761
1095
|
action = "lambda:InvokeFunction"
|
|
762
1096
|
function_name = aws_lambda_function.api_lambda.function_name
|
|
@@ -765,6 +1099,7 @@ resource "aws_lambda_permission" "api_gateway_invoke" {
|
|
|
765
1099
|
<%_ } _%>
|
|
766
1100
|
principal = "apigateway.amazonaws.com"
|
|
767
1101
|
source_arn = "${module.rest_api.api_execution_arn}/*/*"
|
|
1102
|
+
<%_ } _%>
|
|
768
1103
|
|
|
769
1104
|
<%_ if (backend.type === 'fastapi') { _%>
|
|
770
1105
|
depends_on = [module.rest_api, aws_lambda_alias.live]
|
|
@@ -776,12 +1111,23 @@ resource "aws_lambda_permission" "api_gateway_invoke" {
|
|
|
776
1111
|
<%_ if (backend.type === 'fastapi') { _%>
|
|
777
1112
|
# Lambda permission for API Gateway to invoke with response streaming via alias
|
|
778
1113
|
resource "aws_lambda_permission" "api_gateway_invoke_streaming" {
|
|
1114
|
+
<%_ if (backend.integrationPattern === 'isolated') { _%>
|
|
1115
|
+
for_each = local.operations
|
|
1116
|
+
|
|
1117
|
+
statement_id = "AllowStreamingFromAPIGateway"
|
|
1118
|
+
action = "lambda:InvokeFunctionWithResponseStream"
|
|
1119
|
+
function_name = aws_lambda_function.api_lambda[each.key].function_name
|
|
1120
|
+
qualifier = aws_lambda_alias.live[each.key].name
|
|
1121
|
+
principal = "apigateway.amazonaws.com"
|
|
1122
|
+
source_arn = "${module.rest_api.api_execution_arn}/*/${local.permission_source_suffix[each.key]}"
|
|
1123
|
+
<%_ } else { _%>
|
|
779
1124
|
statement_id = "AllowStreamingFromAPIGateway"
|
|
780
1125
|
action = "lambda:InvokeFunctionWithResponseStream"
|
|
781
1126
|
function_name = aws_lambda_function.api_lambda.function_name
|
|
782
1127
|
qualifier = aws_lambda_alias.live.name
|
|
783
1128
|
principal = "apigateway.amazonaws.com"
|
|
784
1129
|
source_arn = "${module.rest_api.api_execution_arn}/*/*"
|
|
1130
|
+
<%_ } _%>
|
|
785
1131
|
|
|
786
1132
|
depends_on = [module.rest_api, aws_lambda_alias.live]
|
|
787
1133
|
}
|
|
@@ -846,6 +1192,86 @@ output "stage_id" {
|
|
|
846
1192
|
value = aws_api_gateway_stage.api_stage.id
|
|
847
1193
|
}
|
|
848
1194
|
|
|
1195
|
+
<%_ if (backend.integrationPattern === 'isolated') { _%>
|
|
1196
|
+
# Lambda Function Outputs, keyed by operation name
|
|
1197
|
+
output "operations" {
|
|
1198
|
+
description = "Names of the API operations, each with its own Lambda function"
|
|
1199
|
+
value = keys(local.operations)
|
|
1200
|
+
}
|
|
1201
|
+
|
|
1202
|
+
output "lambda_function_names" {
|
|
1203
|
+
description = "Name of each operation's Lambda function, keyed by operation name"
|
|
1204
|
+
value = { for op, fn in aws_lambda_function.api_lambda : op => fn.function_name }
|
|
1205
|
+
}
|
|
1206
|
+
|
|
1207
|
+
output "lambda_function_arns" {
|
|
1208
|
+
description = "ARN of each operation's Lambda function, keyed by operation name"
|
|
1209
|
+
value = { for op, fn in aws_lambda_function.api_lambda : op => fn.arn }
|
|
1210
|
+
}
|
|
1211
|
+
|
|
1212
|
+
output "lambda_invoke_arns" {
|
|
1213
|
+
description = "Invoke ARN of each operation's Lambda function, keyed by operation name"
|
|
1214
|
+
value = { for op, fn in aws_lambda_function.api_lambda : op => fn.invoke_arn }
|
|
1215
|
+
}
|
|
1216
|
+
|
|
1217
|
+
output "lambda_qualified_arns" {
|
|
1218
|
+
description = "Qualified ARN of each operation's Lambda function, keyed by operation name"
|
|
1219
|
+
value = { for op, fn in aws_lambda_function.api_lambda : op => fn.qualified_arn }
|
|
1220
|
+
}
|
|
1221
|
+
|
|
1222
|
+
output "lambda_versions" {
|
|
1223
|
+
description = "Version of each operation's Lambda function, keyed by operation name"
|
|
1224
|
+
value = { for op, fn in aws_lambda_function.api_lambda : op => fn.version }
|
|
1225
|
+
}
|
|
1226
|
+
|
|
1227
|
+
output "lambda_source_code_hash" {
|
|
1228
|
+
description = "Base64-encoded SHA256 hash of the Lambda deployment package, shared by every operation"
|
|
1229
|
+
value = data.archive_file.lambda_zip.output_base64sha256
|
|
1230
|
+
}
|
|
1231
|
+
|
|
1232
|
+
# IAM Role Outputs, keyed by operation name
|
|
1233
|
+
output "lambda_execution_role_arns" {
|
|
1234
|
+
description = "ARN of each operation's Lambda execution role, keyed by operation name"
|
|
1235
|
+
value = { for op, role in aws_iam_role.lambda_execution_role : op => role.arn }
|
|
1236
|
+
}
|
|
1237
|
+
|
|
1238
|
+
output "lambda_execution_role_names" {
|
|
1239
|
+
description = "Name of each operation's Lambda execution role, keyed by operation name. Attach additional policies to a specific operation's role by name."
|
|
1240
|
+
value = { for op, role in aws_iam_role.lambda_execution_role : op => role.name }
|
|
1241
|
+
}
|
|
1242
|
+
|
|
1243
|
+
output "security_group_id" {
|
|
1244
|
+
description = "Security group ID shared by the API Lambda functions, for use in ingress rules on resources they must reach (e.g. a database). Null unless enable_vpc is true."
|
|
1245
|
+
value = var.enable_vpc ? aws_security_group.api_lambda[0].id : null
|
|
1246
|
+
}
|
|
1247
|
+
|
|
1248
|
+
# Integration Outputs, keyed by operation name
|
|
1249
|
+
output "integration_ids" {
|
|
1250
|
+
description = "ID of each operation's Lambda integration, keyed by operation name"
|
|
1251
|
+
value = { for op, i in aws_api_gateway_integration.lambda_integration : op => i.id }
|
|
1252
|
+
}
|
|
1253
|
+
|
|
1254
|
+
output "method_ids" {
|
|
1255
|
+
description = "ID of each operation's API Gateway method, keyed by operation name"
|
|
1256
|
+
value = { for op, m in aws_api_gateway_method.operation_methods : op => m.id }
|
|
1257
|
+
}
|
|
1258
|
+
|
|
1259
|
+
output "resource_ids" {
|
|
1260
|
+
description = "ID of each API Gateway resource, keyed by its path"
|
|
1261
|
+
value = local.resource_ids
|
|
1262
|
+
}
|
|
1263
|
+
|
|
1264
|
+
# CloudWatch Log Groups, keyed by operation name
|
|
1265
|
+
output "lambda_log_group_names" {
|
|
1266
|
+
description = "Name of each operation's Lambda CloudWatch log group, keyed by operation name"
|
|
1267
|
+
value = { for op, lg in aws_cloudwatch_log_group.lambda_logs : op => lg.name }
|
|
1268
|
+
}
|
|
1269
|
+
|
|
1270
|
+
output "lambda_log_group_arns" {
|
|
1271
|
+
description = "ARN of each operation's Lambda CloudWatch log group, keyed by operation name"
|
|
1272
|
+
value = { for op, lg in aws_cloudwatch_log_group.lambda_logs : op => lg.arn }
|
|
1273
|
+
}
|
|
1274
|
+
<%_ } else { _%>
|
|
849
1275
|
# Lambda Function Outputs
|
|
850
1276
|
output "lambda_function_name" {
|
|
851
1277
|
description = "Name of the Lambda function"
|
|
@@ -924,3 +1350,4 @@ output "lambda_log_group_arn" {
|
|
|
924
1350
|
description = "ARN of the Lambda CloudWatch log group"
|
|
925
1351
|
value = aws_cloudwatch_log_group.lambda_logs.arn
|
|
926
1352
|
}
|
|
1353
|
+
<%_ } _%>
|
|
@@ -9,9 +9,26 @@ export interface AddOpenApiMetadataGenerateTargetOptions {
|
|
|
9
9
|
apiNameKebabCase: string;
|
|
10
10
|
specPath: string;
|
|
11
11
|
specBuildTargetName: string;
|
|
12
|
+
/**
|
|
13
|
+
* How integrations are mapped to operations. Terraform only needs the
|
|
14
|
+
* operations metadata for the `isolated` pattern, since the `shared` pattern
|
|
15
|
+
* routes every operation to one proxy Lambda.
|
|
16
|
+
*/
|
|
17
|
+
integrationPattern?: 'isolated' | 'shared';
|
|
12
18
|
}
|
|
13
19
|
/**
|
|
14
|
-
* Adds a
|
|
15
|
-
*
|
|
20
|
+
* Adds a target which generates metadata about the API's operations from its
|
|
21
|
+
* OpenAPI specification, so that infrastructure can define an integration per
|
|
22
|
+
* operation.
|
|
23
|
+
*
|
|
24
|
+
* For CDK this generates TypeScript, giving the construct type-safe integrations.
|
|
25
|
+
* For Terraform this generates JSON, which the vended module reads to build one
|
|
26
|
+
* Lambda function and route per operation.
|
|
16
27
|
*/
|
|
17
|
-
export declare const addSharedConstructsOpenApiMetadataGenerateTarget: (tree: Tree,
|
|
28
|
+
export declare const addSharedConstructsOpenApiMetadataGenerateTarget: (tree: Tree, options: AddOpenApiMetadataGenerateTargetOptions) => void;
|
|
29
|
+
/**
|
|
30
|
+
* Directory the operations metadata for an API is generated into, relative to
|
|
31
|
+
* the shared Terraform project's `src`. The vended module reads the file from
|
|
32
|
+
* here, so the module template and this target must agree on the location.
|
|
33
|
+
*/
|
|
34
|
+
export declare const terraformOperationsMetadataDir: (apiNameKebabCase: string) => string;
|
|
@@ -4,16 +4,23 @@
|
|
|
4
4
|
*/ import { joinPathFragments, updateJson } from "@nx/devkit";
|
|
5
5
|
import { updateGitIgnore } from "../git.js";
|
|
6
6
|
import { addDependencyToTargetIfNotPresent } from "../nx.js";
|
|
7
|
-
import { PACKAGES_DIR, SHARED_CONSTRUCTS_DIR } from "../shared-constructs-constants.js";
|
|
7
|
+
import { PACKAGES_DIR, SHARED_CONSTRUCTS_DIR, SHARED_TERRAFORM_DIR } from "../shared-constructs-constants.js";
|
|
8
8
|
/**
|
|
9
|
-
* Adds a
|
|
10
|
-
*
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
9
|
+
* Adds a target which generates metadata about the API's operations from its
|
|
10
|
+
* OpenAPI specification, so that infrastructure can define an integration per
|
|
11
|
+
* operation.
|
|
12
|
+
*
|
|
13
|
+
* For CDK this generates TypeScript, giving the construct type-safe integrations.
|
|
14
|
+
* For Terraform this generates JSON, which the vended module reads to build one
|
|
15
|
+
* Lambda function and route per operation.
|
|
16
|
+
*/ export const addSharedConstructsOpenApiMetadataGenerateTarget = (tree, options)=>{
|
|
17
|
+
if (options.iac === 'cdk') {
|
|
18
|
+
addCdkOpenApiMetadataGenerateTarget(tree, options);
|
|
19
|
+
} else if (options.iac === 'terraform') {
|
|
20
|
+
addTerraformOpenApiOperationsGenerateTarget(tree, options);
|
|
16
21
|
}
|
|
22
|
+
};
|
|
23
|
+
const addCdkOpenApiMetadataGenerateTarget = (tree, { apiNameKebabCase, specPath, specBuildTargetName })=>{
|
|
17
24
|
const generatedMetadataDir = joinPathFragments('generated', apiNameKebabCase);
|
|
18
25
|
const generatedMetadataDirFromRoot = joinPathFragments(joinPathFragments(PACKAGES_DIR, SHARED_CONSTRUCTS_DIR), 'src', generatedMetadataDir);
|
|
19
26
|
updateJson(tree, joinPathFragments(PACKAGES_DIR, SHARED_CONSTRUCTS_DIR, 'project.json'), (config)=>{
|
|
@@ -58,5 +65,54 @@ import { PACKAGES_DIR, SHARED_CONSTRUCTS_DIR } from "../shared-constructs-consta
|
|
|
58
65
|
joinPathFragments('src', generatedMetadataDir)
|
|
59
66
|
]);
|
|
60
67
|
};
|
|
68
|
+
/**
|
|
69
|
+
* Directory the operations metadata for an API is generated into, relative to
|
|
70
|
+
* the shared Terraform project's `src`. The vended module reads the file from
|
|
71
|
+
* here, so the module template and this target must agree on the location.
|
|
72
|
+
*/ export const terraformOperationsMetadataDir = (apiNameKebabCase)=>joinPathFragments('generated', apiNameKebabCase);
|
|
73
|
+
const addTerraformOpenApiOperationsGenerateTarget = (tree, { apiNameKebabCase, specPath, specBuildTargetName, integrationPattern })=>{
|
|
74
|
+
// The `shared` pattern routes every operation to a single proxy Lambda, so the
|
|
75
|
+
// module has no need for the operations metadata.
|
|
76
|
+
if (integrationPattern !== 'isolated') {
|
|
77
|
+
return;
|
|
78
|
+
}
|
|
79
|
+
const generatedDirFromRoot = joinPathFragments(PACKAGES_DIR, SHARED_TERRAFORM_DIR, 'src', terraformOperationsMetadataDir(apiNameKebabCase));
|
|
80
|
+
updateJson(tree, joinPathFragments(PACKAGES_DIR, SHARED_TERRAFORM_DIR, 'project.json'), (config)=>{
|
|
81
|
+
config.targets ??= {};
|
|
82
|
+
// Terraform reads the operations metadata with `file()`, so it must exist
|
|
83
|
+
// before plan; the target is wired into `build` below.
|
|
84
|
+
const operationsTargetName = `generate:${apiNameKebabCase}-operations`;
|
|
85
|
+
if (!config.targets[operationsTargetName]) {
|
|
86
|
+
config.targets[operationsTargetName] = {
|
|
87
|
+
cache: true,
|
|
88
|
+
executor: 'nx:run-commands',
|
|
89
|
+
inputs: [
|
|
90
|
+
{
|
|
91
|
+
dependentTasksOutputFiles: '**/*.json'
|
|
92
|
+
}
|
|
93
|
+
],
|
|
94
|
+
outputs: [
|
|
95
|
+
joinPathFragments('{workspaceRoot}', generatedDirFromRoot)
|
|
96
|
+
],
|
|
97
|
+
options: {
|
|
98
|
+
commands: [
|
|
99
|
+
`nx g @aws/nx-plugin:open-api#json-metadata --openApiSpecPath="${specPath}" --outputPath="${generatedDirFromRoot}" --no-interactive`
|
|
100
|
+
]
|
|
101
|
+
},
|
|
102
|
+
dependsOn: [
|
|
103
|
+
specBuildTargetName
|
|
104
|
+
]
|
|
105
|
+
};
|
|
106
|
+
}
|
|
107
|
+
addDependencyToTargetIfNotPresent(config, 'build', operationsTargetName);
|
|
108
|
+
return config;
|
|
109
|
+
});
|
|
110
|
+
// Ignore the generated metadata by default
|
|
111
|
+
// Users can safely remove the entry from the .gitignore if they prefer to check it in
|
|
112
|
+
updateGitIgnore(tree, joinPathFragments(PACKAGES_DIR, SHARED_TERRAFORM_DIR), (patterns)=>[
|
|
113
|
+
...patterns,
|
|
114
|
+
joinPathFragments('src', terraformOperationsMetadataDir(apiNameKebabCase))
|
|
115
|
+
]);
|
|
116
|
+
};
|
|
61
117
|
|
|
62
118
|
//# sourceMappingURL=open-api-metadata.js.map
|