@@ -113,6 +113,7 @@ class ChangeSetTerminal(ChangeSetEntity, abc.ABC): ...
113
113
114
114
115
115
class NodeTemplate (ChangeSetNode ):
116
+ mappings : Final [NodeMappings ]
116
117
parameters : Final [NodeParameters ]
117
118
conditions : Final [NodeConditions ]
118
119
resources : Final [NodeResources ]
@@ -121,11 +122,13 @@ def __init__(
121
122
self ,
122
123
scope : Scope ,
123
124
change_type : ChangeType ,
125
+ mappings : NodeMappings ,
124
126
parameters : NodeParameters ,
125
127
conditions : NodeConditions ,
126
128
resources : NodeResources ,
127
129
):
128
130
super ().__init__ (scope = scope , change_type = change_type )
131
+ self .mappings = mappings
129
132
self .parameters = parameters
130
133
self .conditions = conditions
131
134
self .resources = resources
@@ -168,6 +171,24 @@ def __init__(self, scope: Scope, change_type: ChangeType, parameters: list[NodeP
168
171
self .parameters = parameters
169
172
170
173
174
+ class NodeMapping (ChangeSetNode ):
175
+ name : Final [str ]
176
+ bindings : Final [NodeObject ]
177
+
178
+ def __init__ (self , scope : Scope , change_type : ChangeType , name : str , bindings : NodeObject ):
179
+ super ().__init__ (scope = scope , change_type = change_type )
180
+ self .name = name
181
+ self .bindings = bindings
182
+
183
+
184
+ class NodeMappings (ChangeSetNode ):
185
+ mappings : Final [list [NodeMapping ]]
186
+
187
+ def __init__ (self , scope : Scope , change_type : ChangeType , mappings : list [NodeMapping ]):
188
+ super ().__init__ (scope = scope , change_type = change_type )
189
+ self .mappings = mappings
190
+
191
+
171
192
class NodeCondition (ChangeSetNode ):
172
193
name : Final [str ]
173
194
body : Final [ChangeSetEntity ]
@@ -300,6 +321,7 @@ def __init__(self, scope: Scope, value: Any):
300
321
TypeKey : Final [str ] = "Type"
301
322
ConditionKey : Final [str ] = "Condition"
302
323
ConditionsKey : Final [str ] = "Conditions"
324
+ MappingsKey : Final [str ] = "Mappings"
303
325
ResourcesKey : Final [str ] = "Resources"
304
326
PropertiesKey : Final [str ] = "Properties"
305
327
ParametersKey : Final [str ] = "Parameters"
@@ -309,7 +331,15 @@ def __init__(self, scope: Scope, value: Any):
309
331
FnNot : Final [str ] = "Fn::Not"
310
332
FnGetAttKey : Final [str ] = "Fn::GetAtt"
311
333
FnEqualsKey : Final [str ] = "Fn::Equals"
312
- INTRINSIC_FUNCTIONS : Final [set [str ]] = {RefKey , FnIf , FnNot , FnEqualsKey , FnGetAttKey }
334
+ FnFindInMapKey : Final [str ] = "Fn::FindInMap"
335
+ INTRINSIC_FUNCTIONS : Final [set [str ]] = {
336
+ RefKey ,
337
+ FnIf ,
338
+ FnNot ,
339
+ FnEqualsKey ,
340
+ FnGetAttKey ,
341
+ FnFindInMapKey ,
342
+ }
313
343
314
344
315
345
class ChangeSetModel :
@@ -455,6 +485,36 @@ def _resolve_intrinsic_function_ref(self, arguments: ChangeSetEntity) -> ChangeT
455
485
node_resource = self ._retrieve_or_visit_resource (resource_name = logical_id )
456
486
return node_resource .change_type
457
487
488
+ def _resolve_intrinsic_function_fn_find_in_map (self , arguments : ChangeSetEntity ) -> ChangeType :
489
+ if arguments .change_type != ChangeType .UNCHANGED :
490
+ return arguments .change_type
491
+ # TODO: validate arguments structure and type.
492
+ # TODO: add support for nested functions, here we assume the arguments are string literals.
493
+
494
+ if not isinstance (arguments , NodeArray ) or not arguments .array :
495
+ raise RuntimeError ()
496
+ argument_mapping_name = arguments .array [0 ]
497
+ if not isinstance (argument_mapping_name , TerminalValue ):
498
+ raise NotImplementedError ()
499
+ argument_top_level_key = arguments .array [1 ]
500
+ if not isinstance (argument_top_level_key , TerminalValue ):
501
+ raise NotImplementedError ()
502
+ argument_second_level_key = arguments .array [2 ]
503
+ if not isinstance (argument_second_level_key , TerminalValue ):
504
+ raise NotImplementedError ()
505
+ mapping_name = argument_mapping_name .value
506
+ top_level_key = argument_top_level_key .value
507
+ second_level_key = argument_second_level_key .value
508
+
509
+ node_mapping = self ._retrieve_mapping (mapping_name = mapping_name )
510
+ # TODO: a lookup would be beneficial in this scenario too;
511
+ # consider implications downstream and for replication.
512
+ top_level_object = node_mapping .bindings .bindings .get (top_level_key )
513
+ if not isinstance (top_level_object , NodeObject ):
514
+ raise RuntimeError ()
515
+ target_map_value = top_level_object .bindings .get (second_level_key )
516
+ return target_map_value .change_type
517
+
458
518
def _resolve_intrinsic_function_fn_if (self , arguments : ChangeSetEntity ) -> ChangeType :
459
519
# TODO: validate arguments structure and type.
460
520
if not isinstance (arguments , NodeArray ) or not arguments .array :
@@ -705,6 +765,36 @@ def _visit_resources(
705
765
change_type = change_type .for_child (resource .change_type )
706
766
return NodeResources (scope = scope , change_type = change_type , resources = resources )
707
767
768
+ def _visit_mapping (
769
+ self , scope : Scope , name : str , before_mapping : Maybe [dict ], after_mapping : Maybe [dict ]
770
+ ) -> NodeMapping :
771
+ bindings = self ._visit_object (
772
+ scope = scope , before_object = before_mapping , after_object = after_mapping
773
+ )
774
+ return NodeMapping (
775
+ scope = scope , change_type = bindings .change_type , name = name , bindings = bindings
776
+ )
777
+
778
+ def _visit_mappings (
779
+ self , scope : Scope , before_mappings : Maybe [dict ], after_mappings : Maybe [dict ]
780
+ ) -> NodeMappings :
781
+ change_type = ChangeType .UNCHANGED
782
+ mappings : list [NodeMapping ] = list ()
783
+ mapping_names = self ._safe_keys_of (before_mappings , after_mappings )
784
+ for mapping_name in mapping_names :
785
+ scope_mapping , (before_mapping , after_mapping ) = self ._safe_access_in (
786
+ scope , mapping_name , before_mappings , after_mappings
787
+ )
788
+ mapping = self ._visit_mapping (
789
+ scope = scope ,
790
+ name = mapping_name ,
791
+ before_mapping = before_mapping ,
792
+ after_mapping = after_mapping ,
793
+ )
794
+ mappings .append (mapping )
795
+ change_type = change_type .for_child (mapping .change_type )
796
+ return NodeMappings (scope = scope , change_type = change_type , mappings = mappings )
797
+
708
798
def _visit_dynamic_parameter (self , parameter_name : str ) -> ChangeSetEntity :
709
799
scope = Scope ("Dynamic" ).open_scope ("Parameters" )
710
800
scope_parameter , (before_parameter , after_parameter ) = self ._safe_access_in (
@@ -845,6 +935,14 @@ def _visit_conditions(
845
935
def _model (self , before_template : Maybe [dict ], after_template : Maybe [dict ]) -> NodeTemplate :
846
936
root_scope = Scope ()
847
937
# TODO: visit other child types
938
+
939
+ mappings_scope , (before_mappings , after_mappings ) = self ._safe_access_in (
940
+ root_scope , MappingsKey , before_template , after_template
941
+ )
942
+ mappings = self ._visit_mappings (
943
+ scope = mappings_scope , before_mappings = before_mappings , after_mappings = after_mappings
944
+ )
945
+
848
946
parameters_scope , (before_parameters , after_parameters ) = self ._safe_access_in (
849
947
root_scope , ParametersKey , before_template , after_template
850
948
)
@@ -876,6 +974,7 @@ def _model(self, before_template: Maybe[dict], after_template: Maybe[dict]) -> N
876
974
return NodeTemplate (
877
975
scope = root_scope ,
878
976
change_type = resources .change_type ,
977
+ mappings = mappings ,
879
978
parameters = parameters ,
880
979
conditions = conditions ,
881
980
resources = resources ,
@@ -919,6 +1018,23 @@ def _retrieve_parameter_if_exists(self, parameter_name: str) -> Optional[NodePar
919
1018
return node_parameter
920
1019
return None
921
1020
1021
+ def _retrieve_mapping (self , mapping_name ) -> NodeMapping :
1022
+ # TODO: add caching mechanism, and raise appropriate error if missing.
1023
+ scope_mappings , (before_mappings , after_mappings ) = self ._safe_access_in (
1024
+ Scope (), MappingsKey , self ._before_template , self ._after_template
1025
+ )
1026
+ before_mappings = before_mappings or dict ()
1027
+ after_mappings = after_mappings or dict ()
1028
+ if mapping_name in before_mappings or mapping_name in after_mappings :
1029
+ scope_mapping , (before_mapping , after_mapping ) = self ._safe_access_in (
1030
+ scope_mappings , mapping_name , before_mappings , after_mappings
1031
+ )
1032
+ node_mapping = self ._visit_mapping (
1033
+ scope_mapping , mapping_name , before_mapping , after_mapping
1034
+ )
1035
+ return node_mapping
1036
+ raise RuntimeError ()
1037
+
922
1038
def _retrieve_or_visit_resource (self , resource_name : str ) -> NodeResource :
923
1039
resources_scope , (before_resources , after_resources ) = self ._safe_access_in (
924
1040
Scope (),
0 commit comments