From a68acba5d6d67905900078faae7d5248818629c0 Mon Sep 17 00:00:00 2001 From: Luis Henrique Date: Thu, 28 Mar 2024 12:59:10 -0300 Subject: [PATCH 01/32] feat: add get att intrinsic function --- faster_sam/cloudformation.py | 45 +++++++++++++++++++++++++++ tests/fixtures/templates/example2.yml | 2 ++ tests/fixtures/templates/example3.yml | 11 ++++++- tests/fixtures/templates/example4.yml | 4 +++ tests/test_cloudformation.py | 2 ++ 5 files changed, 63 insertions(+), 1 deletion(-) diff --git a/faster_sam/cloudformation.py b/faster_sam/cloudformation.py index f461118c..011c50df 100644 --- a/faster_sam/cloudformation.py +++ b/faster_sam/cloudformation.py @@ -407,6 +407,9 @@ def eval(function: Dict[str, Any], template: Dict[str, Any]) -> Any: if "Fn::FindInMap" == fun: return IntrinsicFunctions.find_in_map(val, template) + if "Fn::GetAtt" == fun: + return IntrinsicFunctions.get_att(val, template) + if "Ref" == fun: return IntrinsicFunctions.ref(val, template) @@ -486,3 +489,45 @@ def ref(value: str, template: Dict[str, Any]) -> Optional[str]: # NOTE: this is a partial implementation return None + + @staticmethod + def get_att(value: List[Any], template: Dict[str, Any]) -> Optional[str]: + """ + Gets the value of an attribute from a CloudFormation template based on a list + of logical name and attribute name. + + Parameters + ---------- + value : List[Any] + List containing the logical name and attribute name + template : Dict[str, Any] + A dictionary representing the CloudFormation template. + + Returns + ------- + Any + The value of atribute name, or None if the keys are not found. + """ + logical_name, attribute_name = value + + if logical_name not in template["Resources"]: + return None + + if isinstance(attribute_name, dict): + attribute_name = IntrinsicFunctions.eval(attribute_name, template) + + if attribute_name is None: + return None + + if attribute_name not in template["Resources"][logical_name]["Properties"]: + return None + + function_value = template["Resources"][logical_name]["Properties"][attribute_name] + + if isinstance(function_value, dict): + function_value = IntrinsicFunctions.eval(function_value, template) + + if function_value is None: + return None + + return function_value diff --git a/tests/fixtures/templates/example2.yml b/tests/fixtures/templates/example2.yml index 5eff81e0..676b51ae 100644 --- a/tests/fixtures/templates/example2.yml +++ b/tests/fixtures/templates/example2.yml @@ -31,6 +31,8 @@ Globals: - Environments - Ref: Environment - LogLevel + SENDER_ACCOUNT: + Fn::GetAtt: [ HelloWorldFunction, CodeUri ] Resources: ApiGateway: diff --git a/tests/fixtures/templates/example3.yml b/tests/fixtures/templates/example3.yml index ab2ae126..855b5917 100644 --- a/tests/fixtures/templates/example3.yml +++ b/tests/fixtures/templates/example3.yml @@ -9,6 +9,12 @@ Parameters: Environment: Type: String Default: development + StageName: + Type: String + Default: v1 + AttributeName: + Type: String + Default: StageName Mappings: Environments: @@ -29,7 +35,8 @@ Resources: Type: AWS::Serverless::Api Properties: Name: sam-api - StageName: v1 + StageName: + Ref: StageName DefinitionBody: Fn::Transform: Name: AWS::Include @@ -63,3 +70,5 @@ Resources: - Environments - Ref: Environment - LogLevel + STAGE_NAME: + Fn::GetAtt: [ ApiGateway, !Ref AttributeName ] diff --git a/tests/fixtures/templates/example4.yml b/tests/fixtures/templates/example4.yml index 8788f263..aeaaf8a2 100644 --- a/tests/fixtures/templates/example4.yml +++ b/tests/fixtures/templates/example4.yml @@ -27,6 +27,10 @@ Globals: Variables: ENVIRONMENT: Ref: Environment + SENDER_ACCOUNT: + Fn::GetAtt: [ ApiGatewayTwo, Region ] + STAGE_NAME: + Fn::GetAtt: [ Stage, Region ] Resources: ApiGateway: diff --git a/tests/test_cloudformation.py b/tests/test_cloudformation.py index 1180a5b1..1a869f0a 100644 --- a/tests/test_cloudformation.py +++ b/tests/test_cloudformation.py @@ -190,10 +190,12 @@ def test_list_environment(self): "tests/fixtures/templates/example2.yml": { "ENVIRONMENT": "development", "LOG_LEVEL": "DEBUG", + "SENDER_ACCOUNT": "tests/", }, "tests/fixtures/templates/example3.yml": { "ENVIRONMENT": "development", "LOG_LEVEL": "DEBUG", + "STAGE_NAME": "v1", }, "tests/fixtures/templates/example4.yml": { "ENVIRONMENT": "development", From 96b1b45e8c99acc6e9bb0835f18d5ff8ab9a80f9 Mon Sep 17 00:00:00 2001 From: Luis Henrique Date: Thu, 28 Mar 2024 13:43:57 -0300 Subject: [PATCH 02/32] feat: add join intrinsic function --- faster_sam/cloudformation.py | 23 ++++++++++++++++++++++- tests/fixtures/templates/example2.yml | 5 +++++ tests/test_cloudformation.py | 1 + 3 files changed, 28 insertions(+), 1 deletion(-) diff --git a/faster_sam/cloudformation.py b/faster_sam/cloudformation.py index 441cc06a..0e21a8a5 100644 --- a/faster_sam/cloudformation.py +++ b/faster_sam/cloudformation.py @@ -394,7 +394,7 @@ def eval(function: Dict[str, Any], template: Dict[str, Any]) -> Any: NotImplementedError If the intrinsic function is not implemented. """ - implemented = ("Fn::Base64", "Fn::FindInMap", "Ref") + implemented = ("Fn::Base64", "Fn::FindInMap", "Ref", "Fn::GetAtt") fun, val = list(function.items())[0] @@ -410,6 +410,9 @@ def eval(function: Dict[str, Any], template: Dict[str, Any]) -> Any: if "Fn::GetAtt" == fun: return IntrinsicFunctions.get_att(val, template) + if "Fn::Join" == fun: + return IntrinsicFunctions.join(val, template) + if "Ref" == fun: return IntrinsicFunctions.ref(val, template) @@ -531,3 +534,21 @@ def get_att(value: List[Any], template: Dict[str, Any]) -> Optional[str]: return None return function_value + + @staticmethod + def join(value: List[Any], template: Dict[str, Any]) -> Optional[str]: + delimiter, values = value + + if len(values) < 2: + return None + + for i in range(len(values)): + if isinstance(values[i], dict): + evaluated_value = IntrinsicFunctions.eval(values[i], template) + + if evaluated_value is None: + return None + + values[i] = evaluated_value + + return delimiter.join(value[1]) diff --git a/tests/fixtures/templates/example2.yml b/tests/fixtures/templates/example2.yml index 676b51ae..a2dfe3c4 100644 --- a/tests/fixtures/templates/example2.yml +++ b/tests/fixtures/templates/example2.yml @@ -8,6 +8,9 @@ Description: > Parameters: Environment: Type: String + Handler: + type: String + Default: handler Mappings: Environments: @@ -33,6 +36,8 @@ Globals: - LogLevel SENDER_ACCOUNT: Fn::GetAtt: [ HelloWorldFunction, CodeUri ] + HANDLER: + Fn::Join: ["." , [fixtures, handlers, lambda_handler, !Ref Handler] ] Resources: ApiGateway: diff --git a/tests/test_cloudformation.py b/tests/test_cloudformation.py index 1a869f0a..2ea6a38c 100644 --- a/tests/test_cloudformation.py +++ b/tests/test_cloudformation.py @@ -191,6 +191,7 @@ def test_list_environment(self): "ENVIRONMENT": "development", "LOG_LEVEL": "DEBUG", "SENDER_ACCOUNT": "tests/", + "HANDLER": "fixtures.handlers.lambda_handler.handler", }, "tests/fixtures/templates/example3.yml": { "ENVIRONMENT": "development", From 021100a8d2414c01a5f790711b19cc169d7f8bce Mon Sep 17 00:00:00 2001 From: Luis Henrique Date: Thu, 28 Mar 2024 13:48:26 -0300 Subject: [PATCH 03/32] feat: add join intrinsic function --- faster_sam/cloudformation.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/faster_sam/cloudformation.py b/faster_sam/cloudformation.py index 0e21a8a5..0dc8cb0f 100644 --- a/faster_sam/cloudformation.py +++ b/faster_sam/cloudformation.py @@ -394,7 +394,7 @@ def eval(function: Dict[str, Any], template: Dict[str, Any]) -> Any: NotImplementedError If the intrinsic function is not implemented. """ - implemented = ("Fn::Base64", "Fn::FindInMap", "Ref", "Fn::GetAtt") + implemented = ("Fn::Base64", "Fn::FindInMap", "Ref", "Fn::GetAtt", "Fn::Join") fun, val = list(function.items())[0] From e4468e76a25b0306ff6ac9cc133955cb5e63b524 Mon Sep 17 00:00:00 2001 From: Luis Henrique Date: Thu, 28 Mar 2024 14:32:43 -0300 Subject: [PATCH 04/32] docs: update docstrings --- faster_sam/cloudformation.py | 18 +++++++++++++++++- 1 file changed, 17 insertions(+), 1 deletion(-) diff --git a/faster_sam/cloudformation.py b/faster_sam/cloudformation.py index 0dc8cb0f..65552983 100644 --- a/faster_sam/cloudformation.py +++ b/faster_sam/cloudformation.py @@ -508,7 +508,7 @@ def get_att(value: List[Any], template: Dict[str, Any]) -> Optional[str]: Returns ------- - Any + Optional[str] The value of atribute name, or None if the keys are not found. """ logical_name, attribute_name = value @@ -537,6 +537,22 @@ def get_att(value: List[Any], template: Dict[str, Any]) -> Optional[str]: @staticmethod def join(value: List[Any], template: Dict[str, Any]) -> Optional[str]: + """ + Joins elements in a list with a specified delimiter. + + Parameters + ---------- + value : List[Any] + A list containing two elements: the delimiter as the first element, + and the values to join as the second element. + template : Dict[str, Any] + A dictionary representing the CloudFormation template. + + Returns + ------- + Optional[str] + The joined string if successful; otherwise, None. + """ delimiter, values = value if len(values) < 2: From 8b802b07f1d96c72f435250bafcaf859adf923bb Mon Sep 17 00:00:00 2001 From: Luis Henrique Date: Thu, 28 Mar 2024 14:34:00 -0300 Subject: [PATCH 05/32] feat: add get att intrinsic function --- faster_sam/cloudformation.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/faster_sam/cloudformation.py b/faster_sam/cloudformation.py index 441cc06a..61d4005d 100644 --- a/faster_sam/cloudformation.py +++ b/faster_sam/cloudformation.py @@ -394,7 +394,7 @@ def eval(function: Dict[str, Any], template: Dict[str, Any]) -> Any: NotImplementedError If the intrinsic function is not implemented. """ - implemented = ("Fn::Base64", "Fn::FindInMap", "Ref") + implemented = ("Fn::Base64", "Fn::FindInMap", "Ref", "Fn::GetAtt") fun, val = list(function.items())[0] @@ -505,7 +505,7 @@ def get_att(value: List[Any], template: Dict[str, Any]) -> Optional[str]: Returns ------- - Any + Optional[str] The value of atribute name, or None if the keys are not found. """ logical_name, attribute_name = value From 6fd85c26fe7355714d9b35eaa59fed966623f5f3 Mon Sep 17 00:00:00 2001 From: Luis Henrique Date: Thu, 28 Mar 2024 15:56:03 -0300 Subject: [PATCH 06/32] feat: add select intrinsic function --- faster_sam/cloudformation.py | 43 ++++++++++++++++++++++++++- tests/fixtures/templates/example4.yml | 8 +++++ tests/test_cloudformation.py | 1 + 3 files changed, 51 insertions(+), 1 deletion(-) diff --git a/faster_sam/cloudformation.py b/faster_sam/cloudformation.py index 65552983..8392d016 100644 --- a/faster_sam/cloudformation.py +++ b/faster_sam/cloudformation.py @@ -394,7 +394,7 @@ def eval(function: Dict[str, Any], template: Dict[str, Any]) -> Any: NotImplementedError If the intrinsic function is not implemented. """ - implemented = ("Fn::Base64", "Fn::FindInMap", "Ref", "Fn::GetAtt", "Fn::Join") + implemented = ("Fn::Base64", "Fn::FindInMap", "Ref", "Fn::GetAtt", "Fn::Join", "Fn::Select") fun, val = list(function.items())[0] @@ -412,6 +412,9 @@ def eval(function: Dict[str, Any], template: Dict[str, Any]) -> Any: if "Fn::Join" == fun: return IntrinsicFunctions.join(val, template) + + if "Fn::Select" == fun: + return IntrinsicFunctions.select(val, template) if "Ref" == fun: return IntrinsicFunctions.ref(val, template) @@ -568,3 +571,41 @@ def join(value: List[Any], template: Dict[str, Any]) -> Optional[str]: values[i] = evaluated_value return delimiter.join(value[1]) + + @staticmethod + def select(value: List[Any], template: Dict[str, Any]) -> Optional[str]: + """ + Selects a value from a list based on the given index. If the value at the index + is a dictionary, it evaluates it using CloudFormation template data. + + Parameters + ---------- + value : List[Any] + A list containing values from which to select. + template : Dict[str, Any] + A dictionary representing the CloudFormation template. + + Returns + ------- + Optional[str] + The selected value from the list, or None if any of the evaluated + values are None. + """ + index, values = value + + if isinstance(index, dict): + index = IntrinsicFunctions.eval(index, template) + + if index is None: + return None + + for i in range(len(values)): + if isinstance(values[i], dict): + evaluated_value = IntrinsicFunctions.eval(values[i], template) + + if evaluated_value is None: + return None + + values[i] = evaluated_value + + return values[index] diff --git a/tests/fixtures/templates/example4.yml b/tests/fixtures/templates/example4.yml index aeaaf8a2..90eaf2db 100644 --- a/tests/fixtures/templates/example4.yml +++ b/tests/fixtures/templates/example4.yml @@ -9,15 +9,21 @@ Parameters: Environment: Type: String Default: development + Handler: + Type: String + Default: handler2 Mappings: Environments: development: LogLevel: DEBUG + Index: 1 staging: LogLevel: WARNING + Index: 1 production: LogLevel: ERROR + Index: 1 Globals: Function: @@ -31,6 +37,8 @@ Globals: Fn::GetAtt: [ ApiGatewayTwo, Region ] STAGE_NAME: Fn::GetAtt: [ Stage, Region ] + HANDLER: + Fn::Select: [ !FindInMap [Environments, !Ref Environment, Index], ["handler1", !Ref Handler]] Resources: ApiGateway: diff --git a/tests/test_cloudformation.py b/tests/test_cloudformation.py index 2ea6a38c..4aa75b68 100644 --- a/tests/test_cloudformation.py +++ b/tests/test_cloudformation.py @@ -201,6 +201,7 @@ def test_list_environment(self): "tests/fixtures/templates/example4.yml": { "ENVIRONMENT": "development", "LOG_LEVEL": "DEBUG", + "HANDLER": "handler2", }, } From dcf019a7c8fe1ee115057a47f980f06856185999 Mon Sep 17 00:00:00 2001 From: Luis Henrique Date: Mon, 1 Apr 2024 10:30:32 -0300 Subject: [PATCH 07/32] fix: resolve intrinsic functions in select --- faster_sam/cloudformation.py | 16 +++++++++++++--- 1 file changed, 13 insertions(+), 3 deletions(-) diff --git a/faster_sam/cloudformation.py b/faster_sam/cloudformation.py index 8392d016..c789300e 100644 --- a/faster_sam/cloudformation.py +++ b/faster_sam/cloudformation.py @@ -412,7 +412,7 @@ def eval(function: Dict[str, Any], template: Dict[str, Any]) -> Any: if "Fn::Join" == fun: return IntrinsicFunctions.join(val, template) - + if "Fn::Select" == fun: return IntrinsicFunctions.select(val, template) @@ -599,6 +599,14 @@ def select(value: List[Any], template: Dict[str, Any]) -> Optional[str]: if index is None: return None + if isinstance(values, dict): + values = IntrinsicFunctions.eval(values, template) + + if values is None: + return None + + result = [] + for i in range(len(values)): if isinstance(values[i], dict): evaluated_value = IntrinsicFunctions.eval(values[i], template) @@ -606,6 +614,8 @@ def select(value: List[Any], template: Dict[str, Any]) -> Optional[str]: if evaluated_value is None: return None - values[i] = evaluated_value + result.append(evaluated_value) + else: + result.append(values[i]) - return values[index] + return result[int(index)] From cecc57ff088e7c9056f70a7410a438d8207c0202 Mon Sep 17 00:00:00 2001 From: Luis Henrique Date: Mon, 1 Apr 2024 10:32:39 -0300 Subject: [PATCH 08/32] feat: add split intrinsic function --- faster_sam/cloudformation.py | 46 +++++++++++++++++++++++++-- tests/fixtures/templates/example3.yml | 2 ++ tests/test_cloudformation.py | 1 + 3 files changed, 47 insertions(+), 2 deletions(-) diff --git a/faster_sam/cloudformation.py b/faster_sam/cloudformation.py index c789300e..706d5f64 100644 --- a/faster_sam/cloudformation.py +++ b/faster_sam/cloudformation.py @@ -394,8 +394,11 @@ def eval(function: Dict[str, Any], template: Dict[str, Any]) -> Any: NotImplementedError If the intrinsic function is not implemented. """ - implemented = ("Fn::Base64", "Fn::FindInMap", "Ref", "Fn::GetAtt", "Fn::Join", "Fn::Select") + implemented = ("Fn::Base64", "Fn::FindInMap", "Ref", "Fn::GetAtt", "Fn::Join", "Fn::Select", "Fn::Split") + if isinstance(function, list): + import ipdb + ipdb.set_trace() fun, val = list(function.items())[0] if fun not in implemented: @@ -415,6 +418,9 @@ def eval(function: Dict[str, Any], template: Dict[str, Any]) -> Any: if "Fn::Select" == fun: return IntrinsicFunctions.select(val, template) + + if "Fn::Split" == fun: + return IntrinsicFunctions.split(val, template) if "Ref" == fun: return IntrinsicFunctions.ref(val, template) @@ -604,7 +610,7 @@ def select(value: List[Any], template: Dict[str, Any]) -> Optional[str]: if values is None: return None - + result = [] for i in range(len(values)): @@ -619,3 +625,39 @@ def select(value: List[Any], template: Dict[str, Any]) -> Optional[str]: result.append(values[i]) return result[int(index)] + + @staticmethod + def split(value: List[Any], template: Dict[str, Any]) -> Optional[str]: + """ + Splits a list of values using a specified delimiter. + + Parameters + ---------- + value : List[Any] + A tuple containing the delimiter as its first element, followed + by a list of values to split. + template : Dict[str, Any] + A dictionary representing the CloudFormation template. + + Returns + ------- + Optional[str] + A list of strings resulting from splitting using the delimiter. + or None if any of the evaluated values are None. + """ + delimiter, value = value + + result = [] + + if isinstance(value, dict): + value = IntrinsicFunctions.eval(value, template) + + if value is None: + return None + + split_parts = value.split(delimiter) + + for part in split_parts: + result.append(part) + + return result diff --git a/tests/fixtures/templates/example3.yml b/tests/fixtures/templates/example3.yml index 855b5917..5e36ae19 100644 --- a/tests/fixtures/templates/example3.yml +++ b/tests/fixtures/templates/example3.yml @@ -72,3 +72,5 @@ Resources: - LogLevel STAGE_NAME: Fn::GetAtt: [ ApiGateway, !Ref AttributeName ] + SENDER_ACCOUNT: + Fn::Select: [ 1, !Split ["|", "dev@gmail|dotz@gmail.com"]] diff --git a/tests/test_cloudformation.py b/tests/test_cloudformation.py index 4aa75b68..50cc0824 100644 --- a/tests/test_cloudformation.py +++ b/tests/test_cloudformation.py @@ -197,6 +197,7 @@ def test_list_environment(self): "ENVIRONMENT": "development", "LOG_LEVEL": "DEBUG", "STAGE_NAME": "v1", + "SENDER_ACCOUNT": "dotz@gmail.com", }, "tests/fixtures/templates/example4.yml": { "ENVIRONMENT": "development", From c97fb1c90519d7d412f8d52dc2b2d19e57e7f3a8 Mon Sep 17 00:00:00 2001 From: Luis Henrique Date: Mon, 1 Apr 2024 10:33:07 -0300 Subject: [PATCH 09/32] feat: add split intrinsic function --- faster_sam/cloudformation.py | 21 +++++++++++++++------ 1 file changed, 15 insertions(+), 6 deletions(-) diff --git a/faster_sam/cloudformation.py b/faster_sam/cloudformation.py index 706d5f64..734a7b9e 100644 --- a/faster_sam/cloudformation.py +++ b/faster_sam/cloudformation.py @@ -394,10 +394,19 @@ def eval(function: Dict[str, Any], template: Dict[str, Any]) -> Any: NotImplementedError If the intrinsic function is not implemented. """ - implemented = ("Fn::Base64", "Fn::FindInMap", "Ref", "Fn::GetAtt", "Fn::Join", "Fn::Select", "Fn::Split") + implemented = ( + "Fn::Base64", + "Fn::FindInMap", + "Ref", + "Fn::GetAtt", + "Fn::Join", + "Fn::Select", + "Fn::Split", + ) if isinstance(function, list): import ipdb + ipdb.set_trace() fun, val = list(function.items())[0] @@ -418,7 +427,7 @@ def eval(function: Dict[str, Any], template: Dict[str, Any]) -> Any: if "Fn::Select" == fun: return IntrinsicFunctions.select(val, template) - + if "Fn::Split" == fun: return IntrinsicFunctions.split(val, template) @@ -610,7 +619,7 @@ def select(value: List[Any], template: Dict[str, Any]) -> Optional[str]: if values is None: return None - + result = [] for i in range(len(values)): @@ -634,7 +643,7 @@ def split(value: List[Any], template: Dict[str, Any]) -> Optional[str]: Parameters ---------- value : List[Any] - A tuple containing the delimiter as its first element, followed + A tuple containing the delimiter as its first element, followed by a list of values to split. template : Dict[str, Any] A dictionary representing the CloudFormation template. @@ -646,7 +655,7 @@ def split(value: List[Any], template: Dict[str, Any]) -> Optional[str]: or None if any of the evaluated values are None. """ delimiter, value = value - + result = [] if isinstance(value, dict): @@ -656,7 +665,7 @@ def split(value: List[Any], template: Dict[str, Any]) -> Optional[str]: return None split_parts = value.split(delimiter) - + for part in split_parts: result.append(part) From bb44bbf6de5de37fdb961fbf2ecc3fefb4f33b47 Mon Sep 17 00:00:00 2001 From: Luis Henrique Date: Sun, 14 Apr 2024 15:35:04 -0300 Subject: [PATCH 10/32] feat: update intrinsic function to handle the short form of get att --- faster_sam/cloudformation.py | 7 +++++-- tests/fixtures/templates/example4.yml | 2 +- 2 files changed, 6 insertions(+), 3 deletions(-) diff --git a/faster_sam/cloudformation.py b/faster_sam/cloudformation.py index 61d4005d..8230bccc 100644 --- a/faster_sam/cloudformation.py +++ b/faster_sam/cloudformation.py @@ -2,7 +2,7 @@ from enum import Enum import logging from pathlib import Path -from typing import Any, Dict, List, Optional +from typing import Any, Dict, List, Optional, Union import yaml @@ -491,7 +491,7 @@ def ref(value: str, template: Dict[str, Any]) -> Optional[str]: return None @staticmethod - def get_att(value: List[Any], template: Dict[str, Any]) -> Optional[str]: + def get_att(value: Union[List[Any], str], template: Dict[str, Any]) -> Optional[str]: """ Gets the value of an attribute from a CloudFormation template based on a list of logical name and attribute name. @@ -508,6 +508,9 @@ def get_att(value: List[Any], template: Dict[str, Any]) -> Optional[str]: Optional[str] The value of atribute name, or None if the keys are not found. """ + if isinstance(value, str): + value = value.split(".") + logical_name, attribute_name = value if logical_name not in template["Resources"]: diff --git a/tests/fixtures/templates/example4.yml b/tests/fixtures/templates/example4.yml index aeaaf8a2..b159a8f8 100644 --- a/tests/fixtures/templates/example4.yml +++ b/tests/fixtures/templates/example4.yml @@ -30,7 +30,7 @@ Globals: SENDER_ACCOUNT: Fn::GetAtt: [ ApiGatewayTwo, Region ] STAGE_NAME: - Fn::GetAtt: [ Stage, Region ] + Fn::GetAtt: Stage.Region Resources: ApiGateway: From aa2b1dbfd28fc39fd5157d5032d188016d9591c0 Mon Sep 17 00:00:00 2001 From: Luis Henrique Date: Mon, 15 Apr 2024 14:33:41 -0300 Subject: [PATCH 11/32] feat: update select function --- faster_sam/cloudformation.py | 17 ++++++----------- swagger.yml | 0 2 files changed, 6 insertions(+), 11 deletions(-) create mode 100644 swagger.yml diff --git a/faster_sam/cloudformation.py b/faster_sam/cloudformation.py index ddad83fc..0fc1bdaa 100644 --- a/faster_sam/cloudformation.py +++ b/faster_sam/cloudformation.py @@ -818,17 +818,12 @@ def select(value: List[Any], template: Dict[str, Any]) -> Optional[str]: if values is None: return None - result = [] + result = "" - for i in range(len(values)): - if isinstance(values[i], dict): - evaluated_value = IntrinsicFunctions.eval(values[i], template) - - if evaluated_value is None: - return None + if isinstance(values[int(index)], dict): + result = IntrinsicFunctions.eval(values[int(index)], template) - result.append(evaluated_value) - else: - result.append(values[i]) + if result is None: + return None - return result[int(index)] + return result diff --git a/swagger.yml b/swagger.yml new file mode 100644 index 00000000..e69de29b From 9c2d633f5f102c465b7e1f2c25880bab71a5e066 Mon Sep 17 00:00:00 2001 From: Luis Henrique Date: Mon, 15 Apr 2024 16:43:49 -0300 Subject: [PATCH 12/32] wip: add sub function --- faster_sam/cloudformation.py | 78 ++++++++++++++++++++++++++++++++++-- 1 file changed, 74 insertions(+), 4 deletions(-) diff --git a/faster_sam/cloudformation.py b/faster_sam/cloudformation.py index 523ca44c..6c260148 100644 --- a/faster_sam/cloudformation.py +++ b/faster_sam/cloudformation.py @@ -1,7 +1,9 @@ import base64 import logging from enum import Enum +import os from pathlib import Path +import re from typing import Any, Dict, List, Optional, Union import yaml @@ -594,10 +596,6 @@ def eval(function: Dict[str, Any], template: Dict[str, Any]) -> Any: "Fn::Split", ) - if isinstance(function, list): - import ipdb - - ipdb.set_trace() fun, val = list(function.items())[0] if fun not in implemented: @@ -856,3 +854,75 @@ def split(value: List[Any], template: Dict[str, Any]) -> Optional[str]: for part in split_parts: result.append(part) + + return result + + def sub(value: List[Any], template: Dict[str, Any]) -> Optional[str]: + """ + Substitutes occurrences in a string with corresponding environment variables. + + This method searches for occurrences in the `value` string in the format `${placeholder}` + and replaces them with their corresponding values from the environment variables. + + If any occurrence is not found in the environment variables, the substitution is not performed. + + Parameters + ---------- + value : List[Any] + A string with the placeholder to be replaced. + template : Dict[str, Any] + A dictionary representing the CloudFormation template. + + Returns + ------- + Optional[str] + The string after performing the substitution, or None if the `value` is not a string or + is not found in the environment variables. + """ + pseudo_parameters = [ + "AWS::AccountId", + "AWS::NotificationARNs", + "AWS::NoValue", + "AWS::Partition", + "AWS::Region", + "AWS::StackId", + "AWS::StackName", + "AWS::URLSuffix" + ] + + def replace_placeholders(matches: List[Any], value: str): + matches = [param if param not in pseudo_parameters else param.replace("::", "_") for param in matches] + + for match in matches: + if match in os.environ: + env_var = os.environ[match] + result = value.replace(f"${{{match}}}", env_var) + else: + return None + + return result + + if isinstance(value, str): + pattern = r"\${(.*?)}" + matches = re.findall(pattern, value) + + replace_placeholders(matches, value) + + elif isinstance(value, list): + string_value = value[0] + + for val in value[1:]: + key, v = val.items() + + if isinstance(v, dict): + v = IntrinsicFunctions.eval(v, template) + + if v is None: + return None + + val[key] = v + + pattern = r"\${({key})}" + matches = re.findall(pattern, val[key]) + + replace_placeholders(matches, string_value) From 998854061914f8394e4c7023d772a2682042ad0d Mon Sep 17 00:00:00 2001 From: Luis Henrique Date: Mon, 15 Apr 2024 19:49:01 -0300 Subject: [PATCH 13/32] wip: add sub function --- faster_sam/cloudformation.py | 38 ++++++++++++++++++++---------------- 1 file changed, 21 insertions(+), 17 deletions(-) diff --git a/faster_sam/cloudformation.py b/faster_sam/cloudformation.py index 6c260148..b3b65f3a 100644 --- a/faster_sam/cloudformation.py +++ b/faster_sam/cloudformation.py @@ -863,8 +863,9 @@ def sub(value: List[Any], template: Dict[str, Any]) -> Optional[str]: This method searches for occurrences in the `value` string in the format `${placeholder}` and replaces them with their corresponding values from the environment variables. - - If any occurrence is not found in the environment variables, the substitution is not performed. + + If any occurrence is not found in the environment variables, the substitution is not + performed. Parameters ---------- @@ -880,18 +881,21 @@ def sub(value: List[Any], template: Dict[str, Any]) -> Optional[str]: is not found in the environment variables. """ pseudo_parameters = [ - "AWS::AccountId", - "AWS::NotificationARNs", - "AWS::NoValue", - "AWS::Partition", - "AWS::Region", - "AWS::StackId", - "AWS::StackName", - "AWS::URLSuffix" - ] - + "AWS::AccountId", + "AWS::NotificationARNs", + "AWS::NoValue", + "AWS::Partition", + "AWS::Region", + "AWS::StackId", + "AWS::StackName", + "AWS::URLSuffix", + ] + def replace_placeholders(matches: List[Any], value: str): - matches = [param if param not in pseudo_parameters else param.replace("::", "_") for param in matches] + matches = [ + param if param not in pseudo_parameters else param.replace("::", "_") + for param in matches + ] for match in matches: if match in os.environ: @@ -910,18 +914,18 @@ def replace_placeholders(matches: List[Any], value: str): elif isinstance(value, list): string_value = value[0] - + for val in value[1:]: key, v = val.items() if isinstance(v, dict): v = IntrinsicFunctions.eval(v, template) - + if v is None: return None - + val[key] = v - + pattern = r"\${({key})}" matches = re.findall(pattern, val[key]) From c8d33daef7d417ad3afa1f8c9d5f052b57893e0e Mon Sep 17 00:00:00 2001 From: Luis Henrique Date: Mon, 15 Apr 2024 19:57:56 -0300 Subject: [PATCH 14/32] refactor: update param type and variable name --- faster_sam/cloudformation.py | 12 ++++++------ tests/fixtures/templates/example4.yml | 2 +- 2 files changed, 7 insertions(+), 7 deletions(-) diff --git a/faster_sam/cloudformation.py b/faster_sam/cloudformation.py index 8230bccc..acc7f215 100644 --- a/faster_sam/cloudformation.py +++ b/faster_sam/cloudformation.py @@ -491,7 +491,7 @@ def ref(value: str, template: Dict[str, Any]) -> Optional[str]: return None @staticmethod - def get_att(value: Union[List[Any], str], template: Dict[str, Any]) -> Optional[str]: + def get_att(value: Union[List[str], str], template: Dict[str, Any]) -> Optional[str]: """ Gets the value of an attribute from a CloudFormation template based on a list of logical name and attribute name. @@ -525,12 +525,12 @@ def get_att(value: Union[List[Any], str], template: Dict[str, Any]) -> Optional[ if attribute_name not in template["Resources"][logical_name]["Properties"]: return None - function_value = template["Resources"][logical_name]["Properties"][attribute_name] + attribute_value = template["Resources"][logical_name]["Properties"][attribute_name] - if isinstance(function_value, dict): - function_value = IntrinsicFunctions.eval(function_value, template) + if isinstance(attribute_value, dict): + attribute_value = IntrinsicFunctions.eval(attribute_value, template) - if function_value is None: + if attribute_value is None: return None - return function_value + return attribute_value diff --git a/tests/fixtures/templates/example4.yml b/tests/fixtures/templates/example4.yml index b159a8f8..9a183b27 100644 --- a/tests/fixtures/templates/example4.yml +++ b/tests/fixtures/templates/example4.yml @@ -30,7 +30,7 @@ Globals: SENDER_ACCOUNT: Fn::GetAtt: [ ApiGatewayTwo, Region ] STAGE_NAME: - Fn::GetAtt: Stage.Region + Fn::GetAtt: ApiGateway.Region Resources: ApiGateway: From 02e7cd9d33ab430f6395c04cf3d9e8b12265cf6a Mon Sep 17 00:00:00 2001 From: Luis Henrique Date: Mon, 15 Apr 2024 20:08:45 -0300 Subject: [PATCH 15/32] feat: update join function --- faster_sam/cloudformation.py | 17 +++++++---------- 1 file changed, 7 insertions(+), 10 deletions(-) diff --git a/faster_sam/cloudformation.py b/faster_sam/cloudformation.py index 65552983..734bed19 100644 --- a/faster_sam/cloudformation.py +++ b/faster_sam/cloudformation.py @@ -555,16 +555,13 @@ def join(value: List[Any], template: Dict[str, Any]) -> Optional[str]: """ delimiter, values = value - if len(values) < 2: - return None - - for i in range(len(values)): - if isinstance(values[i], dict): - evaluated_value = IntrinsicFunctions.eval(values[i], template) + for index, element in enumerate(values): + if isinstance(element, dict): + element = IntrinsicFunctions.eval(element, template) - if evaluated_value is None: - return None + if element is None: + return None - values[i] = evaluated_value + values[index] = element - return delimiter.join(value[1]) + return delimiter.join(values) From 312b9fa1cb14185694c2560337d68532f1b0ffd1 Mon Sep 17 00:00:00 2001 From: Luis Henrique Date: Mon, 15 Apr 2024 20:21:15 -0300 Subject: [PATCH 16/32] feat: update join function --- tests/fixtures/templates/example2.yml | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/tests/fixtures/templates/example2.yml b/tests/fixtures/templates/example2.yml index a2dfe3c4..76b110e8 100644 --- a/tests/fixtures/templates/example2.yml +++ b/tests/fixtures/templates/example2.yml @@ -36,8 +36,13 @@ Globals: - LogLevel SENDER_ACCOUNT: Fn::GetAtt: [ HelloWorldFunction, CodeUri ] - HANDLER: - Fn::Join: ["." , [fixtures, handlers, lambda_handler, !Ref Handler] ] + HANDLER: + Fn::Join: + - "." + - - fixtures + - handlers + - lambda_handler + - Ref: Handler Resources: ApiGateway: From f244a86010673758c2ea63f9fb38fcfd9b4ad778 Mon Sep 17 00:00:00 2001 From: Luis Henrique Date: Mon, 15 Apr 2024 20:44:24 -0300 Subject: [PATCH 17/32] feat: update select function test --- faster_sam/cloudformation.py | 2 +- tests/fixtures/templates/example4.yml | 11 +++++++++-- 2 files changed, 10 insertions(+), 3 deletions(-) diff --git a/faster_sam/cloudformation.py b/faster_sam/cloudformation.py index 0fc1bdaa..8a8000f7 100644 --- a/faster_sam/cloudformation.py +++ b/faster_sam/cloudformation.py @@ -804,7 +804,7 @@ def select(value: List[Any], template: Dict[str, Any]) -> Optional[str]: The selected value from the list, or None if any of the evaluated values are None. """ - index, values = value + index, *values = value if isinstance(index, dict): index = IntrinsicFunctions.eval(index, template) diff --git a/tests/fixtures/templates/example4.yml b/tests/fixtures/templates/example4.yml index 52cb7ace..db3f55c8 100644 --- a/tests/fixtures/templates/example4.yml +++ b/tests/fixtures/templates/example4.yml @@ -37,8 +37,15 @@ Globals: Fn::GetAtt: [ ApiGatewayTwo, Region ] STAGE_NAME: Fn::GetAtt: Stage.Region - HANDLER: - Fn::Select: [ !FindInMap [Environments, !Ref Environment, Index], ["handler1", !Ref Handler]] + HANDLER: + Fn::Select: + - Fn::FindInMap: + - Environments + - Ref: Environment + - Index + - handler1 + - Ref: Handler + Resources: ApiGateway: From b3d14ce6eef5558c484adbca93aaef18f0fa122f Mon Sep 17 00:00:00 2001 From: Luis Henrique Date: Tue, 16 Apr 2024 15:39:55 -0300 Subject: [PATCH 18/32] feat: update select function --- faster_sam/cloudformation.py | 31 +++++++++++---------------- tests/fixtures/templates/example4.yml | 4 ++-- 2 files changed, 14 insertions(+), 21 deletions(-) diff --git a/faster_sam/cloudformation.py b/faster_sam/cloudformation.py index d017a280..cfad33b4 100644 --- a/faster_sam/cloudformation.py +++ b/faster_sam/cloudformation.py @@ -767,40 +767,33 @@ def select(value: List[Any], template: Dict[str, Any]) -> Optional[str]: """ Selects a value from a list based on the given index. If the value at the index is a dictionary, it evaluates it using CloudFormation template data. - Parameters ---------- value : List[Any] A list containing values from which to select. template : Dict[str, Any] A dictionary representing the CloudFormation template. - Returns ------- Optional[str] The selected value from the list, or None if any of the evaluated values are None. """ - index, *values = value + index, objects = value if isinstance(index, dict): - index = IntrinsicFunctions.eval(index, template) + index = IntrinsicFunctions.eval(index, template) if index is None: return None - - if isinstance(values, dict): - values = IntrinsicFunctions.eval(values, template) - - if values is None: - return None - - result = "" - - if isinstance(values[int(index)], dict): - result = IntrinsicFunctions.eval(values[int(index)], template) - - if result is None: + if isinstance(objects, dict): + objects = IntrinsicFunctions.eval(objects, template) + if objects is None: return None - - return result + else: + for i, obj in enumerate(objects): + if isinstance(obj, dict): + objects[i] = IntrinsicFunctions.eval(obj, template) + if objects[i] is None: + return None + return objects[int(index)] diff --git a/tests/fixtures/templates/example4.yml b/tests/fixtures/templates/example4.yml index 714089cf..a894954c 100644 --- a/tests/fixtures/templates/example4.yml +++ b/tests/fixtures/templates/example4.yml @@ -43,8 +43,8 @@ Globals: - Environments - Ref: Environment - Index - - handler1 - - Ref: Handler + - - handler1 + - Ref: Handler Resources: ApiGateway: From 2258fd156af489ad4be5d435bc6e5c14c9345830 Mon Sep 17 00:00:00 2001 From: Luis Henrique Date: Tue, 16 Apr 2024 15:43:55 -0300 Subject: [PATCH 19/32] feat: update select function --- faster_sam/cloudformation.py | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/faster_sam/cloudformation.py b/faster_sam/cloudformation.py index cfad33b4..9a97d2b3 100644 --- a/faster_sam/cloudformation.py +++ b/faster_sam/cloudformation.py @@ -782,18 +782,22 @@ def select(value: List[Any], template: Dict[str, Any]) -> Optional[str]: index, objects = value if isinstance(index, dict): - index = IntrinsicFunctions.eval(index, template) + if index is None: return None + if isinstance(objects, dict): objects = IntrinsicFunctions.eval(objects, template) + if objects is None: return None else: for i, obj in enumerate(objects): if isinstance(obj, dict): objects[i] = IntrinsicFunctions.eval(obj, template) + if objects[i] is None: return None + return objects[int(index)] From 078ab663e0d239f7c7b82878426b97e05380b6c4 Mon Sep 17 00:00:00 2001 From: Luis Henrique Date: Tue, 16 Apr 2024 15:52:25 -0300 Subject: [PATCH 20/32] feat: update split function --- faster_sam/cloudformation.py | 11 +---------- tests/fixtures/templates/example3.yml | 10 ++++++++-- 2 files changed, 9 insertions(+), 12 deletions(-) diff --git a/faster_sam/cloudformation.py b/faster_sam/cloudformation.py index c79e8d8a..39dbe9ec 100644 --- a/faster_sam/cloudformation.py +++ b/faster_sam/cloudformation.py @@ -594,10 +594,6 @@ def eval(function: Dict[str, Any], template: Dict[str, Any]) -> Any: "Fn::Split", ) - if isinstance(function, list): - import ipdb - - ipdb.set_trace() fun, val = list(function.items())[0] if fun not in implemented: @@ -838,15 +834,10 @@ def split(value: List[Any], template: Dict[str, Any]) -> Optional[str]: """ delimiter, value = value - result = [] - if isinstance(value, dict): value = IntrinsicFunctions.eval(value, template) if value is None: return None - split_parts = value.split(delimiter) - - for part in split_parts: - result.append(part) + return value.split(delimiter) diff --git a/tests/fixtures/templates/example3.yml b/tests/fixtures/templates/example3.yml index 5e36ae19..17f7b7f0 100644 --- a/tests/fixtures/templates/example3.yml +++ b/tests/fixtures/templates/example3.yml @@ -71,6 +71,12 @@ Resources: - Ref: Environment - LogLevel STAGE_NAME: - Fn::GetAtt: [ ApiGateway, !Ref AttributeName ] + Fn::GetAtt: + - ApiGateway + - Ref: AttributeName SENDER_ACCOUNT: - Fn::Select: [ 1, !Split ["|", "dev@gmail|dotz@gmail.com"]] + Fn::Select: + - 1 + - Fn::Split: + - "|" + - "dev@gmail|dotz@gmail.com" From e8102048c9ebf5f63b3822e8568c37d62afbaa94 Mon Sep 17 00:00:00 2001 From: Luis Henrique Date: Tue, 16 Apr 2024 15:56:23 -0300 Subject: [PATCH 21/32] feat: update split function --- faster_sam/cloudformation.py | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/faster_sam/cloudformation.py b/faster_sam/cloudformation.py index 39dbe9ec..87be04a0 100644 --- a/faster_sam/cloudformation.py +++ b/faster_sam/cloudformation.py @@ -832,12 +832,12 @@ def split(value: List[Any], template: Dict[str, Any]) -> Optional[str]: A list of strings resulting from splitting using the delimiter. or None if any of the evaluated values are None. """ - delimiter, value = value + delimiter, source = value - if isinstance(value, dict): - value = IntrinsicFunctions.eval(value, template) + if isinstance(source, dict): + source = IntrinsicFunctions.eval(source, template) - if value is None: + if source is None: return None - return value.split(delimiter) + return source.split(delimiter) From 3a25222a7433cb577f3f843c6f0d628c2d075759 Mon Sep 17 00:00:00 2001 From: Luis Henrique Date: Tue, 16 Apr 2024 16:38:09 -0300 Subject: [PATCH 22/32] test: add getatt function tests --- tests/fixtures/templates/example3.yml | 2 ++ tests/test_cloudformation.py | 42 ++++++++++++++++++++++++++- 2 files changed, 43 insertions(+), 1 deletion(-) diff --git a/tests/fixtures/templates/example3.yml b/tests/fixtures/templates/example3.yml index 855b5917..e0bb708b 100644 --- a/tests/fixtures/templates/example3.yml +++ b/tests/fixtures/templates/example3.yml @@ -37,6 +37,8 @@ Resources: Name: sam-api StageName: Ref: StageName + Tags: + Ref: Tags DefinitionBody: Fn::Transform: Name: AWS::Include diff --git a/tests/test_cloudformation.py b/tests/test_cloudformation.py index 6ebd4fc0..4fc4cbc1 100644 --- a/tests/test_cloudformation.py +++ b/tests/test_cloudformation.py @@ -6,7 +6,7 @@ import yaml import faster_sam.cloudformation as cf -from faster_sam.cloudformation import CloudformationTemplate +from faster_sam.cloudformation import CloudformationTemplate, IntrinsicFunctions @contextmanager @@ -267,6 +267,46 @@ def test_lambda_handler(self): self.assertEqual(handler_path, "tests.fixtures.handlers.lambda_handler.handler") +class TestIntrinsicFunctions(unittest.TestCase): + def test_getatt_function(self): + scenarios = { + "Resolved Function": { + "template": "tests/fixtures/templates/example3.yml", + "function": {"Fn::GetAtt": ["ApiGateway", {"Ref": "AttributeName"}]}, + "expected": "v1", + }, + "Attribute ID is None": { + "template": "tests/fixtures/templates/example3.yml", + "function": {"Fn::GetAtt": ["ApiFunction", {"Ref": "AttributeName"}]}, + "expected": None, + }, + "Attribute Name is None": { + "template": "tests/fixtures/templates/example3.yml", + "function": {"Fn::GetAtt": ["ApiGateway", "attibute"]}, + "expected": None, + }, + "Unresolved Attribute Function": { + "template": "tests/fixtures/templates/example3.yml", + "function": {"Fn::GetAtt": ["ApiGateway", {"Ref": "Attribute"}]}, + "expected": None, + }, + "Unresolved Function Return Value": { + "template": "tests/fixtures/templates/example3.yml", + "function": {"Fn::GetAtt": ["ApiGateway", "Tags"]}, + "expected": None, + }, + } + + for key, values in scenarios.items(): + with self.subTest(case=key, template=values["template"]): + cloudformation = CloudformationTemplate( + values["template"], parameters={"Environment": "development"} + ) + value = IntrinsicFunctions.eval(values["function"], cloudformation.template) + + self.assertEqual(value, values["expected"]) + + class TestResource(unittest.TestCase): def test_resource(self): resource_id = "Test" From 0621aa3f6cb2cb22307581c12a934697ee1a178a Mon Sep 17 00:00:00 2001 From: Luis Henrique Date: Tue, 16 Apr 2024 17:03:58 -0300 Subject: [PATCH 23/32] test: add join function tests --- tests/test_cloudformation.py | 33 +++++++++++++++++++++++++++++++++ 1 file changed, 33 insertions(+) diff --git a/tests/test_cloudformation.py b/tests/test_cloudformation.py index 1d5d6047..890f6f52 100644 --- a/tests/test_cloudformation.py +++ b/tests/test_cloudformation.py @@ -307,6 +307,39 @@ def test_getatt_function(self): self.assertEqual(value, values["expected"]) + def test_join_function(self): + scenarios = { + "Resolved Function": { + "template": "tests/fixtures/templates/example2.yml", + "function": { + "Fn::Join": [ + ".", + ["fixtures", "handlers", "lambda_handler", {"Ref": "Handler"}], + ] + }, + "expected": "fixtures.handlers.lambda_handler.handler", + }, + "Unresolved Function with Incorrect Reference": { + "template": "tests/fixtures/templates/example2.yml", + "function": { + "Fn::Join": [ + ".", + ["fixtures", "handlers", "lambda_handler", {"Ref": "fixture"}], + ] + }, + "expected": None, + }, + } + + for key, values in scenarios.items(): + with self.subTest(case=key, template=values["template"]): + cloudformation = CloudformationTemplate( + values["template"], parameters={"Environment": "development"} + ) + value = IntrinsicFunctions.eval(values["function"], cloudformation.template) + + self.assertEqual(value, values["expected"]) + class TestResource(unittest.TestCase): def test_resource(self): From 901ac5f299e4455b9cc82abb567835d76e869c2d Mon Sep 17 00:00:00 2001 From: Luis Henrique Date: Tue, 16 Apr 2024 17:47:00 -0300 Subject: [PATCH 24/32] test: add select function tests --- tests/fixtures/templates/example4.yml | 3 ++ tests/test_cloudformation.py | 58 +++++++++++++++++++++++++++ 2 files changed, 61 insertions(+) diff --git a/tests/fixtures/templates/example4.yml b/tests/fixtures/templates/example4.yml index a894954c..5fca08fc 100644 --- a/tests/fixtures/templates/example4.yml +++ b/tests/fixtures/templates/example4.yml @@ -12,6 +12,9 @@ Parameters: Handler: Type: String Default: handler2 + HandlerList: + Type: String + Default: [handler, handler2] Mappings: Environments: diff --git a/tests/test_cloudformation.py b/tests/test_cloudformation.py index 5f739648..834933c8 100644 --- a/tests/test_cloudformation.py +++ b/tests/test_cloudformation.py @@ -341,6 +341,64 @@ def test_join_function(self): self.assertEqual(value, values["expected"]) + def test_select_function(self): + scenarios = { + "Resolved Function with Correct Index": { + "template": "tests/fixtures/templates/example4.yml", + "function": { + "Fn::Select": [ + {"Fn::FindInMap": ["Environments", {"Ref": "Environment"}, "Index"]}, + ["handler1", {"Ref": "Handler"}], + ] + }, + "expected": "handler2", + }, + "Unresolved Function with Incorrect Reference in List": { + "template": "tests/fixtures/templates/example4.yml", + "function": { + "Fn::Select": [ + {"Fn::FindInMap": ["Environments", {"Ref": "Environment"}, "Index"]}, + ["handler1", {"Ref": "Fixture"}], + ] + }, + "expected": None, + }, + "Resolved Function with Correct Reference in List": { + "template": "tests/fixtures/templates/example4.yml", + "function": { + "Fn::Select": [ + {"Fn::FindInMap": ["Environments", {"Ref": "Environment"}, "Index"]}, + {"Ref": "HandlerList"}, + ] + }, + "expected": "handler2", + }, + "Unresolved Function with Incorrect Index Reference": { + "template": "tests/fixtures/templates/example4.yml", + "function": {"Fn::Select": [{"Ref": "index"}, {"Ref": "HandlerList"}]}, + "expected": None, + }, + "Unresolved Function with Incorrect Reference in Map": { + "template": "tests/fixtures/templates/example4.yml", + "function": { + "Fn::Select": [ + {"Fn::FindInMap": ["Environments", {"Ref": "Environment"}, "Index"]}, + {"Ref": "Fixture"}, + ] + }, + "expected": None, + }, + } + + for key, values in scenarios.items(): + with self.subTest(case=key, template=values["template"]): + cloudformation = CloudformationTemplate( + values["template"], parameters={"Environment": "development"} + ) + value = IntrinsicFunctions.eval(values["function"], cloudformation.template) + + self.assertEqual(value, values["expected"]) + class TestResource(unittest.TestCase): def test_resource(self): From 350ab84c55636e60cecc36d0a389d52dcf7104c2 Mon Sep 17 00:00:00 2001 From: Luis Henrique Date: Tue, 16 Apr 2024 18:21:03 -0300 Subject: [PATCH 25/32] test: add split function tests --- tests/fixtures/templates/example3.yml | 3 +++ tests/test_cloudformation.py | 28 +++++++++++++++++++++++++++ 2 files changed, 31 insertions(+) diff --git a/tests/fixtures/templates/example3.yml b/tests/fixtures/templates/example3.yml index 159f8361..7680a67f 100644 --- a/tests/fixtures/templates/example3.yml +++ b/tests/fixtures/templates/example3.yml @@ -15,6 +15,9 @@ Parameters: AttributeName: Type: String Default: StageName + Accounts: + Type: String + Default: dev@gmail|dotz@gmail.com Mappings: Environments: diff --git a/tests/test_cloudformation.py b/tests/test_cloudformation.py index 5da536b5..f6150f9a 100644 --- a/tests/test_cloudformation.py +++ b/tests/test_cloudformation.py @@ -400,6 +400,34 @@ def test_select_function(self): self.assertEqual(value, values["expected"]) + def test_split_function(self): + scenarios = { + "Resolved Function": { + "template": "tests/fixtures/templates/example3.yml", + "function": {"Fn::Split": ["|", "dev@gmail|dotz@gmail.com"]}, + "expected": ["dev@gmail", "dotz@gmail.com"], + }, + "Resolved Function with Reference": { + "template": "tests/fixtures/templates/example3.yml", + "function": {"Fn::Split": ["|", {"Ref": "Accounts"}]}, + "expected": ["dev@gmail", "dotz@gmail.com"], + }, + "Unresolved Function with Incorrect Reference": { + "template": "tests/fixtures/templates/example3.yml", + "function": {"Fn::Split": ["|", {"Ref": "AccountList"}]}, + "expected": None, + }, + } + + for key, values in scenarios.items(): + with self.subTest(case=key, template=values["template"]): + cloudformation = CloudformationTemplate( + values["template"], parameters={"Environment": "development"} + ) + value = IntrinsicFunctions.eval(values["function"], cloudformation.template) + + self.assertEqual(value, values["expected"]) + class TestResource(unittest.TestCase): def test_resource(self): From 6b1f7170b466b114ae3214790a3926eea62cd4f1 Mon Sep 17 00:00:00 2001 From: Luis Henrique Date: Tue, 16 Apr 2024 19:29:36 -0300 Subject: [PATCH 26/32] feat: make format --- tests/test_cloudformation.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/test_cloudformation.py b/tests/test_cloudformation.py index f6150f9a..9639a292 100644 --- a/tests/test_cloudformation.py +++ b/tests/test_cloudformation.py @@ -418,7 +418,7 @@ def test_split_function(self): "expected": None, }, } - + for key, values in scenarios.items(): with self.subTest(case=key, template=values["template"]): cloudformation = CloudformationTemplate( From 9521938012b2043674c46cede411adfe014d88fa Mon Sep 17 00:00:00 2001 From: Luis Henrique Date: Tue, 16 Apr 2024 23:06:40 -0300 Subject: [PATCH 27/32] feat: add sub intrinsic function --- faster_sam/cloudformation.py | 86 ++++++++++++++++++++++++++- tests/fixtures/templates/example4.yml | 6 ++ tests/test_cloudformation.py | 50 +++++++++++++++- 3 files changed, 140 insertions(+), 2 deletions(-) diff --git a/faster_sam/cloudformation.py b/faster_sam/cloudformation.py index ccd19034..59ea22f8 100644 --- a/faster_sam/cloudformation.py +++ b/faster_sam/cloudformation.py @@ -589,11 +589,12 @@ def eval(function: Dict[str, Any], template: Dict[str, Any]) -> Any: implemented = ( "Fn::Base64", "Fn::FindInMap", - "Ref", "Fn::GetAtt", "Fn::Join", "Fn::Select", "Fn::Split", + "Fn::Sub", + "Ref", ) fun, val = list(function.items())[0] @@ -619,6 +620,9 @@ def eval(function: Dict[str, Any], template: Dict[str, Any]) -> Any: if "Fn::Split" == fun: return IntrinsicFunctions.split(val, template) + if "Fn::Sub" == fun: + return IntrinsicFunctions.sub(val, template) + if "Ref" == fun: return IntrinsicFunctions.ref(val, template) @@ -843,3 +847,83 @@ def split(value: List[Any], template: Dict[str, Any]) -> Optional[str]: return None return source.split(delimiter) + + @staticmethod + def sub(value: List[Any], template: Dict[str, Any]) -> Optional[str]: + """ + Substitutes intrinsic functions and environment variables in the given value. + + Parameters + ---------- + value : List[Any] + A list containing the string to perform substitutions on and a dictionary + containing variable names and their corresponding values. + template : Dict[str, Any] + A dictionary representing the CloudFormation template. + + Returns + ------- + Optional[str] + The resulting string after performing substitutions, or None if any of the + variables or intrinsic functions could not be resolved. + """ + + pseudo_parameters = [ + "AWS::AccountId", + "AWS::NotificationARNs", + "AWS::NoValue", + "AWS::Partition", + "AWS::Region", + "AWS::StackId", + "AWS::StackName", + "AWS::URLSuffix", + ] + + pattern = r"\${(.*?)}" + + def replace(string: str, matches: List[Any]): + matches = [ + param if param not in pseudo_parameters else param.replace("::", "_") + for param in matches + ] + + for match in matches: + if match in os.environ: + env_var = os.environ[match] + return string.replace(f"${{{match}}}", env_var) + else: + return None + + if isinstance(value, list): + string, var_list = value + + for var_dict in var_list: + var_name, var_value = list(var_dict.items())[0] + + if isinstance(var_name, dict): + var_name = IntrinsicFunctions.eval(var_name, template) + + if var_name is None: + return None + + if isinstance(var_value, dict): + var_value = IntrinsicFunctions.eval(var_value, template) + + if var_value is None: + return None + + if var_name in string: + result = string.replace(f"${{{var_name}}}", str(var_value)) + else: + return None + + matches = re.findall(pattern, result) + + if not matches: + return result + + return replace(result, matches) + else: + matches = re.findall(pattern, value) + + return replace(value, matches) diff --git a/tests/fixtures/templates/example4.yml b/tests/fixtures/templates/example4.yml index 5fca08fc..38188227 100644 --- a/tests/fixtures/templates/example4.yml +++ b/tests/fixtures/templates/example4.yml @@ -15,6 +15,12 @@ Parameters: HandlerList: Type: String Default: [handler, handler2] + Account: + Type: String + Default: 1234 + VarName: + Type: String + Default: account Mappings: Environments: diff --git a/tests/test_cloudformation.py b/tests/test_cloudformation.py index 9639a292..39ad804f 100644 --- a/tests/test_cloudformation.py +++ b/tests/test_cloudformation.py @@ -1,7 +1,8 @@ import io -import unittest +from unittest import mock from contextlib import contextmanager from pathlib import Path +import unittest import yaml @@ -428,6 +429,53 @@ def test_split_function(self): self.assertEqual(value, values["expected"]) + @mock.patch.dict("os.environ", {"account": "1234"}) + def test_sub_function(self): + scenarios = { + "x1": { + "template": "tests/fixtures/templates/example4.yml", + "function": {"Fn::Sub": ["account-${account}", [{"account": {"Ref": "Account"}}]]}, + "expected": "account-1234", + }, + "x2": { + "template": "tests/fixtures/templates/example4.yml", + "function": {"Fn::Sub": ["account-${account}", [{"account": {"Ref": "acc"}}]]}, + "expected": None, + }, + "x3": { + "template": "tests/fixtures/templates/example4.yml", + "function": {"Fn::Sub": "account-${account}"}, + "expected": "account-1234", + }, + "x4": { + "template": "tests/fixtures/templates/example4.yml", + "function": {"Fn::Sub": "account-${acc}"}, + "expected": None, + }, + "x5": { + "template": "tests/fixtures/templates/example4.yml", + "function": {"Fn::Sub": ["account-${account}", [{"value": {"Ref": "Account"}}]]}, + "expected": None, + }, + # "x": { + # "template": "tests/fixtures/templates/example4.yml", + # "function": {"Fn::Sub": + # ["account-${account}", + # [!Ref VarName: {"Ref": "Account"}}]] + # }, + # "expected": "account-1234", + # }, + } + + for key, values in scenarios.items(): + with self.subTest(case=key, template=values["template"]): + cloudformation = CloudformationTemplate( + values["template"], parameters={"Environment": "development"} + ) + value = IntrinsicFunctions.eval(values["function"], cloudformation.template) + + self.assertEqual(value, values["expected"]) + class TestResource(unittest.TestCase): def test_resource(self): From 050e3cc2b467ee41180ccd34e8145361b33a7cdd Mon Sep 17 00:00:00 2001 From: Luis Henrique Date: Tue, 16 Apr 2024 23:12:13 -0300 Subject: [PATCH 28/32] feat: add sub intrinsic function --- tests/test_cloudformation.py | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/tests/test_cloudformation.py b/tests/test_cloudformation.py index 39ad804f..a46f9aec 100644 --- a/tests/test_cloudformation.py +++ b/tests/test_cloudformation.py @@ -432,27 +432,27 @@ def test_split_function(self): @mock.patch.dict("os.environ", {"account": "1234"}) def test_sub_function(self): scenarios = { - "x1": { + "Resolved Function with list of variables": { "template": "tests/fixtures/templates/example4.yml", "function": {"Fn::Sub": ["account-${account}", [{"account": {"Ref": "Account"}}]]}, "expected": "account-1234", }, - "x2": { + "Unresolved Function with nonexistent reference": { "template": "tests/fixtures/templates/example4.yml", "function": {"Fn::Sub": ["account-${account}", [{"account": {"Ref": "acc"}}]]}, "expected": None, }, - "x3": { + "Resolved Function with string value": { "template": "tests/fixtures/templates/example4.yml", "function": {"Fn::Sub": "account-${account}"}, "expected": "account-1234", }, - "x4": { + "Unresolved Function with nonexistent variable": { "template": "tests/fixtures/templates/example4.yml", "function": {"Fn::Sub": "account-${acc}"}, "expected": None, }, - "x5": { + "Unresolved Function with nonexistent variable name": { "template": "tests/fixtures/templates/example4.yml", "function": {"Fn::Sub": ["account-${account}", [{"value": {"Ref": "Account"}}]]}, "expected": None, From a7777b21820c960af43447964eff74b20cddc700 Mon Sep 17 00:00:00 2001 From: Luis Henrique Date: Wed, 17 Apr 2024 10:34:06 -0300 Subject: [PATCH 29/32] feat: update cloudformation templates --- tests/fixtures/templates/example2.yml | 2 -- tests/fixtures/templates/example4.yml | 4 ---- tests/test_cloudformation.py | 1 - 3 files changed, 7 deletions(-) diff --git a/tests/fixtures/templates/example2.yml b/tests/fixtures/templates/example2.yml index 676b51ae..5eff81e0 100644 --- a/tests/fixtures/templates/example2.yml +++ b/tests/fixtures/templates/example2.yml @@ -31,8 +31,6 @@ Globals: - Environments - Ref: Environment - LogLevel - SENDER_ACCOUNT: - Fn::GetAtt: [ HelloWorldFunction, CodeUri ] Resources: ApiGateway: diff --git a/tests/fixtures/templates/example4.yml b/tests/fixtures/templates/example4.yml index 9a183b27..8788f263 100644 --- a/tests/fixtures/templates/example4.yml +++ b/tests/fixtures/templates/example4.yml @@ -27,10 +27,6 @@ Globals: Variables: ENVIRONMENT: Ref: Environment - SENDER_ACCOUNT: - Fn::GetAtt: [ ApiGatewayTwo, Region ] - STAGE_NAME: - Fn::GetAtt: ApiGateway.Region Resources: ApiGateway: diff --git a/tests/test_cloudformation.py b/tests/test_cloudformation.py index 4fc4cbc1..4a909bc3 100644 --- a/tests/test_cloudformation.py +++ b/tests/test_cloudformation.py @@ -212,7 +212,6 @@ def test_list_environment(self): "tests/fixtures/templates/example2.yml": { "ENVIRONMENT": "development", "LOG_LEVEL": "DEBUG", - "SENDER_ACCOUNT": "tests/", }, "tests/fixtures/templates/example3.yml": { "ENVIRONMENT": "development", From 194fcf2679c3532c48f9d48fd0991cf3ee4889aa Mon Sep 17 00:00:00 2001 From: Luis Henrique <101938473+Luis-Henrique@users.noreply.github.com> Date: Wed, 17 Apr 2024 10:37:42 -0300 Subject: [PATCH 30/32] feat: remove file --- swagger.yml | 0 1 file changed, 0 insertions(+), 0 deletions(-) delete mode 100644 swagger.yml diff --git a/swagger.yml b/swagger.yml deleted file mode 100644 index e69de29b..00000000 From a65a5666119eae66e5dbe70f7ba52b3792e70f8a Mon Sep 17 00:00:00 2001 From: Luis Henrique Date: Wed, 17 Apr 2024 11:24:10 -0300 Subject: [PATCH 31/32] fix: update replace function --- faster_sam/cloudformation.py | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/faster_sam/cloudformation.py b/faster_sam/cloudformation.py index 59ea22f8..fa8e7cef 100644 --- a/faster_sam/cloudformation.py +++ b/faster_sam/cloudformation.py @@ -887,13 +887,17 @@ def replace(string: str, matches: List[Any]): for param in matches ] + result_string = string + for match in matches: if match in os.environ: env_var = os.environ[match] - return string.replace(f"${{{match}}}", env_var) + result_string = result_string.replace(f"${{{match}}}", env_var) else: return None + return result_string + if isinstance(value, list): string, var_list = value From 0cf27a0cce9cc94a7106a12805eaac523dd04d1a Mon Sep 17 00:00:00 2001 From: Luis Henrique Date: Wed, 17 Apr 2024 13:13:31 -0300 Subject: [PATCH 32/32] feat: uodate replace function --- faster_sam/cloudformation.py | 63 ++++++++++++++++++------------------ tests/test_cloudformation.py | 36 +++++++++++++++++++++ 2 files changed, 68 insertions(+), 31 deletions(-) diff --git a/faster_sam/cloudformation.py b/faster_sam/cloudformation.py index fa8e7cef..deccf6b4 100644 --- a/faster_sam/cloudformation.py +++ b/faster_sam/cloudformation.py @@ -848,6 +848,36 @@ def split(value: List[Any], template: Dict[str, Any]) -> Optional[str]: return source.split(delimiter) + def replace_placeholders(string: str, matches: List[Any]): + pseudo_parameters = [ + "AWS::AccountId", + "AWS::NotificationARNs", + "AWS::NoValue", + "AWS::Partition", + "AWS::Region", + "AWS::StackId", + "AWS::StackName", + "AWS::URLSuffix", + ] + + result_string = string + + for match in matches: + if match in pseudo_parameters: + if match.replace("::", "_") in os.environ: + env_var = os.environ[match.replace("::", "_")] + result_string = result_string.replace(f"${{{match}}}", env_var) + else: + return None + else: + if match in os.environ: + env_var = os.environ[match] + result_string = result_string.replace(f"${{{match}}}", env_var) + else: + return None + + return result_string + @staticmethod def sub(value: List[Any], template: Dict[str, Any]) -> Optional[str]: """ @@ -867,37 +897,8 @@ def sub(value: List[Any], template: Dict[str, Any]) -> Optional[str]: The resulting string after performing substitutions, or None if any of the variables or intrinsic functions could not be resolved. """ - - pseudo_parameters = [ - "AWS::AccountId", - "AWS::NotificationARNs", - "AWS::NoValue", - "AWS::Partition", - "AWS::Region", - "AWS::StackId", - "AWS::StackName", - "AWS::URLSuffix", - ] - pattern = r"\${(.*?)}" - def replace(string: str, matches: List[Any]): - matches = [ - param if param not in pseudo_parameters else param.replace("::", "_") - for param in matches - ] - - result_string = string - - for match in matches: - if match in os.environ: - env_var = os.environ[match] - result_string = result_string.replace(f"${{{match}}}", env_var) - else: - return None - - return result_string - if isinstance(value, list): string, var_list = value @@ -926,8 +927,8 @@ def replace(string: str, matches: List[Any]): if not matches: return result - return replace(result, matches) + return IntrinsicFunctions.replace_placeholders(result, matches) else: matches = re.findall(pattern, value) - return replace(value, matches) + return IntrinsicFunctions.replace_placeholders(value, matches) diff --git a/tests/test_cloudformation.py b/tests/test_cloudformation.py index ca01948f..f9ac0afa 100644 --- a/tests/test_cloudformation.py +++ b/tests/test_cloudformation.py @@ -428,6 +428,42 @@ def test_split_function(self): self.assertEqual(value, values["expected"]) + @mock.patch.dict( + "os.environ", + { + "AWS_AccountId": "123456789", + "ENVIRONMENT": "dev", + }, + ) + def test_replace_placeholders(self): + scenarios = [ + { + "name": "with env vars", + "string": "account-${AWS::AccountId}-${ENVIRONMENT}", + "matches": ["AWS::AccountId", "ENVIRONMENT"], + "expected_result": "account-123456789-dev", + }, + { + "name": "missing env vars", + "string": "account-${Id}", + "matches": ["Id"], + "expected_result": None, + }, + { + "name": "no matches", + "string": "account-${AWS::AccountId}-${ENVIRONMENT}", + "matches": ["OTHER_VAR"], + "expected_result": None, + }, + ] + + for scenario in scenarios: + with self.subTest(case=scenario["name"]): + result = IntrinsicFunctions.replace_placeholders( + scenario["string"], scenario["matches"] + ) + self.assertEqual(result, scenario["expected_result"]) + @mock.patch.dict("os.environ", {"account": "1234"}) def test_sub_function(self): scenarios = {