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.
- lambda_api_decorators_cdk/__init__.py +5 -0
- lambda_api_decorators_cdk/api_type.py +8 -0
- lambda_api_decorators_cdk/ast_helper.py +361 -0
- lambda_api_decorators_cdk/lambda_api.py +85 -0
- lambda_api_decorators_cdk/lambda_api_config.py +151 -0
- lambda_api_decorators_cdk/resource_builder.py +794 -0
- lambda_api_decorators_cdk/source_layout.py +8 -0
- lambda_api_decorators_cdk-0.2.3.dist-info/METADATA +144 -0
- lambda_api_decorators_cdk-0.2.3.dist-info/RECORD +12 -0
- lambda_api_decorators_cdk-0.2.3.dist-info/WHEEL +5 -0
- lambda_api_decorators_cdk-0.2.3.dist-info/licenses/LICENSE +21 -0
- lambda_api_decorators_cdk-0.2.3.dist-info/top_level.txt +1 -0
|
@@ -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)
|