@@ -113,6 +113,7 @@ class ChangeSetTerminal(ChangeSetEntity, abc.ABC): ...
113113
114114
115115class NodeTemplate (ChangeSetNode ):
116+ mappings : Final [NodeMappings ]
116117 parameters : Final [NodeParameters ]
117118 conditions : Final [NodeConditions ]
118119 resources : Final [NodeResources ]
@@ -121,11 +122,13 @@ def __init__(
121122 self ,
122123 scope : Scope ,
123124 change_type : ChangeType ,
125+ mappings : NodeMappings ,
124126 parameters : NodeParameters ,
125127 conditions : NodeConditions ,
126128 resources : NodeResources ,
127129 ):
128130 super ().__init__ (scope = scope , change_type = change_type )
131+ self .mappings = mappings
129132 self .parameters = parameters
130133 self .conditions = conditions
131134 self .resources = resources
@@ -168,6 +171,24 @@ def __init__(self, scope: Scope, change_type: ChangeType, parameters: list[NodeP
168171 self .parameters = parameters
169172
170173
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+
171192class NodeCondition (ChangeSetNode ):
172193 name : Final [str ]
173194 body : Final [ChangeSetEntity ]
@@ -300,6 +321,7 @@ def __init__(self, scope: Scope, value: Any):
300321TypeKey : Final [str ] = "Type"
301322ConditionKey : Final [str ] = "Condition"
302323ConditionsKey : Final [str ] = "Conditions"
324+ MappingsKey : Final [str ] = "Mappings"
303325ResourcesKey : Final [str ] = "Resources"
304326PropertiesKey : Final [str ] = "Properties"
305327ParametersKey : Final [str ] = "Parameters"
@@ -309,7 +331,15 @@ def __init__(self, scope: Scope, value: Any):
309331FnNot : Final [str ] = "Fn::Not"
310332FnGetAttKey : Final [str ] = "Fn::GetAtt"
311333FnEqualsKey : 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+ }
313343
314344
315345class ChangeSetModel :
@@ -455,6 +485,36 @@ def _resolve_intrinsic_function_ref(self, arguments: ChangeSetEntity) -> ChangeT
455485 node_resource = self ._retrieve_or_visit_resource (resource_name = logical_id )
456486 return node_resource .change_type
457487
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+
458518 def _resolve_intrinsic_function_fn_if (self , arguments : ChangeSetEntity ) -> ChangeType :
459519 # TODO: validate arguments structure and type.
460520 if not isinstance (arguments , NodeArray ) or not arguments .array :
@@ -705,6 +765,36 @@ def _visit_resources(
705765 change_type = change_type .for_child (resource .change_type )
706766 return NodeResources (scope = scope , change_type = change_type , resources = resources )
707767
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+
708798 def _visit_dynamic_parameter (self , parameter_name : str ) -> ChangeSetEntity :
709799 scope = Scope ("Dynamic" ).open_scope ("Parameters" )
710800 scope_parameter , (before_parameter , after_parameter ) = self ._safe_access_in (
@@ -845,6 +935,14 @@ def _visit_conditions(
845935 def _model (self , before_template : Maybe [dict ], after_template : Maybe [dict ]) -> NodeTemplate :
846936 root_scope = Scope ()
847937 # 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+
848946 parameters_scope , (before_parameters , after_parameters ) = self ._safe_access_in (
849947 root_scope , ParametersKey , before_template , after_template
850948 )
@@ -876,6 +974,7 @@ def _model(self, before_template: Maybe[dict], after_template: Maybe[dict]) -> N
876974 return NodeTemplate (
877975 scope = root_scope ,
878976 change_type = resources .change_type ,
977+ mappings = mappings ,
879978 parameters = parameters ,
880979 conditions = conditions ,
881980 resources = resources ,
@@ -919,6 +1018,23 @@ def _retrieve_parameter_if_exists(self, parameter_name: str) -> Optional[NodePar
9191018 return node_parameter
9201019 return None
9211020
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+
9221038 def _retrieve_or_visit_resource (self , resource_name : str ) -> NodeResource :
9231039 resources_scope , (before_resources , after_resources ) = self ._safe_access_in (
9241040 Scope (),
0 commit comments