samcli/lib/intrinsic_resolver/intrinsic_property_resolver.py (469 lines of code) (raw):
"""
Process and simplifies CloudFormation intrinsic properties such as FN::* and Ref
"""
import base64
import copy
import logging
import re
from collections import OrderedDict
from samcli.commands._utils.template import get_template_data
from samcli.lib.intrinsic_resolver.invalid_intrinsic_exception import InvalidIntrinsicException, InvalidSymbolException
from samcli.lib.intrinsic_resolver.invalid_intrinsic_validation import (
verify_all_list_intrinsic_type,
verify_in_bounds,
verify_intrinsic_type_bool,
verify_intrinsic_type_dict,
verify_intrinsic_type_int,
verify_intrinsic_type_list,
verify_intrinsic_type_str,
verify_non_null,
verify_number_arguments,
)
LOG = logging.getLogger(__name__)
class IntrinsicResolver:
AWS_INCLUDE = "AWS::Include"
SUPPORTED_MACRO_TRANSFORMATIONS = [AWS_INCLUDE]
_PSEUDO_REGEX = r"AWS::.*?"
_ATTRIBUTE_REGEX = r"[a-zA-Z0-9]*?\.?[a-zA-Z0-9]*?"
_REGEX_SUB_FUNCTION = r"\$\{(" + _PSEUDO_REGEX + "||" + _ATTRIBUTE_REGEX + r")\}"
FN_JOIN = "Fn::Join"
FN_SPLIT = "Fn::Split"
FN_SUB = "Fn::Sub"
FN_SELECT = "Fn::Select"
FN_BASE64 = "Fn::Base64"
FN_FIND_IN_MAP = "Fn::FindInMap"
FN_TRANSFORM = "Fn::Transform"
FN_GET_AZS = "Fn::GetAZs"
REF = "Ref"
FN_GET_ATT = "Fn::GetAtt"
FN_IMPORT_VALUE = "Fn::ImportValue"
SUPPORTED_INTRINSIC_FUNCTIONS = [
FN_JOIN,
FN_SPLIT,
FN_SUB,
FN_SELECT,
FN_BASE64,
FN_FIND_IN_MAP,
FN_TRANSFORM,
FN_GET_AZS,
REF,
FN_GET_ATT,
FN_IMPORT_VALUE,
]
FN_AND = "Fn::And"
FN_OR = "Fn::Or"
FN_IF = "Fn::If"
FN_EQUALS = "Fn::Equals"
FN_NOT = "Fn::Not"
CONDITIONAL_FUNCTIONS = [FN_AND, FN_OR, FN_IF, FN_EQUALS, FN_NOT]
def __init__(self, template, symbol_resolver):
"""
Initializes the Intrinsic Property class with the default intrinsic_key_function_map and
conditional_key_function_map.
In the future, for items like Fn::ImportValue multiple templates can be provided
into the function.
"""
self._template = None
self._resources = None
self._mapping = None
self._parameters = None
self._conditions = None
self._outputs = None
self.init_template(template)
self._symbol_resolver = symbol_resolver
self.intrinsic_key_function_map = self.default_intrinsic_function_map()
self.conditional_key_function_map = self.default_conditional_key_map()
def init_template(self, template):
self._template = copy.deepcopy(template or {})
self._resources = self._template.get("Resources", {})
self._mapping = self._template.get("Mappings", {})
self._parameters = self._template.get("Parameters", {})
self._conditions = self._template.get("Conditions", {})
self._outputs = self._template.get("Outputs", {})
def default_intrinsic_function_map(self):
"""
Returns a dictionary containing the mapping from
Intrinsic Function Key -> Intrinsic Resolver.
The intrinsic_resolver function has the format lambda intrinsic: some_retun_value
Return
-------
A dictionary containing the mapping from Intrinsic Function Key -> Intrinsic Resolver
"""
return {
IntrinsicResolver.FN_JOIN: self.handle_fn_join,
IntrinsicResolver.FN_SPLIT: self.handle_fn_split,
IntrinsicResolver.FN_SUB: self.handle_fn_sub,
IntrinsicResolver.FN_SELECT: self.handle_fn_select,
IntrinsicResolver.FN_BASE64: self.handle_fn_base64,
IntrinsicResolver.FN_FIND_IN_MAP: self.handle_find_in_map,
IntrinsicResolver.FN_TRANSFORM: self.handle_fn_transform,
IntrinsicResolver.FN_GET_AZS: self.handle_fn_get_azs,
IntrinsicResolver.REF: self.handle_fn_ref,
IntrinsicResolver.FN_GET_ATT: self.handle_fn_getatt,
IntrinsicResolver.FN_IMPORT_VALUE: self.handle_fn_import_value,
}
def default_conditional_key_map(self):
"""
Returns a dictionary containing the mapping from Conditional
Conditional Intrinsic Function Key -> Conditional Intrinsic Resolver.
The intrinsic_resolver function has the format lambda intrinsic: some_retun_value
The code was split between conditionals and other intrinsic keys for readability purposes.
Return
-------
A dictionary containing the mapping from Intrinsic Function Key -> Intrinsic Resolver
"""
return {
IntrinsicResolver.FN_AND: self.handle_fn_and,
IntrinsicResolver.FN_OR: self.handle_fn_or,
IntrinsicResolver.FN_IF: self.handle_fn_if,
IntrinsicResolver.FN_EQUALS: self.handle_fn_equals,
IntrinsicResolver.FN_NOT: self.handle_fn_not,
}
def set_intrinsic_key_function_map(self, function_map):
"""
Sets the mapping from
Conditional Intrinsic Function Key -> Conditional Intrinsic Resolver.
The intrinsic_resolver function has the format lambda intrinsic: some_retun_value
A user of this function can set the function map directly or can get the default_conditional_key_map directly.
"""
self.intrinsic_key_function_map = function_map
def set_conditional_function_map(self, function_map):
"""
Sets the mapping from
Conditional Intrinsic Function Key -> Conditional Intrinsic Resolver.
The intrinsic_resolver function has the format lambda intrinsic: some_retun_value
A user of this function can set the function map directly or can get the default_intrinsic_function_map directly
The code was split between conditionals and other intrinsic keys for readability purposes.
"""
self.conditional_key_function_map = function_map
def intrinsic_property_resolver(self, intrinsic, ignore_errors, parent_function="template"):
"""
This resolves the intrinsic of the format
{
intrinsic: dict
} by calling the function with the relevant intrinsic function resolver.
This also supports returning a string, list, boolean, int since they may be intermediate steps in the recursion
process. No transformations are done on these.
By default this will just return the item if non of the types match. This is because of the function
resolve_all_attributes which will recreate the resources by processing every aspect of resource.
This code resolves in a top down depth first fashion in order to create a functional style recursion that
doesn't mutate any of the properties.
Parameters
----------
intrinsic : dict, str, list, bool, int
This is an intrinsic property or an intermediate step
ignore_errors : bool
Whether to ignore errors
parent_function : str
In case there is a missing property, this is used to figure out where the property resolved is missing.
Return
---------
The simplified version of the intrinsic function. This could be a list,str,dict depending on the format required
"""
if intrinsic is None:
raise InvalidIntrinsicException("Missing Intrinsic property in {}".format(parent_function))
if isinstance(intrinsic, list):
return [self.intrinsic_property_resolver(item, ignore_errors) for item in intrinsic]
if not isinstance(intrinsic, dict) or intrinsic == {}:
return intrinsic
# `intrinsic` is a dict at this point.
keys = list(intrinsic.keys())
key = keys[0]
if key in self.intrinsic_key_function_map:
intrinsic_value = intrinsic.get(key)
return self.intrinsic_key_function_map.get(key)(intrinsic_value, ignore_errors)
if key in self.conditional_key_function_map:
intrinsic_value = intrinsic.get(key)
return self.conditional_key_function_map.get(key)(intrinsic_value, ignore_errors)
# In this case, it is a dictionary that doesn't directly contain an intrinsic resolver, we must recursively
# resolve each of it's sub properties.
sanitized_dict = {}
for key, val in intrinsic.items():
try:
sanitized_key = self.intrinsic_property_resolver(key, ignore_errors, parent_function=parent_function)
sanitized_val = self.intrinsic_property_resolver(val, ignore_errors, parent_function=parent_function)
verify_intrinsic_type_str(
sanitized_key,
message="The keys of the dictionary {} in {} must all resolve to a string".format(
sanitized_key, parent_function
),
)
sanitized_dict[sanitized_key] = sanitized_val
# On any exception, leave the key:val of the orginal intact and continue on.
# https://github.com/awslabs/aws-sam-cli/issues/1386
except Exception:
if ignore_errors:
LOG.debug("Unable to resolve property %s: %s. Leaving as is.", key, val)
sanitized_dict[key] = val
else:
raise
return sanitized_dict
def resolve_template(self, ignore_errors=False):
"""
This resolves all the attributes of the CloudFormation dictionary Resources, Outputs, Mappings, Parameters,
Conditions.
Return
-------
Return a processed template
"""
processed_template = self._template
if self._resources:
processed_template["Resources"] = self.resolve_attribute(self._resources, ignore_errors)
if self._outputs:
processed_template["Outputs"] = self.resolve_attribute(self._outputs, ignore_errors)
return processed_template
def resolve_attribute(self, cloud_formation_property, ignore_errors=False):
"""
This will parse through every entry in a CloudFormation root key and resolve them based on the symbol_resolver.
Customers can optionally ignore resource errors and default to whatever the resource provides.
Parameters
-----------
cloud_formation_property: dict
A high Level dictionary containg either the Mappings, Resources, Outputs, or Parameters Dictionary
ignore_errors: bool
An option to ignore errors that are InvalidIntrinsicException and InvalidSymbolException
Return
-------
A resolved template with all references possible simplified
"""
processed_dict = OrderedDict()
for key, val in cloud_formation_property.items():
processed_key = self._symbol_resolver.get_translation(key) or key
try:
processed_resource = self.intrinsic_property_resolver(val, ignore_errors, parent_function=processed_key)
processed_dict[processed_key] = processed_resource
except (InvalidIntrinsicException, InvalidSymbolException) as e:
resource_type = val.get("Type", "")
if ignore_errors:
LOG.error("Unable to process properties of %s.%s", key, resource_type)
processed_dict[key] = val
else:
raise InvalidIntrinsicException(
"Exception with property of {}.{}".format(key, resource_type) + ": " + str(e.args)
) from e
return processed_dict
def handle_fn_join(self, intrinsic_value, ignore_errors):
"""
{ "Fn::Join" : [ "delimiter", [ comma-delimited list of values ] ] }
This function will join the items in the list together based on the string using the python join.
This intrinsic function will resolve all the objects within the function's value and check their type.
Parameter
----------
intrinsic_value: list, dict
This is the value of the object inside the Fn::Join intrinsic function property
Return
-------
A string with the resolved attributes
"""
arguments = self.intrinsic_property_resolver(
intrinsic_value, ignore_errors, parent_function=IntrinsicResolver.FN_JOIN
)
verify_intrinsic_type_list(arguments, IntrinsicResolver.FN_JOIN)
delimiter = arguments[0]
verify_intrinsic_type_str(delimiter, IntrinsicResolver.FN_JOIN, position_in_list="first")
value_list = self.intrinsic_property_resolver(
arguments[1], ignore_errors, parent_function=IntrinsicResolver.FN_JOIN
)
verify_intrinsic_type_list(
value_list,
IntrinsicResolver.FN_JOIN,
message="The list of values in {} after the " "delimiter must be a list".format(IntrinsicResolver.FN_JOIN),
)
sanitized_value_list = [
self.intrinsic_property_resolver(item, ignore_errors, parent_function=IntrinsicResolver.FN_JOIN)
for item in value_list
]
verify_all_list_intrinsic_type(
sanitized_value_list, verification_func=verify_intrinsic_type_str, property_type=IntrinsicResolver.FN_JOIN
)
return delimiter.join(sanitized_value_list)
def handle_fn_split(self, intrinsic_value, ignore_errors):
"""
{ "Fn::Split" : [ "delimiter", "source string" ] }
This function will then split the source_string based on the delimiter
This intrinsic function will resolve all the objects within the function's value and check their type.
Parameter
----------
intrinsic_value: list, dict
This is the value of the object inside the Fn::Split intrinsic function property
Return
-------
A string with the resolved attributes
"""
arguments = self.intrinsic_property_resolver(
intrinsic_value, ignore_errors, parent_function=IntrinsicResolver.FN_SPLIT
)
verify_intrinsic_type_list(arguments, IntrinsicResolver.FN_SPLIT)
delimiter = arguments[0]
verify_intrinsic_type_str(delimiter, IntrinsicResolver.FN_SPLIT, position_in_list="first")
source_string = self.intrinsic_property_resolver(
arguments[1], ignore_errors, parent_function=IntrinsicResolver.FN_SPLIT
)
verify_intrinsic_type_str(source_string, IntrinsicResolver.FN_SPLIT, position_in_list="second")
return source_string.split(delimiter)
def handle_fn_base64(self, intrinsic_value, ignore_errors):
"""
{ "Fn::Base64" : valueToEncode }
This intrinsic function will then base64 encode the string using python's base64.
This function will resolve all the intrinsic properties in valueToEncode
Parameter
----------
intrinsic_value: list, dict
This is the value of the object inside the Fn::Base64 intrinsic function property
Return
-------
A string with the resolved attributes
"""
data = self.intrinsic_property_resolver(
intrinsic_value, ignore_errors, parent_function=IntrinsicResolver.FN_BASE64
)
verify_intrinsic_type_str(data, IntrinsicResolver.FN_BASE64)
# Encoding then decoding is required to return a string of the data
return base64.b64encode(data.encode()).decode()
def handle_fn_select(self, intrinsic_value, ignore_errors):
"""
{ "Fn::Select" : [ index, listOfObjects ] }
It will select the item in the listOfObjects using python's base64.
This intrinsic function will resolve all the objects within the function's value and check their type.
Parameter
----------
intrinsic_value: list, dict
This is the value of the object inside the Fn::Select intrinsic function property
Return
-------
A string with the resolved attributes
"""
arguments = self.intrinsic_property_resolver(
intrinsic_value, ignore_errors, parent_function=IntrinsicResolver.FN_SELECT
)
verify_intrinsic_type_list(arguments, IntrinsicResolver.FN_SELECT)
index = self.intrinsic_property_resolver(
arguments[0], ignore_errors, parent_function=IntrinsicResolver.FN_SELECT
)
verify_intrinsic_type_int(index, IntrinsicResolver.FN_SELECT)
list_of_objects = self.intrinsic_property_resolver(
arguments[1], ignore_errors, parent_function=IntrinsicResolver.FN_SELECT
)
verify_intrinsic_type_list(list_of_objects, IntrinsicResolver.FN_SELECT)
sanitized_objects = [
self.intrinsic_property_resolver(item, ignore_errors, parent_function=IntrinsicResolver.FN_SELECT)
for item in list_of_objects
]
verify_in_bounds(index=index, objects=sanitized_objects, property_type=IntrinsicResolver.FN_SELECT)
return sanitized_objects[index]
def handle_find_in_map(self, intrinsic_value, ignore_errors):
"""
{ "Fn::FindInMap" : [ "MapName", "TopLevelKey", "SecondLevelKey"] } This function will then lookup the
specified dictionary in the Mappings dictionary as mappings[map_name][top_level_key][second_level_key].
This intrinsic function will resolve all the objects within the function's value and check their type.
The format of the Mappings dictionary is:
"Mappings": {
"map_name": {
"top_level_key": {
"second_level_key": "value"
}
}
}
}
Parameter
----------
intrinsic_value: list, dict
This is the value of the object inside the Fn::FindInMap intrinsic function property
Return
-------
A string with the resolved attributes
"""
arguments = self.intrinsic_property_resolver(
intrinsic_value, ignore_errors, parent_function=IntrinsicResolver.FN_FIND_IN_MAP
)
verify_intrinsic_type_list(arguments, IntrinsicResolver.FN_FIND_IN_MAP)
verify_number_arguments(arguments, num=3, property_type=IntrinsicResolver.FN_FIND_IN_MAP)
map_name = self.intrinsic_property_resolver(
arguments[0], ignore_errors, parent_function=IntrinsicResolver.FN_FIND_IN_MAP
)
top_level_key = self.intrinsic_property_resolver(
arguments[1], ignore_errors, parent_function=IntrinsicResolver.FN_FIND_IN_MAP
)
second_level_key = self.intrinsic_property_resolver(
arguments[2], ignore_errors, parent_function=IntrinsicResolver.FN_FIND_IN_MAP
)
verify_intrinsic_type_str(map_name, IntrinsicResolver.FN_FIND_IN_MAP, position_in_list="first")
verify_intrinsic_type_str(top_level_key, IntrinsicResolver.FN_FIND_IN_MAP, position_in_list="second")
verify_intrinsic_type_str(second_level_key, IntrinsicResolver.FN_FIND_IN_MAP, position_in_list="third")
map_value = self._mapping.get(map_name)
verify_intrinsic_type_dict(
map_value,
IntrinsicResolver.FN_FIND_IN_MAP,
position_in_list="first",
message="The MapName is missing in the Mappings dictionary in Fn::FindInMap for {}".format(map_name),
)
top_level_value = map_value.get(top_level_key)
verify_intrinsic_type_dict(
top_level_value,
IntrinsicResolver.FN_FIND_IN_MAP,
message="The TopLevelKey is missing in the Mappings dictionary in Fn::FindInMap "
"for {}".format(top_level_key),
)
second_level_value = top_level_value.get(second_level_key)
verify_non_null(
second_level_value,
IntrinsicResolver.FN_FIND_IN_MAP,
message="The SecondLevelKey is missing in the Mappings dictionary in Fn::FindInMap "
"for {}".format(second_level_key),
)
return second_level_value
def handle_fn_get_azs(self, intrinsic_value, ignore_errors):
"""
{ "Fn::GetAZs" : "" }
{ "Fn::GetAZs" : { "Ref" : "AWS::Region" } }
{ "Fn::GetAZs" : "us-east-1" }
This intrinsic function will get the availability zones specified for the specified region. This is usually used
with {"Ref": "AWS::Region"}. If it is an empty string, it will get the default region.
This intrinsic function will resolve all the objects within the function's value and check their type.
Parameter
----------
intrinsic_value: list, dict
This is the value of the object inside the Fn::GetAZs intrinsic function property
Return
-------
A string with the resolved attributes
"""
intrinsic_value = self.intrinsic_property_resolver(
intrinsic_value, ignore_errors, parent_function=IntrinsicResolver.FN_GET_AZS
)
verify_intrinsic_type_str(intrinsic_value, IntrinsicResolver.FN_GET_AZS)
if not intrinsic_value:
intrinsic_value = self._symbol_resolver.handle_pseudo_region()
if intrinsic_value not in self._symbol_resolver.REGIONS:
raise InvalidIntrinsicException(
"Invalid region string passed in to {}".format(IntrinsicResolver.FN_GET_AZS)
)
return self._symbol_resolver.REGIONS.get(intrinsic_value)
def handle_fn_transform(self, intrinsic_value, ignore_errors):
"""
{ "Fn::Transform" : { "Name" : macro name, "Parameters" : {key : value, ... } } }
This intrinsic function will transform the data with the body provided
This intrinsic function will resolve all the objects within the function's value and check their type.
Parameter
----------
intrinsic_value: list, dict
This is the value of the object inside the Fn::Transform intrinsic function property
Return
-------
A string with the resolved attributes
"""
macro_name = intrinsic_value.get("Name")
name = self.intrinsic_property_resolver(
macro_name, ignore_errors, parent_function=IntrinsicResolver.FN_TRANSFORM
)
if name not in IntrinsicResolver.SUPPORTED_MACRO_TRANSFORMATIONS:
raise InvalidIntrinsicException(
"The type {} is not currently supported in {}".format(name, IntrinsicResolver.FN_TRANSFORM)
)
parameters = intrinsic_value.get("Parameters")
verify_intrinsic_type_dict(
parameters, IntrinsicResolver.FN_TRANSFORM, message=" Fn::Transform requires parameters section"
)
location = self.intrinsic_property_resolver(parameters.get("Location"), ignore_errors)
location_data = get_template_data(location)
return location_data
@staticmethod
def handle_fn_import_value(intrinsic_value, ignore_errors):
"""
{ "Fn::ImportValue" : sharedValueToImport }
This intrinsic function requires handling multiple stacks, which is not currently supported by SAM-CLI.
Thus, it will thrown an exception.
Return
-------
An InvalidIntrinsicException
"""
raise InvalidIntrinsicException("Fn::ImportValue is currently not supported by IntrinsicResolver")
def handle_fn_getatt(self, intrinsic_value, ignore_errors):
"""
{ "Fn::GetAtt" : [ "logicalNameOfResource", "attributeName" ] }
This intrinsic function gets the attribute for logical_resource specified. Each attribute might have a different
functionality depending on the type.
This intrinsic function will resolve all the objects within the function's value and check their type.
This calls the symbol resolver in order to resolve the relevant attribute.
Parameter
----------
intrinsic_value: list, dict
This is the value of the object inside the Fn::GetAtt intrinsic function property
Return
-------
A string with the resolved attributes
"""
arguments = self.intrinsic_property_resolver(
intrinsic_value, ignore_errors, parent_function=IntrinsicResolver.FN_GET_ATT
)
verify_intrinsic_type_list(arguments, IntrinsicResolver.FN_GET_ATT)
verify_number_arguments(arguments, IntrinsicResolver.FN_GET_ATT, num=2)
logical_id = self.intrinsic_property_resolver(
arguments[0], ignore_errors, parent_function=IntrinsicResolver.FN_GET_ATT
)
resource_type = self.intrinsic_property_resolver(
arguments[1], ignore_errors, parent_function=IntrinsicResolver.FN_GET_ATT
)
verify_intrinsic_type_str(logical_id, IntrinsicResolver.FN_GET_ATT)
verify_intrinsic_type_str(resource_type, IntrinsicResolver.FN_GET_ATT)
return self._symbol_resolver.resolve_symbols(logical_id, resource_type)
def handle_fn_ref(self, intrinsic_value, ignore_errors):
"""
{"Ref": "Logical ID"}
This intrinsic function gets the reference to a certain attribute. Some Ref's have different functionality with
different resource types.
This intrinsic function will resolve all the objects within the function's value and check their type.
This calls the symbol resolver in order to resolve the relevant attribute.
Parameter
----------
intrinsic_value: str
This is the value of the object inside the Ref intrinsic function property
Return
-------
A string with the resolved attributes
"""
arguments = self.intrinsic_property_resolver(
intrinsic_value, ignore_errors, parent_function=IntrinsicResolver.REF
)
verify_intrinsic_type_str(arguments, IntrinsicResolver.REF)
return self._symbol_resolver.resolve_symbols(arguments, IntrinsicResolver.REF)
def handle_fn_sub(self, intrinsic_value, ignore_errors):
"""
{ "Fn::Sub" : [ String, { Var1Name: Var1Value, Var2Name: Var2Value } ] } or { "Fn::Sub" : String }
This intrinsic function will substitute the variables specified in the list into the string provided. The string
will also parse out pseudo properties and anything of the form ${}.
This intrinsic function will resolve all the objects within the function's value and check their type.
Parameter
----------
intrinsic_value: list, dict
This is the value of the object inside the Fn::Join intrinsic function property
Return
-------
A string with the resolved attributes
"""
def resolve_sub_attribute(intrinsic_item, symbol_resolver):
if "." in intrinsic_item:
(logical_id, attribute_type) = intrinsic_item.rsplit(".", 1)
else:
(logical_id, attribute_type) = intrinsic_item, IntrinsicResolver.REF
return symbol_resolver.resolve_symbols(logical_id, attribute_type, ignore_errors=True)
if isinstance(intrinsic_value, str):
intrinsic_value = [intrinsic_value, {}]
verify_intrinsic_type_list(
intrinsic_value, IntrinsicResolver.FN_SUB, message="The arguments to a Fn::Sub must be a list or a string"
)
verify_number_arguments(intrinsic_value, IntrinsicResolver.FN_SUB, num=2)
sub_str = self.intrinsic_property_resolver(
intrinsic_value[0], ignore_errors, parent_function=IntrinsicResolver.FN_SUB
)
verify_intrinsic_type_str(sub_str, IntrinsicResolver.FN_SUB, position_in_list="first")
variables = intrinsic_value[1]
verify_intrinsic_type_dict(variables, IntrinsicResolver.FN_SUB, position_in_list="second")
sanitized_variables = self.intrinsic_property_resolver(
variables, ignore_errors, parent_function=IntrinsicResolver.FN_SUB
)
subable_props = re.findall(string=sub_str, pattern=IntrinsicResolver._REGEX_SUB_FUNCTION)
for sub_item in subable_props:
sanitized_item = sanitized_variables[sub_item] if sub_item in sanitized_variables else sub_item
result = resolve_sub_attribute(sanitized_item, self._symbol_resolver)
sub_str = re.sub(pattern=r"\$\{" + sub_item + r"\}", string=sub_str, repl=str(result))
return sub_str
def handle_fn_if(self, intrinsic_value, ignore_errors):
"""
{"Fn::If": [condition_name, value_if_true, value_if_false]}
This intrinsic function will evaluate the condition from the Conditions dictionary and then return value_if_true
or value_if_false depending on the value.
The Conditions dictionary will have the following format:
{
"Conditions": {
"condition_name": True/False or "{Intrinsic Function}"
}
}
This intrinsic function will resolve all the objects within the function's value and check their type.
Parameter
----------
intrinsic_value: list, dict
This is the value of the object inside the Fn::Join intrinsic function property
Return
-------
This will return value_if_true and value_if_false depending on how the condition is evaluated
"""
arguments = self.intrinsic_property_resolver(
intrinsic_value, ignore_errors, parent_function=IntrinsicResolver.FN_IF
)
verify_intrinsic_type_list(arguments, IntrinsicResolver.FN_IF)
verify_number_arguments(arguments, IntrinsicResolver.FN_IF, num=3)
condition_name = self.intrinsic_property_resolver(
arguments[0], ignore_errors, parent_function=IntrinsicResolver.FN_IF
)
verify_intrinsic_type_str(condition_name, IntrinsicResolver.FN_IF)
value_if_true = self.intrinsic_property_resolver(
arguments[1], ignore_errors, parent_function=IntrinsicResolver.FN_IF
)
value_if_false = self.intrinsic_property_resolver(
arguments[2], ignore_errors, parent_function=IntrinsicResolver.FN_IF
)
condition = self._conditions.get(condition_name)
verify_intrinsic_type_dict(
condition,
IntrinsicResolver.FN_IF,
message="The condition is missing in the Conditions dictionary for {}".format(IntrinsicResolver.FN_IF),
)
condition_evaluated = self.intrinsic_property_resolver(
condition, ignore_errors, parent_function=IntrinsicResolver.FN_IF
)
verify_intrinsic_type_bool(
condition_evaluated,
IntrinsicResolver.FN_IF,
message="The result of {} must evaluate to bool".format(IntrinsicResolver.FN_IF),
)
return value_if_true if condition_evaluated else value_if_false
def handle_fn_equals(self, intrinsic_value, ignore_errors):
"""
{"Fn::Equals" : ["value_1", "value_2"]}
This intrinsic function will verify that both items in the intrinsic function are equal after resolving them.
This intrinsic function will resolve all the objects within the function's value and check their type.
Parameter
----------
intrinsic_value: list, dict
This is the value of the object inside the Fn::Join intrinsic function property
Return
-------
A boolean depending on if both arguments is equal
"""
arguments = self.intrinsic_property_resolver(
intrinsic_value, ignore_errors, parent_function=IntrinsicResolver.FN_EQUALS
)
verify_intrinsic_type_list(arguments, IntrinsicResolver.FN_EQUALS)
verify_number_arguments(arguments, IntrinsicResolver.FN_EQUALS, num=2)
value_1 = self.intrinsic_property_resolver(
arguments[0], ignore_errors, parent_function=IntrinsicResolver.FN_EQUALS
)
value_2 = self.intrinsic_property_resolver(
arguments[1], ignore_errors, parent_function=IntrinsicResolver.FN_EQUALS
)
return value_1 == value_2
def handle_fn_not(self, intrinsic_value, ignore_errors):
"""
{"Fn::Not": [{condition}]}
This intrinsic function will negate the evaluation of the condition specified.
This intrinsic function will resolve all the objects within the function's value and check their type.
Parameter
----------
intrinsic_value: list, dict
This is the value of the object inside the Fn::Join intrinsic function property
Return
-------
A boolean that is the opposite of the condition evaluated
"""
arguments = self.intrinsic_property_resolver(
intrinsic_value, ignore_errors, parent_function=IntrinsicResolver.FN_NOT
)
verify_intrinsic_type_list(arguments, IntrinsicResolver.FN_NOT)
verify_number_arguments(arguments, IntrinsicResolver.FN_NOT, num=1)
argument_sanitised = self.intrinsic_property_resolver(
arguments[0], ignore_errors, parent_function=IntrinsicResolver.FN_NOT
)
if isinstance(argument_sanitised, dict) and "Condition" in arguments[0]:
condition_name = argument_sanitised.get("Condition")
verify_intrinsic_type_str(condition_name, IntrinsicResolver.FN_NOT)
condition = self._conditions.get(condition_name)
verify_non_null(condition, IntrinsicResolver.FN_NOT, position_in_list="first")
argument_sanitised = self.intrinsic_property_resolver(
condition, ignore_errors, parent_function=IntrinsicResolver.FN_NOT
)
verify_intrinsic_type_bool(
argument_sanitised,
IntrinsicResolver.FN_NOT,
message="The result of {} must evaluate to bool".format(IntrinsicResolver.FN_NOT),
)
return not argument_sanitised
@staticmethod
def get_prefix_position_in_list(i):
"""
Gets the prefix for the string "ith element of the list", handling first, second, and third.
:param i:
:return:
"""
first, second, third = 1, 2, 3
prefix = "{} th ".format(str(i))
if i == first:
prefix = "first "
elif i == second:
prefix = "second "
elif i == third:
prefix = "third "
return prefix
def handle_fn_and(self, intrinsic_value, ignore_errors):
"""
{"Fn::And": [{condition}, {...}]}
This intrinsic checks that every item in the list evaluates to a boolean. The items in the list can either
be of the format {Condition: condition_name} which finds and evaluates the Conditions dictionary of another
intrinsic function.
The Conditions dictionary will have the following format:
{
"Conditions": {
"condition_name": True/False or "{Intrinsic Function}"
}
}
This intrinsic function will resolve all the objects within the function's value and check their type.
Parameter
----------
intrinsic_value: list, dict
This is the value of the object inside the Fn::Join intrinsic function property
Return
-------
A boolean depending on if all of the properties in Fn::And evaluate to True
"""
arguments = self.intrinsic_property_resolver(
intrinsic_value, ignore_errors, parent_function=IntrinsicResolver.FN_AND
)
verify_intrinsic_type_list(arguments, IntrinsicResolver.FN_AND)
for i, argument in enumerate(arguments):
if isinstance(argument, dict) and "Condition" in argument:
condition_name = argument.get("Condition")
verify_intrinsic_type_str(condition_name, IntrinsicResolver.FN_AND)
condition = self._conditions.get(condition_name)
verify_non_null(
condition, IntrinsicResolver.FN_AND, position_in_list=self.get_prefix_position_in_list(i)
)
condition_evaluated = self.intrinsic_property_resolver(
condition, ignore_errors, parent_function=IntrinsicResolver.FN_AND
)
verify_intrinsic_type_bool(condition_evaluated, IntrinsicResolver.FN_AND)
if not condition_evaluated:
return False
else:
condition = self.intrinsic_property_resolver(
argument, ignore_errors, parent_function=IntrinsicResolver.FN_AND
)
verify_intrinsic_type_bool(condition, IntrinsicResolver.FN_AND)
if not condition:
return False
return True
def handle_fn_or(self, intrinsic_value, ignore_errors):
"""
{"Fn::Or": [{condition}, {...}]}
This intrinsic checks that a single item in the list evaluates to a boolean. The items in the list can either
be of the format {Condition: condition_name} which finds and evaluates the Conditions dictionary of another
intrinsic function.
The Conditions dictionary will have the following format:
{
"Conditions": {
"condition_name": True/False or "{Intrinsic Function}"
}
}
This intrinsic function will resolve all the objects within the function's value and check their type.
Parameter
----------
intrinsic_value: list, dict
This is the value of the object inside the Fn::Join intrinsic function property
Return
-------
A boolean depending on if any of the properties in Fn::And evaluate to True
"""
arguments = self.intrinsic_property_resolver(
intrinsic_value, ignore_errors, parent_function=IntrinsicResolver.FN_OR
)
verify_intrinsic_type_list(arguments, IntrinsicResolver.FN_OR)
for i, argument in enumerate(arguments):
if isinstance(argument, dict) and "Condition" in argument:
condition_name = argument.get("Condition")
verify_intrinsic_type_str(condition_name, IntrinsicResolver.FN_OR)
condition = self._conditions.get(condition_name)
verify_non_null(
condition, IntrinsicResolver.FN_OR, position_in_list=self.get_prefix_position_in_list(i)
)
condition_evaluated = self.intrinsic_property_resolver(
condition, ignore_errors, parent_function=IntrinsicResolver.FN_OR
)
verify_intrinsic_type_bool(condition_evaluated, IntrinsicResolver.FN_OR)
if condition_evaluated:
return True
else:
condition = self.intrinsic_property_resolver(
argument, ignore_errors, parent_function=IntrinsicResolver.FN_OR
)
verify_intrinsic_type_bool(condition, IntrinsicResolver.FN_OR)
if condition:
return True
return False