|
| 1 | +importcopy |
1 | 2 | importjson
|
2 | 3 | importlogging
|
3 | 4 | importos
|
| 5 | +importre |
4 | 6 | fromcopyimportdeepcopy
|
5 |
| -fromtypingimportDict,Optional,Type,Union |
| 7 | +fromdataclassesimportdataclass |
| 8 | +fromtypingimportAny,Callable,Dict,Optional,Type,Union |
6 | 9 |
|
7 | 10 | importboto3
|
8 | 11 | frombotocore.exceptionsimportClientError
|
|
12 | 15 | fromlocalstack.aws.connectimportconnect_to
|
13 | 16 | fromlocalstack.services.cloudformation.engine.policy_loaderimportcreate_policy_loader
|
14 | 17 | fromlocalstack.services.cloudformation.engine.template_deployerimportresolve_refs_recursively
|
| 18 | +fromlocalstack.services.cloudformation.engine.validationsimportValidationError |
15 | 19 | fromlocalstack.services.cloudformation.storesimportget_cloudformation_store
|
16 | 20 | fromlocalstack.utilsimporttestutil
|
17 | 21 | fromlocalstack.utils.objectsimportrecurse_object
|
|
26 | 30 | TransformResult=Union[dict,str]
|
27 | 31 |
|
28 | 32 |
|
| 33 | +@dataclass |
| 34 | +classResolveRefsRecursivelyContext: |
| 35 | +account_id:str |
| 36 | +region_name:str |
| 37 | +stack_name:str |
| 38 | +resources:dict |
| 39 | +mappings:dict |
| 40 | +conditions:dict |
| 41 | +parameters:dict |
| 42 | + |
| 43 | +defresolve(self,value:Any)->Any: |
| 44 | +returnresolve_refs_recursively( |
| 45 | +self.account_id, |
| 46 | +self.region_name, |
| 47 | +self.stack_name, |
| 48 | +self.resources, |
| 49 | +self.mappings, |
| 50 | +self.conditions, |
| 51 | +self.parameters, |
| 52 | +value, |
| 53 | + ) |
| 54 | + |
| 55 | + |
29 | 56 | classTransformer:
|
30 | 57 | """Abstract class for Fn::Transform intrinsic functions"""
|
31 | 58 |
|
@@ -155,7 +182,20 @@ def apply_global_transformations(
|
155 | 182 | account_id,region_name,processed_template,stack_parameters
|
156 | 183 | )
|
157 | 184 | eliftransformation["Name"]==EXTENSIONS_TRANSFORM:
|
158 |
| -continue |
| 185 | +resolve_context=ResolveRefsRecursivelyContext( |
| 186 | +account_id, |
| 187 | +region_name, |
| 188 | +stack_name, |
| 189 | +resources, |
| 190 | +mappings, |
| 191 | +conditions, |
| 192 | +stack_parameters, |
| 193 | + ) |
| 194 | + |
| 195 | +processed_template=apply_language_extensions_transform( |
| 196 | +processed_template, |
| 197 | +resolve_context, |
| 198 | + ) |
159 | 199 | eliftransformation["Name"]==SECRETSMANAGER_TRANSFORM:
|
160 | 200 | # https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/transform-aws-secretsmanager.html
|
161 | 201 | LOG.warning("%s is not yet supported. Ignoring.",SECRETSMANAGER_TRANSFORM)
|
@@ -269,6 +309,144 @@ def execute_macro(
|
269 | 309 | returnresult.get("fragment")
|
270 | 310 |
|
271 | 311 |
|
| 312 | +defapply_language_extensions_transform( |
| 313 | +template:dict, |
| 314 | +resolve_context:ResolveRefsRecursivelyContext, |
| 315 | +)->dict: |
| 316 | +""" |
| 317 | + Resolve language extensions constructs |
| 318 | + """ |
| 319 | + |
| 320 | +def_visit(obj,path,**_): |
| 321 | +# Fn::ForEach |
| 322 | +# TODO: can this be used in non-resource positions? |
| 323 | +ifisinstance(obj,dict)andany("Fn::ForEach"inkeyforkeyinobj): |
| 324 | +newobj= {} |
| 325 | +forkeyinobj: |
| 326 | +if"Fn::ForEach"notinkey: |
| 327 | +newobj[key]=obj[key] |
| 328 | +continue |
| 329 | + |
| 330 | +new_entries=expand_fn_foreach(obj[key],resolve_context) |
| 331 | +newobj.update(**new_entries) |
| 332 | +returnnewobj |
| 333 | +# Fn::Length |
| 334 | +elifisinstance(obj,dict)and"Fn::Length"inobj: |
| 335 | +value=obj["Fn::Length"] |
| 336 | +ifisinstance(value,dict): |
| 337 | +value=resolve_context.resolve(value) |
| 338 | + |
| 339 | +ifisinstance(value,list): |
| 340 | +# TODO: what if one of the elements was AWS::NoValue? |
| 341 | +# no conversion required |
| 342 | +returnlen(value) |
| 343 | +elifisinstance(value,str): |
| 344 | +length=len(value.split(",")) |
| 345 | +returnlength |
| 346 | +returnobj |
| 347 | +elifisinstance(obj,dict)and"Fn::ToJsonString"inobj: |
| 348 | +# TODO: is the default representation ok here? |
| 349 | +returnjson.dumps(obj["Fn::ToJsonString"],default=str,separators=(",",":")) |
| 350 | + |
| 351 | +# reference |
| 352 | +returnobj |
| 353 | + |
| 354 | +returnrecurse_object(template,_visit) |
| 355 | + |
| 356 | + |
| 357 | +defexpand_fn_foreach( |
| 358 | +foreach_defn:list, |
| 359 | +resolve_context:ResolveRefsRecursivelyContext, |
| 360 | +extra_replace_mapping:dict|None=None, |
| 361 | +)->dict: |
| 362 | +iflen(foreach_defn)!=3: |
| 363 | +raiseValidationError( |
| 364 | +f"Fn::ForEach: invalid number of arguments, expected 3 got{len(foreach_defn)}" |
| 365 | + ) |
| 366 | +output= {} |
| 367 | +iteration_name,iteration_value,template=foreach_defn |
| 368 | +ifnotisinstance(iteration_name,str): |
| 369 | +raiseValidationError( |
| 370 | +f"Fn::ForEach: incorrect type for iteration name '{iteration_name}', expected str" |
| 371 | + ) |
| 372 | +ifisinstance(iteration_value,dict): |
| 373 | +# we have a reference |
| 374 | +if"Ref"initeration_value: |
| 375 | +iteration_value=resolve_context.resolve(iteration_value) |
| 376 | +else: |
| 377 | +raiseNotImplementedError( |
| 378 | +f"Fn::Transform: intrinsic{iteration_value} not supported in this position yet" |
| 379 | + ) |
| 380 | +ifnotisinstance(iteration_value,list): |
| 381 | +raiseValidationError( |
| 382 | +f"Fn::ForEach: incorrect type for iteration variables '{iteration_value}', expected list" |
| 383 | + ) |
| 384 | + |
| 385 | +ifnotisinstance(template,dict): |
| 386 | +raiseValidationError( |
| 387 | +f"Fn::ForEach: incorrect type for template '{template}', expected dict" |
| 388 | + ) |
| 389 | + |
| 390 | +# TODO: locations other than resources |
| 391 | +replace_template_value="${"+iteration_name+"}" |
| 392 | +forvariableiniteration_value: |
| 393 | +# there might be multiple children, which could themselves be a `Fn::ForEach` call |
| 394 | +forlogical_resource_id_templateintemplate: |
| 395 | +iflogical_resource_id_template.startswith("Fn::ForEach"): |
| 396 | +result=expand_fn_foreach( |
| 397 | +template[logical_resource_id_template], |
| 398 | +resolve_context, |
| 399 | + {iteration_name:variable}, |
| 400 | + ) |
| 401 | +output.update(**result) |
| 402 | +continue |
| 403 | + |
| 404 | +ifreplace_template_valuenotinlogical_resource_id_template: |
| 405 | +raiseValidationError("Fn::ForEach: no placeholder in logical resource id") |
| 406 | + |
| 407 | +defgen_visit(variable:str)->Callable: |
| 408 | +def_visit(obj:Any,path:Any): |
| 409 | +ifisinstance(obj,dict)and"Ref"inobj: |
| 410 | +ref_variable=obj["Ref"] |
| 411 | +ifref_variable==iteration_name: |
| 412 | +returnvariable |
| 413 | +elifisinstance(obj,dict)and"Fn::Sub"inobj: |
| 414 | +arguments=recurse_object(obj["Fn::Sub"],_visit) |
| 415 | +ifisinstance(arguments,str): |
| 416 | +# simple case |
| 417 | +# TODO: can this reference anything outside of the template? |
| 418 | +result=arguments |
| 419 | +variables_found=re.findall("\\${([^}]+)}",arguments) |
| 420 | +forvarinvariables_found: |
| 421 | +ifvar==iteration_name: |
| 422 | +result=result.replace(f"${{{var}}}",variable) |
| 423 | +returnresult |
| 424 | +else: |
| 425 | +raiseNotImplementedError |
| 426 | +elifisinstance(obj,dict)and"Fn::Join"inobj: |
| 427 | +# first visit arguments |
| 428 | +arguments=recurse_object( |
| 429 | +obj["Fn::Join"], |
| 430 | +_visit, |
| 431 | + ) |
| 432 | +separator,items=arguments |
| 433 | +returnseparator.join(items) |
| 434 | +returnobj |
| 435 | + |
| 436 | +return_visit |
| 437 | + |
| 438 | +logical_resource_id=logical_resource_id_template.replace( |
| 439 | +replace_template_value,variable |
| 440 | + ) |
| 441 | +forkey,valuein (extra_replace_mappingor {}).items(): |
| 442 | +logical_resource_id=logical_resource_id.replace("${"+key+"}",value) |
| 443 | +resource_body=copy.deepcopy(template[logical_resource_id_template]) |
| 444 | +body=recurse_object(resource_body,gen_visit(variable)) |
| 445 | +output[logical_resource_id]=body |
| 446 | + |
| 447 | +returnoutput |
| 448 | + |
| 449 | + |
272 | 450 | defapply_serverless_transformation(
|
273 | 451 | account_id:str,region_name:str,parsed_template:dict,template_parameters:dict
|
274 | 452 | )->Optional[str]:
|
|