lambda-api-decorators-cdk 0.2.3__py3-none-any.whl

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.
@@ -0,0 +1,794 @@
1
+ from aws_cdk import (
2
+ aws_ec2 as ec2,
3
+ aws_dynamodb as dynamodb,
4
+ aws_iam as iam,
5
+ Duration,
6
+ aws_apigateway as apigateway,
7
+ aws_apigatewayv2 as apigateway2,
8
+ aws_apigatewayv2_integrations as integrations,
9
+ aws_lambda as lambda_,
10
+ aws_s3 as s3,
11
+ aws_lambda_python_alpha as _lambda_python)
12
+ import hashlib
13
+ import os
14
+ from pathlib import Path
15
+ from lambda_api_decorators_cdk import ast_helper
16
+ from lambda_api_decorators_cdk.source_layout import SourceLayout
17
+ from typing import Optional, List, Dict
18
+
19
+ class ResourceBuilder():
20
+ '''
21
+ Entrypoint to lambda_api_decorators_cdk's functionality. Create a Builder object and set the custom requirements for your Lambda Functions.
22
+
23
+ Refer to the constructor method to get started instantiating a builder.
24
+
25
+ For more configuration options, use the set_default_XXXX, add_common_XXXX and add_custom_XXXX methods to add VPCs, Layers, Roles, Runtimes, Security Groups and Environment Variables.
26
+ '''
27
+
28
+ def __init__(self,
29
+ default_runtime: Optional[lambda_.Runtime] = None,
30
+ default_timeout: Optional[Duration] = None,
31
+ default_memory_size: Optional[int] = None,
32
+ default_vpc = None,
33
+ default_role: Optional[iam.Role] = None,
34
+ common_layers: Optional[List] = None,
35
+ common_security_groups: Optional[List[ec2.SecurityGroup]] = None,
36
+ common_environments: Optional[Dict[str, str]] = None,
37
+ custom_runtimes: Optional[Dict[str, lambda_.Runtime]] = None,
38
+ custom_roles: Optional[Dict[str, iam.Role]] = None,
39
+ custom_layers: Optional[Dict[str, None]] = None, ####TODO
40
+ custom_environments: Optional[Dict[str, str]] = None,
41
+ custom_security_groups: Optional[Dict[str, ec2.SecurityGroup]] = None,
42
+ custom_vpcs = None, ####TODO
43
+ dynamodb_tables: Optional[Dict[str, dynamodb.ITable]] = None,
44
+ s3_buckets: Optional[Dict[str, s3.IBucket]] = None,
45
+ ) -> 'ResourceBuilder':
46
+
47
+ '''
48
+ Creates base instance of a Resource Builder. By default, there are no predetermined custom, common nor default settings with the exception of the following custom runtimes: python3.8, python3.9, python.10, python.11, python.12.
49
+ You can optionally specify arguments (Keep in mind some of them are constructs of the aws_cdk toolkit) such as:
50
+ @param default_runtime
51
+ @param default_timeout
52
+ @param default_memory_size
53
+ @param default_vpc
54
+ @param default_role
55
+ @param common_layers
56
+ @param common_security_groups
57
+ @param common_environments
58
+ @param custom_runtimes
59
+ @param custom_roles
60
+ @param custom_layers
61
+ @param custom_environments
62
+ @param custom_security_groups
63
+ @param custom_vpcs
64
+ @param dynamodb_tables
65
+ @param s3_buckets
66
+ '''
67
+ # Check and set properties based on provided keyword arguments
68
+ self.default_runtime = default_runtime
69
+ self.default_timeout = default_timeout
70
+ self.default_memory_size = default_memory_size
71
+ self.default_vpc = default_vpc
72
+ self.default_role = default_role
73
+
74
+ self.common_layers = common_layers if common_layers is not None else []
75
+ self.common_security_groups = common_security_groups if common_security_groups is not None else []
76
+ self.common_environments = common_environments if common_environments is not None else {}
77
+
78
+ self.custom_runtimes = custom_runtimes if custom_runtimes is not None else {}
79
+ self.custom_roles = custom_roles if custom_roles is not None else {}
80
+ self.custom_layers = custom_layers if custom_layers is not None else {}
81
+ self.custom_environments = custom_environments if custom_environments is not None else {}
82
+ self.custom_security_groups = custom_security_groups if custom_security_groups is not None else {}
83
+ self.custom_vpcs = custom_vpcs if custom_vpcs is not None else {}
84
+ self.dynamodb_tables = dynamodb_tables if dynamodb_tables is not None else {}
85
+ self.s3_buckets = s3_buckets if s3_buckets is not None else {}
86
+ self._physical_dynamodb_tables = {}
87
+ self._physical_s3_buckets = {}
88
+
89
+ self.custom_runtimes.update({'python3.8':lambda_.Runtime.PYTHON_3_8})
90
+ self.custom_runtimes.update({'python3.9':lambda_.Runtime.PYTHON_3_9})
91
+ self.custom_runtimes.update({'python3.10':lambda_.Runtime.PYTHON_3_10})
92
+ self.custom_runtimes.update({'python3.11':lambda_.Runtime.PYTHON_3_11})
93
+ self.custom_runtimes.update({'python3.12':lambda_.Runtime.PYTHON_3_12})
94
+
95
+
96
+ #Setters
97
+ def set_default_runtime(self, runtime: lambda_.Runtime):
98
+ '''Set the default runtime every Lambda Function in the scope of the builder will have.'''
99
+ self.default_runtime = runtime
100
+
101
+ def set_default_timeout(self, timeout: Duration):
102
+ '''Set the default timeout every Lambda Function in the scope of the builder will have. If not specified, timeout will be CDK's default.'''
103
+ self.default_timeout = timeout
104
+
105
+ def set_default_memory_size(self, memory_size: int):
106
+ '''Set the default memory size every Lambda Function in the scope of the builder will have. If not specified, memory size will be CDK's default.'''
107
+ self.default_memory_size = memory_size
108
+
109
+ def set_default_vpc(self, vpc, vpc_subnets: list):
110
+ '''Set the default VPC and Subnets every Lambda Function in the scope of the builder will have. If not specified, none will be assigned to Lambda.'''
111
+ self.default_vpc = (vpc, vpc_subnets)
112
+
113
+ def set_default_role(self, role: iam.Role):
114
+ '''Set the default Role and permissions every Lambda Function in the scope of the builder will have. If not specified, CDK will create it's own for your Lambda.'''
115
+ self.default_role = role
116
+
117
+ #Adders
118
+ def add_common_layer(self, layer = lambda_.LayerVersion | _lambda_python.PythonLayerVersion):
119
+ '''Add a common layer for all your Lambda Functions.'''
120
+ self.common_layers.append(layer) if layer not in self.common_layers else None
121
+
122
+ def add_common_security_group(self, security_group):
123
+ '''Add a common security group for all your Lambda Functions.'''
124
+ self.common_security_groups.append(security_group) if security_group not in self.common_security_groups else None
125
+
126
+ def add_common_environment(self, key:str, value):
127
+ '''Add a common environment variable and value for all your Lambda Functions.'''
128
+ self.common_environments.update({key:value})
129
+
130
+ def add_custom_vpc(self, key: str, vpc: ec2.Vpc, vpc_subnets: list):
131
+ self.custom_vpcs.update({key:(vpc, vpc_subnets)})
132
+
133
+ def add_custom_environment(self, key: str, value: str | int | float):
134
+ '''Add a custom environment variable and value for every lambda function with the decorator @environment(key).'''
135
+ self.custom_environments.update({key:value})
136
+
137
+ def add_custom_runtime(self, key: str, value: lambda_.Runtime):
138
+ '''Add a custom Rruntime for every lambda function with the decorator @runtime(key).'''
139
+ self.custom_runtimes.update({key:value})
140
+
141
+ def add_custom_role(self, key: str, value: iam.Role):
142
+ '''Add a custom Role for every lambda function with the decorator @role(key).'''
143
+ self.custom_roles.update({key:value})
144
+
145
+ def add_custom_layer(self, key: str, value: lambda_.LayerVersion | _lambda_python.PythonLayerVersion):
146
+ '''Add a custom Layer for every lambda function with the decorator @layer(key).'''
147
+ self.custom_layers.update({key:value})
148
+
149
+ def add_custom_security_group(self, key: str, value: ec2.SecurityGroup):
150
+ '''Add a custom security group for every lambda function with the decorator @security_group(key).'''
151
+ self.custom_security_groups.update({key:value})
152
+
153
+
154
+ #Getters
155
+ def get_default_runtime(self) -> lambda_.Runtime | None:
156
+ return self.default_runtime
157
+
158
+ def get_default_timeout(self) -> Duration | None:
159
+ return self.default_timeout
160
+
161
+ def get_default_memory_size(self) -> int | None:
162
+ return self.default_memory_size
163
+
164
+ def get_default_vpc(self) -> tuple | None:
165
+ return self.default_vpc
166
+
167
+ def get_default_role(self) -> iam.Role | None:
168
+ return self.default_role
169
+
170
+ def get_common_layers(self) -> list | None:
171
+ return self.common_layers
172
+
173
+ def get_common_layer(self, value: str) -> lambda_.LayerVersion | _lambda_python.PythonLayerVersion:
174
+ return self.common_layers[value]
175
+
176
+ def get_common_security_groups(self) -> list | None:
177
+ return self.common_security_groups
178
+
179
+ def get_common_security_group(self, value: str):
180
+ return self.common_security_groups[value]
181
+
182
+ def get_common_environments(self):
183
+ return self.common_environments
184
+
185
+ def get_common_environment(self, value: str):
186
+ return self.common_environments[value]
187
+
188
+ def get_custom_layer(self, value: str) -> lambda_.LayerVersion | _lambda_python.PythonLayerVersion:
189
+ if value in self.custom_layers:
190
+ return self.custom_layers[value]
191
+ else: raise KeyError(f'Value {value} not previously declared as custom layer')
192
+
193
+ def get_custom_roles(self):
194
+ return self.custom_roles
195
+
196
+ def get_custom_role(self, value: str) -> iam.Role:
197
+ if value in self.custom_roles:
198
+ return self.custom_roles[value]
199
+ else: raise KeyError(f'Value {value} not previously declared as custom role')
200
+
201
+ def get_custom_security_group(self, value: str) -> ec2.SecurityGroup:
202
+ if value in self.custom_security_groups:
203
+ return self.custom_security_groups[value]
204
+ else: raise KeyError(f'Value {value} not previously declared as custom security group')
205
+
206
+ def get_custom_environment(self, value: str) -> str:
207
+ if value in self.custom_environments:
208
+ return self.custom_environments[value]
209
+ else: raise KeyError(f'Value {value} not previously declared as custom environment')
210
+
211
+ def get_custom_runtime(self, value: str) -> lambda_.Runtime:
212
+ if value in self.custom_runtimes:
213
+ return self.custom_runtimes[value]
214
+ else: raise KeyError(f'Value {value} not previously declared as custom runtime')
215
+
216
+ def get_custom_vpc(self, value: str) -> tuple:
217
+ #TODO
218
+ return self.custom_vpcs[value]
219
+
220
+ def build(self, construct, api_resource: apigateway.IResource, lambda_path:str,
221
+ print_tree: bool = False,
222
+ source_layout: SourceLayout = SourceLayout.ROOT,
223
+ layers_path: Optional[str] = None):
224
+ '''
225
+ Dynamically create Lambda Functions and Rest Api resources based on the options assigned to the builder.
226
+ @param construct: Stack which new resources and functions will be assigned to.
227
+ @param api_resource: REST API root resource from which the new resources/endpoints will be added.
228
+ @param lambda_path: Relative path (from cdk project workspace root dir) to the lambda functions defined.
229
+ @param print_tree: Optional value to output to terminal the API and functions built in a tree syntaxis.. Defaults to False.
230
+ '''
231
+
232
+ self._validate_source_layout(source_layout)
233
+ lambda_root = Path(lambda_path).resolve()
234
+ layer_sources = self._discover_layer_sources(layers_path)
235
+ graph = ast_helper.get_lambda_graph(str(lambda_root))
236
+ if print_tree:
237
+ ast_helper.dump_tree(graph)
238
+ self._prepare_layers(construct, graph, layer_sources)
239
+ self.build_from_graph(
240
+ construct, graph, api_resource, lambda_root, source_layout)
241
+
242
+ def build_http(self, construct, http_api: apigateway2.HttpApi,
243
+ lambda_path:str, print_tree: bool = False,
244
+ source_layout: SourceLayout = SourceLayout.ROOT,
245
+ layers_path: Optional[str] = None):
246
+ '''
247
+ Dynamically create Lambda Functions and Rest Api resources based on the options assigned to the builder.
248
+ @param construct: Stack which new resources and functions will be assigned to.
249
+ @param api_resource: REST API root resource from which the new resources/endpoints will be added.
250
+ @param lambda_path: Relative path (from cdk project workspace root dir) to the lambda functions defined.
251
+ @param print_tree: Optional value to output to terminal the API and functions built in a tree syntaxis.. Defaults to False.
252
+ '''
253
+ self._validate_source_layout(source_layout)
254
+ lambda_root = Path(lambda_path).resolve()
255
+ layer_sources = self._discover_layer_sources(layers_path)
256
+ graph = ast_helper.get_lambda_graph(str(lambda_root))
257
+ if print_tree:
258
+ ast_helper.dump_tree(graph)
259
+ self._prepare_layers(construct, graph, layer_sources)
260
+ self.build_http_from_graph(
261
+ construct, graph, http_api, lambda_root, source_layout)
262
+
263
+ @staticmethod
264
+ def _discover_layer_sources(layers_path: Optional[str]):
265
+ if layers_path is None:
266
+ return None
267
+
268
+ layers_root = Path(layers_path).resolve()
269
+ if not layers_root.exists():
270
+ raise ValueError(f"layers_path does not exist: {layers_root}")
271
+ if not layers_root.is_dir():
272
+ raise ValueError(f"layers_path is not a directory: {layers_root}")
273
+
274
+ return {
275
+ child.name: child
276
+ for child in sorted(layers_root.iterdir(), key=lambda path: path.name)
277
+ if not child.name.startswith('.')
278
+ and child.name != '__pycache__'
279
+ and child.is_dir()
280
+ }
281
+
282
+ @staticmethod
283
+ def _iter_methods(graph: ast_helper.Resource):
284
+ methods = []
285
+
286
+ def visit(resource):
287
+ for method in resource.get_methods():
288
+ methods.append((resource.get_path(), method))
289
+ for child in resource.get_connections():
290
+ visit(child)
291
+
292
+ visit(graph)
293
+ methods.sort(key=lambda item: (
294
+ item[1].get_path_to_file(),
295
+ item[1].get_file(),
296
+ item[1].get_handler(),
297
+ item[0],
298
+ item[1].get_method(),
299
+ ))
300
+ return [method for _, method in methods]
301
+
302
+ def _resolve_runtime(self, decorators: dict):
303
+ runtime = self.get_default_runtime()
304
+ if 'runtime' in decorators:
305
+ runtime = self.get_custom_runtime(decorators['runtime'])
306
+ return runtime
307
+
308
+ @staticmethod
309
+ def _runtime_name(runtime):
310
+ try:
311
+ name = runtime.name
312
+ except Exception as error:
313
+ raise ValueError("Runtime compatibility metadata is unreadable") from error
314
+ if not isinstance(name, str):
315
+ raise ValueError("Runtime compatibility metadata is unusable")
316
+ return name
317
+
318
+ def _validate_explicit_layer(self, layer, runtime, identifier):
319
+ try:
320
+ compatible_runtimes = layer.compatible_runtimes
321
+ except Exception as error:
322
+ raise ValueError(
323
+ f"Compatibility metadata for explicit layer {identifier!r} "
324
+ "is unreadable"
325
+ ) from error
326
+
327
+ if compatible_runtimes is None:
328
+ return
329
+ try:
330
+ declared_names = [
331
+ self._runtime_name(candidate) for candidate in compatible_runtimes
332
+ ]
333
+ except (TypeError, ValueError) as error:
334
+ raise ValueError(
335
+ f"Compatibility metadata for explicit layer {identifier!r} "
336
+ "is unusable"
337
+ ) from error
338
+
339
+ if runtime is None:
340
+ raise ValueError(
341
+ f"Explicit layer {identifier!r} requires a concrete Lambda runtime"
342
+ )
343
+ runtime_name = self._runtime_name(runtime)
344
+ if runtime_name not in declared_names:
345
+ raise ValueError(
346
+ f"Explicit layer {identifier!r} is not compatible with Lambda "
347
+ f"runtime {runtime_name!r}; declared compatible runtimes: "
348
+ f"{declared_names!r}"
349
+ )
350
+
351
+ def _prepare_layers(self, construct, graph, layer_sources):
352
+ if layer_sources is None:
353
+ return
354
+
355
+ explicit_layer_keys = set(self.custom_layers)
356
+ required_runtimes = {}
357
+ for method in self._iter_methods(graph):
358
+ decorators = self._configuration_decorators(method)
359
+ runtime = self._resolve_runtime(decorators)
360
+
361
+ for common_layer in self.common_layers:
362
+ identifier = getattr(
363
+ common_layer, 'layer_version_arn', 'common layer')
364
+ self._validate_explicit_layer(
365
+ common_layer, runtime, identifier)
366
+
367
+ requested_layers = decorators.get('layer', [])
368
+ if not isinstance(requested_layers, list):
369
+ requested_layers = [requested_layers]
370
+ for layer_key in requested_layers:
371
+ if layer_key in explicit_layer_keys:
372
+ self._validate_explicit_layer(
373
+ self.custom_layers[layer_key], runtime, layer_key)
374
+ elif layer_key in layer_sources:
375
+ if runtime is None:
376
+ raise ValueError(
377
+ f"Autodiscovered layer {layer_key!r} requires a "
378
+ "concrete Lambda runtime"
379
+ )
380
+ if runtime.family is not lambda_.RuntimeFamily.PYTHON:
381
+ raise ValueError(
382
+ f"Autodiscovered layer {layer_key!r} requires a "
383
+ "Python Lambda runtime"
384
+ )
385
+ runtime_name = self._runtime_name(runtime)
386
+ required_runtimes.setdefault(layer_key, {})\
387
+ .setdefault(runtime_name, runtime)
388
+ else:
389
+ self.get_custom_layer(layer_key)
390
+
391
+ for layer_key, entry in layer_sources.items():
392
+ if layer_key not in required_runtimes:
393
+ continue
394
+ layer = _lambda_python.PythonLayerVersion(
395
+ construct,
396
+ f"AutodiscoveredLayer:{layer_key}",
397
+ entry=str(entry),
398
+ compatible_runtimes=list(required_runtimes[layer_key].values()),
399
+ )
400
+ self.add_custom_layer(layer_key, layer)
401
+
402
+ @staticmethod
403
+ def _validate_source_layout(source_layout: SourceLayout):
404
+ if not isinstance(source_layout, SourceLayout):
405
+ raise TypeError("source_layout must be a SourceLayout")
406
+
407
+ @staticmethod
408
+ def _resolve_source(method: ast_helper.Method, lambda_root: Path,
409
+ source_layout: SourceLayout):
410
+ handler_path = (
411
+ Path(method.get_path_to_file()) / method.get_file()
412
+ ).resolve()
413
+ relative_handler = handler_path.relative_to(lambda_root)
414
+
415
+ if source_layout is SourceLayout.ROOT:
416
+ return lambda_root, relative_handler.as_posix()
417
+
418
+ if len(relative_handler.parts) < 2:
419
+ raise ValueError(
420
+ "SERVICE source layout requires handlers to live inside "
421
+ "a first-level service directory beneath lambda_path"
422
+ )
423
+ service = relative_handler.parts[0]
424
+ return (
425
+ lambda_root / service,
426
+ Path(*relative_handler.parts[1:]).as_posix(),
427
+ )
428
+
429
+ def get_options(self, decorators:dict) -> dict:
430
+ options = {}
431
+ options.update({'runtime':self._resolve_runtime(decorators)})
432
+ options.update({'memory_size': self.get_default_memory_size()})
433
+ options.update({'timeout': self.get_default_timeout()})
434
+ options.update({'role': self.get_default_role()})
435
+ options.update({'vpc':self.get_default_vpc()})
436
+ options.update({'environment':dict(self.get_common_environments())})
437
+ options.update({'layer': list(self.get_common_layers())})
438
+ options.update({'security_group':list(self.get_common_security_groups())})
439
+ #Optional values that may be or not be overriden
440
+ options.update({'description':None})
441
+ options.update({'name':None})
442
+ #Add defaults and let the decorators overwrite them (in case of defaults) or aggregate them (in case of common)
443
+ for key, value in decorators.items():
444
+ if key in ['memory_size','description','name']:
445
+ options.update({key: value})
446
+ elif key == 'runtime':
447
+ continue
448
+ elif key == 'timeout':
449
+ timeout = Duration.seconds(value)
450
+ options.update({key: timeout})
451
+ elif key == 'layer':
452
+ if type(value) == list:
453
+ for v in value:
454
+ options[key].append(self.get_custom_layer(v))
455
+ else:
456
+ options[key].append(self.get_custom_layer(value))
457
+ elif key == 'role':
458
+ options[key] = self.get_custom_role(value)
459
+ elif key == 'security_group':
460
+ if type(value) == list:
461
+ for v in value:
462
+ options[key].append(self.get_custom_security_group(v))
463
+ else:
464
+ options[key].append(self.get_custom_security_group(value))
465
+ elif key == 'environment':
466
+ if type(value) == list:
467
+ for v in value:
468
+ options[key][v] = self.get_custom_environment(v)
469
+ else:
470
+ options[key][value] = self.get_custom_environment(value)
471
+ elif key == 'vpc':
472
+ options[key] = self.get_custom_vpc(value)
473
+ return options
474
+
475
+ @staticmethod
476
+ def _configuration_decorators(method: ast_helper.Method) -> dict:
477
+ """Interpret ordered AST metadata using the established option semantics."""
478
+ decorators = {}
479
+ ignored = ast_helper.Method.ALLOWED_METHODS | {
480
+ 'grant_dynamodb', 'grant_s3', 'permission'}
481
+ for invocation in method.get_decorator_invocations():
482
+ if invocation.name in ignored or not invocation.args:
483
+ continue
484
+ value = (invocation.args[0] if len(invocation.args) == 1
485
+ else list(invocation.args))
486
+ if invocation.name in decorators:
487
+ current = decorators[invocation.name]
488
+ if not isinstance(current, list):
489
+ current = [current]
490
+ decorators[invocation.name] = current
491
+ current.extend(value if isinstance(value, list) else [value])
492
+ else:
493
+ decorators[invocation.name] = value
494
+ return decorators
495
+
496
+ @staticmethod
497
+ def _physical_resource_id(resource_type: str, physical_name: str) -> str:
498
+ """Create a stable, collision-safe construct ID for an imported resource."""
499
+ digest = hashlib.sha256(physical_name.encode("utf-8")).hexdigest()[:12]
500
+ return f"Permission{resource_type}:{digest}"
501
+
502
+ @staticmethod
503
+ def _find_imported_resource(construct, construct_id, resource_type,
504
+ name_attribute, physical_name):
505
+ """Find a compatible physical import already owned by this scope."""
506
+ resource = construct.node.try_find_child(construct_id)
507
+ if resource is None:
508
+ return None
509
+ if (
510
+ not isinstance(resource, resource_type)
511
+ or getattr(resource, name_attribute, None) != physical_name
512
+ ):
513
+ raise RuntimeError(
514
+ f"Construct {construct_id!r} already exists but is not the "
515
+ f"expected imported resource {physical_name!r}"
516
+ )
517
+ return resource
518
+
519
+ @staticmethod
520
+ def _grant_arguments(invocation, physical_field: str):
521
+ """Read a grant invocation whose public shape was validated by the AST."""
522
+ if invocation.args:
523
+ if len(invocation.args) != 2 or invocation.kwargs:
524
+ raise ValueError(f"Malformed {invocation.name} invocation")
525
+ resource_key, access = invocation.args
526
+ if access not in ("read", "write"):
527
+ raise ValueError(f"Malformed {invocation.name} invocation")
528
+ return resource_key, None, access
529
+
530
+ arguments = dict(invocation.kwargs)
531
+ resource_key = arguments.get("resource_key")
532
+ physical_name = arguments.get(physical_field)
533
+ access = arguments.get("access")
534
+ if (
535
+ access not in ("read", "write")
536
+ or (resource_key is None) == (physical_name is None)
537
+ ):
538
+ raise ValueError(f"Malformed {invocation.name} invocation")
539
+ return resource_key, physical_name, access
540
+
541
+ def _resolve_dynamodb_table(self, construct, resource_key, table_name):
542
+ if resource_key is not None:
543
+ try:
544
+ return self.dynamodb_tables[resource_key]
545
+ except KeyError:
546
+ raise KeyError(
547
+ f"DynamoDB table resource key {resource_key!r} is not registered"
548
+ ) from None
549
+
550
+ cache_key = (construct, table_name)
551
+ if cache_key not in self._physical_dynamodb_tables:
552
+ construct_id = self._physical_resource_id(
553
+ "DynamoDBTable", table_name)
554
+ table = self._find_imported_resource(
555
+ construct,
556
+ construct_id,
557
+ dynamodb.TableBase,
558
+ "table_name",
559
+ table_name,
560
+ )
561
+ if table is None:
562
+ table = dynamodb.Table.from_table_attributes(
563
+ construct,
564
+ construct_id,
565
+ table_name=table_name,
566
+ grant_index_permissions=True,
567
+ )
568
+ self._physical_dynamodb_tables[cache_key] = table
569
+ return self._physical_dynamodb_tables[cache_key]
570
+
571
+ def _resolve_s3_bucket(self, construct, resource_key, bucket_name):
572
+ if resource_key is not None:
573
+ try:
574
+ return self.s3_buckets[resource_key]
575
+ except KeyError:
576
+ raise KeyError(
577
+ f"S3 bucket resource key {resource_key!r} is not registered"
578
+ ) from None
579
+
580
+ cache_key = (construct, bucket_name)
581
+ if cache_key not in self._physical_s3_buckets:
582
+ construct_id = self._physical_resource_id("S3Bucket", bucket_name)
583
+ bucket = self._find_imported_resource(
584
+ construct,
585
+ construct_id,
586
+ s3.BucketBase,
587
+ "bucket_name",
588
+ bucket_name,
589
+ )
590
+ if bucket is None:
591
+ bucket = s3.Bucket.from_bucket_name(
592
+ construct, construct_id, bucket_name)
593
+ self._physical_s3_buckets[cache_key] = bucket
594
+ return self._physical_s3_buckets[cache_key]
595
+
596
+ @staticmethod
597
+ def _ensure_permission_applied(applied, permission_kind):
598
+ if not applied:
599
+ raise RuntimeError(
600
+ f"{permission_kind} permission could not be applied because "
601
+ "the selected execution role cannot accept policy mutations"
602
+ )
603
+
604
+ @classmethod
605
+ def _ensure_role_policy_applied(cls, function, statement,
606
+ permission_kind):
607
+ result = function.role.add_to_principal_policy(statement)
608
+ cls._ensure_permission_applied(
609
+ result.statement_added and any(
610
+ isinstance(child, iam.Policy)
611
+ for child in function.role.node.children
612
+ ),
613
+ permission_kind,
614
+ )
615
+
616
+ @classmethod
617
+ def _ensure_grant_applied(cls, function, grant, permission_kind):
618
+ # Narrow test doubles used by existing callers may not model CDK Grant.
619
+ if grant is None:
620
+ return
621
+ cls._ensure_permission_applied(
622
+ grant.success and any(
623
+ isinstance(child, iam.Policy)
624
+ for child in function.role.node.children
625
+ ),
626
+ permission_kind,
627
+ )
628
+
629
+ def _apply_dynamodb_grant(self, construct, function, invocation):
630
+ resource_key, table_name, access = self._grant_arguments(
631
+ invocation, "table_name"
632
+ )
633
+ table = self._resolve_dynamodb_table(construct, resource_key, table_name)
634
+ if access == "read":
635
+ grant = table.grant_read_data(function)
636
+ else:
637
+ grant = table.grant_read_write_data(function)
638
+ self._ensure_grant_applied(function, grant, invocation.name)
639
+
640
+ def _apply_s3_grant(self, construct, function, invocation):
641
+ resource_key, bucket_name, access = self._grant_arguments(
642
+ invocation, "bucket_name"
643
+ )
644
+ bucket = self._resolve_s3_bucket(construct, resource_key, bucket_name)
645
+ if access == "read":
646
+ grant = bucket.grant_read(function)
647
+ else:
648
+ grant = bucket.grant_read_write(function)
649
+ self._ensure_grant_applied(function, grant, invocation.name)
650
+
651
+ @staticmethod
652
+ def _apply_generic_permission(function, invocation):
653
+ if invocation.args:
654
+ raise ValueError("Malformed permission invocation")
655
+ arguments = dict(invocation.kwargs)
656
+ try:
657
+ actions = list(arguments["actions"])
658
+ resources = list(arguments["resources"])
659
+ except (KeyError, TypeError):
660
+ raise ValueError("Malformed permission invocation") from None
661
+ statement = iam.PolicyStatement(
662
+ effect=iam.Effect.ALLOW,
663
+ actions=actions,
664
+ resources=resources,
665
+ )
666
+ ResourceBuilder._ensure_role_policy_applied(
667
+ function, statement, invocation.name)
668
+
669
+ def _apply_permissions(self, construct, function, method):
670
+ """Apply each ordered permission invocation to a created function once."""
671
+ for invocation in method.get_decorator_invocations():
672
+ if invocation.name == "grant_dynamodb":
673
+ self._apply_dynamodb_grant(construct, function, invocation)
674
+ elif invocation.name == "grant_s3":
675
+ self._apply_s3_grant(construct, function, invocation)
676
+ elif invocation.name == "permission":
677
+ self._apply_generic_permission(function, invocation)
678
+
679
+ def build_lambda_function(self, construct, method: ast_helper.Method,
680
+ lambda_root: Optional[Path] = None,
681
+ source_layout: SourceLayout = SourceLayout.ROOT):
682
+ # Create Lambda function with aggregated metadata from all decorators
683
+
684
+ logical_id = method.get_logical_id()
685
+ handler = method.get_handler()
686
+ if lambda_root is None:
687
+ file = method.get_file()
688
+ entry_path = method.get_path_to_file()
689
+ else:
690
+ entry_path, file = self._resolve_source(
691
+ method, lambda_root, source_layout)
692
+ entry_path = str(entry_path)
693
+ options = self.get_options(self._configuration_decorators(method))
694
+ vpc_options = options['vpc']
695
+ vpc = vpc_options[0] if vpc_options is not None else None
696
+ vpc_subnets = vpc_options[1] if vpc_options is not None else None
697
+ lambda_function = _lambda_python.PythonFunction(
698
+ construct, logical_id,
699
+ function_name = options['name'] if options['name'] else logical_id,
700
+ description = options['description'],
701
+ entry = entry_path,
702
+ index = file,
703
+ handler = handler,
704
+ runtime = options['runtime'],
705
+ timeout = options['timeout'],
706
+ layers = options['layer'],
707
+ memory_size=options['memory_size'],
708
+ security_groups= options['security_group'],
709
+ vpc=vpc,
710
+ vpc_subnets=vpc_subnets,
711
+ allow_public_subnet=False,
712
+ environment= options['environment'],
713
+ role= options['role']
714
+ )
715
+ self._apply_permissions(construct, lambda_function, method)
716
+ return lambda_function
717
+
718
+ def _build_discovered_lambda(self, construct, method, lambda_root,
719
+ source_layout):
720
+ if lambda_root is None:
721
+ return self.build_lambda_function(construct, method)
722
+ return self.build_lambda_function(
723
+ construct, method, lambda_root, source_layout)
724
+
725
+ def build_from_graph(self, construct, graph: ast_helper.Resource,
726
+ api_resource: apigateway.IResource,
727
+ lambda_root: Optional[Path] = None,
728
+ source_layout: SourceLayout = SourceLayout.ROOT):
729
+
730
+ path = graph.get_path()
731
+ level = path.count('/')
732
+ if level <= 1 and len(path) <= 1: #root '/'
733
+ new_resource = api_resource
734
+ for method in graph.get_methods():
735
+ lbda = self._build_discovered_lambda(
736
+ construct, method, lambda_root, source_layout)
737
+ new_resource.add_method(method.get_method(), apigateway.LambdaIntegration(lbda))
738
+ else:
739
+ #We can get a skip from /something to /something/one/two/method, so resources with no methods "one" and "two" should be created
740
+ new_api_resources = path[len(api_resource.path):].lstrip('/').split('/')
741
+ if len(new_api_resources) > 1: #Resources with no methods associated need to be created. No possible conflict because graph is sorted.
742
+ for res in new_api_resources[:-1]: # Exclude last resource that will be created w/lambda
743
+ api_resource = api_resource.add_resource(res)
744
+ resource_name = path[path.rindex('/')+1:] #Now we can create the resource associated with the node even if
745
+ new_resource = api_resource.add_resource(resource_name)
746
+ for method in graph.get_methods():
747
+ lbda = self._build_discovered_lambda(
748
+ construct, method, lambda_root, source_layout)
749
+ new_resource.add_method(method.get_method(), apigateway.LambdaIntegration(lbda))
750
+
751
+ for node in graph.get_connections():
752
+ self.build_from_graph(
753
+ construct, node, new_resource, lambda_root, source_layout)
754
+
755
+ def build_http_from_graph(self, construct, graph: ast_helper.Resource,
756
+ http_api: apigateway2.HttpApi,
757
+ lambda_root: Optional[Path] = None,
758
+ source_layout: SourceLayout = SourceLayout.ROOT):
759
+ method_mapping = {
760
+ 'GET': apigateway2.HttpMethod.GET,
761
+ 'POST': apigateway2.HttpMethod.POST,
762
+ 'PUT': apigateway2.HttpMethod.PUT,
763
+ 'DELETE': apigateway2.HttpMethod.DELETE,
764
+ 'PATCH': apigateway2.HttpMethod.PATCH,
765
+ 'OPTIONS': apigateway2.HttpMethod.OPTIONS,
766
+ 'HEAD': apigateway2.HttpMethod.HEAD,
767
+ }
768
+ path = graph.get_path()
769
+ level = path.count('/')
770
+ if level <= 1 and len(path) <= 1: #root '/'
771
+ # new_resource = api_resource
772
+ for method in graph.get_methods():
773
+ lbda = self._build_discovered_lambda(
774
+ construct, method, lambda_root, source_layout)
775
+ api_lbda_integration = integrations.HttpLambdaIntegration(f"{method.get_logical_id()}ApiLambdaIntegration",lbda)
776
+ http_api.add_routes(
777
+ path='/',
778
+ methods=[method_mapping[method.get_method()]],
779
+ integration= api_lbda_integration
780
+ )
781
+ else:
782
+ for method in graph.get_methods():
783
+ lbda = self._build_discovered_lambda(
784
+ construct, method, lambda_root, source_layout)
785
+ api_lbda_integration = integrations.HttpLambdaIntegration(f"{method.get_logical_id()}ApiLambdaIntegration",lbda)
786
+ http_api.add_routes(
787
+ path=path,
788
+ methods=[method_mapping[method.get_method()]],
789
+ integration= api_lbda_integration
790
+ )
791
+
792
+ for node in graph.get_connections():
793
+ self.build_http_from_graph(
794
+ construct, node, http_api, lambda_root, source_layout)