From 25ec6a6f0a467eb1464d2c2e702a99618a3d18f7 Mon Sep 17 00:00:00 2001 From: Guillaume Charest <1690085+gcharest@users.noreply.github.com> Date: Thu, 3 Oct 2024 18:26:58 +0000 Subject: [PATCH 1/5] feat: add aws lambdas integration support --- app/integrations/aws/lambdas.py | 38 ++++++++++++++++++++++ app/tests/integrations/aws/test_lambas.py | 39 +++++++++++++++++++++++ 2 files changed, 77 insertions(+) create mode 100644 app/integrations/aws/lambdas.py create mode 100644 app/tests/integrations/aws/test_lambas.py diff --git a/app/integrations/aws/lambdas.py b/app/integrations/aws/lambdas.py new file mode 100644 index 00000000..9df90efe --- /dev/null +++ b/app/integrations/aws/lambdas.py @@ -0,0 +1,38 @@ +import logging +from integrations.aws.client import execute_aws_api_call, handle_aws_api_errors + +logger = logging.getLogger(__name__) + + +@handle_aws_api_errors +def list_functions(): + """List all Lambda functions. + + Returns: + list: A list of Lambda functions. + """ + return execute_aws_api_call("lambda", "list_functions", paginated=True, keys=["Functions"]) + + +@handle_aws_api_errors +def list_layers(): + """List all Lambda layers. + + Returns: + list: A list of Lambda layers. + """ + return execute_aws_api_call("lambda", "list_layers", paginated=True, keys=["Layers"]) + + +@handle_aws_api_errors +def get_layer_version(layer_name, version_number): + """Get a Lambda layer version. + + Args: + layer_name (str): The name of the layer. + version_number (int): The version number. + + Returns: + dict: The Lambda layer version. + """ + return execute_aws_api_call("lambda", "get_layer_version", LayerName=layer_name, VersionNumber=version_number) \ No newline at end of file diff --git a/app/tests/integrations/aws/test_lambas.py b/app/tests/integrations/aws/test_lambas.py new file mode 100644 index 00000000..90783a4d --- /dev/null +++ b/app/tests/integrations/aws/test_lambas.py @@ -0,0 +1,39 @@ +from unittest.mock import patch, MagicMock +from integrations.aws.lambdas import list_functions, list_layers, get_layer_version + + +@patch("integrations.aws.lambdas.execute_aws_api_call") +def test_list_functions(mock_execute_aws_api_call): + mock_execute_aws_api_call.return_value = [ + {"FunctionName": "function1"}, + {"FunctionName": "function2"}, + ] + result = list_functions() + mock_execute_aws_api_call.assert_called_once_with("lambda", "list_functions", paginated=True, keys=["Functions"]) + assert result == [ + {"FunctionName": "function1"}, + {"FunctionName": "function2"}, + ] + + +@patch("integrations.aws.lambdas.execute_aws_api_call") +def test_list_layers(mock_execute_aws_api_call): + mock_execute_aws_api_call.return_value = [ + {"LayerName": "layer1", "LatestMatchingVersion": {"Version": 1}}, + {"LayerName": "layer2", "LatestMatchingVersion": {"Version": 23}}, + ] + + result = list_layers() + mock_execute_aws_api_call.assert_called_once_with("lambda", "list_layers", paginated=True, keys=["Layers"]) + assert result == [ + {"LayerName": "layer1", "LatestMatchingVersion": {"Version": 1}}, + {"LayerName": "layer2", "LatestMatchingVersion": {"Version": 23}}, + ] + + +@patch("integrations.aws.lambdas.execute_aws_api_call") +def test_get_layer_version(mock_execute_aws_api_call): + mock_execute_aws_api_call.return_value = {"LayerName": "layer1", "Version": 1} + result = get_layer_version("layer1", 1) + mock_execute_aws_api_call.assert_called_once_with("lambda", "get_layer_version", LayerName="layer1", VersionNumber=1) + assert result == {"LayerName": "layer1", "Version": 1} \ No newline at end of file From c96c194a43f2ae3be24020c0cb54edaa96f1c6aa Mon Sep 17 00:00:00 2001 From: Guillaume Charest <1690085+gcharest@users.noreply.github.com> Date: Thu, 3 Oct 2024 18:39:56 +0000 Subject: [PATCH 2/5] fix: fmt --- app/integrations/aws/lambdas.py | 15 ++++++++++++--- app/tests/integrations/aws/test_lambas.py | 14 ++++++++++---- 2 files changed, 22 insertions(+), 7 deletions(-) diff --git a/app/integrations/aws/lambdas.py b/app/integrations/aws/lambdas.py index 9df90efe..86d4ff9f 100644 --- a/app/integrations/aws/lambdas.py +++ b/app/integrations/aws/lambdas.py @@ -11,7 +11,9 @@ def list_functions(): Returns: list: A list of Lambda functions. """ - return execute_aws_api_call("lambda", "list_functions", paginated=True, keys=["Functions"]) + return execute_aws_api_call( + "lambda", "list_functions", paginated=True, keys=["Functions"] + ) @handle_aws_api_errors @@ -21,7 +23,9 @@ def list_layers(): Returns: list: A list of Lambda layers. """ - return execute_aws_api_call("lambda", "list_layers", paginated=True, keys=["Layers"]) + return execute_aws_api_call( + "lambda", "list_layers", paginated=True, keys=["Layers"] + ) @handle_aws_api_errors @@ -35,4 +39,9 @@ def get_layer_version(layer_name, version_number): Returns: dict: The Lambda layer version. """ - return execute_aws_api_call("lambda", "get_layer_version", LayerName=layer_name, VersionNumber=version_number) \ No newline at end of file + return execute_aws_api_call( + "lambda", + "get_layer_version", + LayerName=layer_name, + VersionNumber=version_number, + ) diff --git a/app/tests/integrations/aws/test_lambas.py b/app/tests/integrations/aws/test_lambas.py index 90783a4d..5d286123 100644 --- a/app/tests/integrations/aws/test_lambas.py +++ b/app/tests/integrations/aws/test_lambas.py @@ -9,7 +9,9 @@ def test_list_functions(mock_execute_aws_api_call): {"FunctionName": "function2"}, ] result = list_functions() - mock_execute_aws_api_call.assert_called_once_with("lambda", "list_functions", paginated=True, keys=["Functions"]) + mock_execute_aws_api_call.assert_called_once_with( + "lambda", "list_functions", paginated=True, keys=["Functions"] + ) assert result == [ {"FunctionName": "function1"}, {"FunctionName": "function2"}, @@ -24,7 +26,9 @@ def test_list_layers(mock_execute_aws_api_call): ] result = list_layers() - mock_execute_aws_api_call.assert_called_once_with("lambda", "list_layers", paginated=True, keys=["Layers"]) + mock_execute_aws_api_call.assert_called_once_with( + "lambda", "list_layers", paginated=True, keys=["Layers"] + ) assert result == [ {"LayerName": "layer1", "LatestMatchingVersion": {"Version": 1}}, {"LayerName": "layer2", "LatestMatchingVersion": {"Version": 23}}, @@ -35,5 +39,7 @@ def test_list_layers(mock_execute_aws_api_call): def test_get_layer_version(mock_execute_aws_api_call): mock_execute_aws_api_call.return_value = {"LayerName": "layer1", "Version": 1} result = get_layer_version("layer1", 1) - mock_execute_aws_api_call.assert_called_once_with("lambda", "get_layer_version", LayerName="layer1", VersionNumber=1) - assert result == {"LayerName": "layer1", "Version": 1} \ No newline at end of file + mock_execute_aws_api_call.assert_called_once_with( + "lambda", "get_layer_version", LayerName="layer1", VersionNumber=1 + ) + assert result == {"LayerName": "layer1", "Version": 1} From b672ee61c8d26b6c86f7600b107072c46ec58d99 Mon Sep 17 00:00:00 2001 From: Guillaume Charest <1690085+gcharest@users.noreply.github.com> Date: Thu, 3 Oct 2024 18:40:13 +0000 Subject: [PATCH 3/5] feat: add command for aws lambda --- app/modules/aws/lambdas.py | 74 ++++++++++ app/tests/modules/aws/test_aws_lambdas.py | 158 ++++++++++++++++++++++ 2 files changed, 232 insertions(+) create mode 100644 app/modules/aws/lambdas.py create mode 100644 app/tests/modules/aws/test_aws_lambdas.py diff --git a/app/modules/aws/lambdas.py b/app/modules/aws/lambdas.py new file mode 100644 index 00000000..6a660b55 --- /dev/null +++ b/app/modules/aws/lambdas.py @@ -0,0 +1,74 @@ +from slack_sdk.web import WebClient +from slack_bolt import Respond + +from integrations.aws import lambdas as aws_lambdas + +help_text = """ +\n *AWS Lambda*: +\n • `/aws lambda functions` - List all Lambda functions. +\n • `/aws lambda layers` - List all Lambda layers. +""" + + +def command_handler(client: WebClient, body, respond: Respond, args, logger): + """Handle the command. + + Args: + client (Slack WebClient): The Slack client. + body (dict): The request body. + respond (function): The function to respond to the request. + args (list[str]): The list of arguments. + logger (Logger): The logger. + """ + + action = args.pop(0) if args else "" + + match action: + case "help" | "aide": + respond(help_text) + case "functions" | "function": + request_list_functions(client, body, respond, logger) + case "layers" | "layer": + request_list_layers(client, body, respond, logger) + case _: + respond("Invalid command. Type `/aws lambda help` for more information.") + + +def request_list_functions(client: WebClient, body, respond: Respond, logger): + """List all Lambda functions. + + Args: + client (Slack WebClient): The Slack client. + body (dict): The request body. + respond (function): The function to respond to the request. + """ + respond("Fetching Lambda functions...") + response = aws_lambdas.list_functions() + if response: + logger.info(response) + function_string = "" + for function in response: + function_string += f"\n • {function['FunctionName']}" + respond(f"Lambda functions found:\n{function_string}") + else: + respond("Lambda functions management is currently disabled.") + + +def request_list_layers(client: WebClient, body, respond: Respond, logger): + """List all Lambda layers. + + Args: + client (Slack WebClient): The Slack client. + body (dict): The request body. + respond (function): The function to respond to the request. + """ + response = aws_lambdas.list_layers() + respond("Fetching Lambda layers...") + if response: + logger.info(response) + response_string = "" + for layer in response: + response_string += f"\n • {layer['LayerName']} " + respond(f"Lambda layers found:\n{response_string}") + else: + respond("Lambda layers management is currently disabled.") diff --git a/app/tests/modules/aws/test_aws_lambdas.py b/app/tests/modules/aws/test_aws_lambdas.py new file mode 100644 index 00000000..3256e697 --- /dev/null +++ b/app/tests/modules/aws/test_aws_lambdas.py @@ -0,0 +1,158 @@ +from unittest.mock import patch, MagicMock +from slack_sdk.web import WebClient +from slack_bolt import Respond +from integrations.aws import lambdas as aws_lambdas +from modules.aws import lambdas + + +def test_aws_lambdas_command_handles_empty_command(): + client = MagicMock() + body = MagicMock() + respond = MagicMock() + args = "" + logger = MagicMock() + + lambdas.command_handler(client, body, respond, args, logger) + + respond.assert_called_once_with( + "Invalid command. Type `/aws lambda help` for more information." + ) + logger.info.assert_not_called() + + +def test_aws_lambdas_command_handles_help_command(): + client = MagicMock() + body = MagicMock() + respond = MagicMock() + args = ["help"] + logger = MagicMock() + + lambdas.command_handler(client, body, respond, args, logger) + + respond.assert_called_once_with(lambdas.help_text) + logger.info.assert_not_called() + + +@patch("integrations.aws.lambdas.list_functions") +def test_aws_lambdas_command_handles_functions_command(mock_list_functions): + client = MagicMock() + body = MagicMock() + respond = MagicMock() + args = ["functions"] + logger = MagicMock() + + mock_list_functions.return_value = [{"FunctionName": "test-function"}] + + lambdas.command_handler(client, body, respond, args, logger) + + respond.assert_any_call("Fetching Lambda functions...") + respond.assert_any_call("Lambda functions found:\n\n • test-function") + logger.info.assert_called_once_with([{"FunctionName": "test-function"}]) + + +@patch("integrations.aws.lambdas.list_layers") +def test_aws_lambdas_command_handles_layers_command(mock_list_layers): + client = MagicMock() + body = MagicMock() + respond = MagicMock() + args = ["layers"] + logger = MagicMock() + + mock_list_layers.return_value = [ + { + "LayerName": "aws-sentinel-connector-layer", + "LatestMatchingVersion": {"Version": 163}, + } + ] + + lambdas.command_handler(client, body, respond, args, logger) + + respond.assert_any_call("Fetching Lambda layers...") + respond.assert_any_call( + "Lambda layers found:\n\n • aws-sentinel-connector-layer " + ) + logger.info.assert_called_once_with( + [ + { + "LayerName": "aws-sentinel-connector-layer", + "LatestMatchingVersion": {"Version": 163}, + } + ] + ) + + +@patch("integrations.aws.lambdas.list_functions") +def test_request_list_functions_handles_empty_response(mock_list_functions): + client = MagicMock() + body = MagicMock() + respond = MagicMock() + logger = MagicMock() + + mock_list_functions.return_value = [] + + lambdas.request_list_functions(client, body, respond, logger) + + respond.assert_any_call("Fetching Lambda functions...") + logger.info.assert_not_called() + + +@patch("integrations.aws.lambdas.list_functions") +def test_request_list_functions_handles_non_empty_response(mock_list_functions): + client = MagicMock() + body = MagicMock() + respond = MagicMock() + logger = MagicMock() + + mock_list_functions.return_value = [{"FunctionName": "test-function"}] + + lambdas.request_list_functions(client, body, respond, logger) + + respond.assert_any_call("Fetching Lambda functions...") + respond.assert_any_call("Lambda functions found:\n\n • test-function") + logger.info.assert_called_once_with([{"FunctionName": "test-function"}]) + + +@patch("integrations.aws.lambdas.list_layers") +def test_request_list_layers_handles_empty_response(mock_list_layers): + client = MagicMock() + body = MagicMock() + respond = MagicMock() + logger = MagicMock() + + mock_list_layers.return_value = [] + + lambdas.request_list_layers(client, body, respond, logger) + + respond.assert_any_call("Fetching Lambda layers...") + respond.assert_any_call("Lambda layers management is currently disabled.") + logger.info.assert_not_called() + + +@patch("integrations.aws.lambdas.list_layers") +def test_request_list_layers_handles_non_empty_response(mock_list_layers): + client = MagicMock() + body = MagicMock() + respond = MagicMock() + logger = MagicMock() + + mock_list_layers.return_value = [ + { + "LayerName": "aws-sentinel-connector-layer", + "LatestMatchingVersion": {"Version": 163}, + } + ] + + lambdas.request_list_layers(client, body, respond, logger) + + respond.assert_any_call("Fetching Lambda layers...") + respond.assert_any_call( + "Lambda layers found:\n\n • aws-sentinel-connector-layer " + ) + logger.info.assert_called_once_with( + [ + { + "LayerName": "aws-sentinel-connector-layer", + "LatestMatchingVersion": {"Version": 163}, + } + ] + ) From ab33fd7adf8e3656c6452372266332ee815d695f Mon Sep 17 00:00:00 2001 From: Guillaume Charest <1690085+gcharest@users.noreply.github.com> Date: Thu, 3 Oct 2024 18:40:41 +0000 Subject: [PATCH 4/5] fix: update aws command with lambdas support --- app/modules/aws/aws.py | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/app/modules/aws/aws.py b/app/modules/aws/aws.py index 143f165a..abf9a559 100644 --- a/app/modules/aws/aws.py +++ b/app/modules/aws/aws.py @@ -16,7 +16,7 @@ from integrations.aws.organizations import get_account_id_by_name from integrations.aws import identity_store from integrations.slack import commands as slack_commands -from modules.aws import aws_access_requests, aws_account_health, groups, users +from modules.aws import aws_access_requests, aws_account_health, groups, users, lambdas PREFIX = os.environ.get("PREFIX", "") AWS_ADMIN_GROUPS = os.environ.get("AWS_ADMIN_GROUPS", "sre-ifs@cds-snc.ca").split(",") @@ -33,6 +33,8 @@ \n ``: `sync`, `list` \n ``: name of the group | nom du groupe (sync only) \n Usage: `/aws groups sync`, `/aws groups sync group-name` or/ou `/aws groups list` +\n `/aws lambdas ` +\n - Manage AWS Lambda functions | Gérer les fonctions Lambda AWS \n `/aws help | aide` \n - Show this help text | montre le dialogue d'aide \n `/aws health` @@ -92,6 +94,8 @@ def aws_command( users.command_handler(client, body, respond, args, logger) case "groups": groups.command_handler(client, body, respond, args, logger) + case "lambda" | "lambdas": + lambdas.command_handler(client, body, respond, args, logger) case _: respond( f"Unknown command: `{action}`. Type `/aws help` to see a list of commands.\n" From 3f8477941c2712c8d315072f1dc4696dd10b4de3 Mon Sep 17 00:00:00 2001 From: Guillaume Charest <1690085+gcharest@users.noreply.github.com> Date: Thu, 3 Oct 2024 18:45:58 +0000 Subject: [PATCH 5/5] fix: lint --- app/tests/integrations/aws/test_lambas.py | 2 +- app/tests/modules/aws/test_aws_lambdas.py | 3 --- 2 files changed, 1 insertion(+), 4 deletions(-) diff --git a/app/tests/integrations/aws/test_lambas.py b/app/tests/integrations/aws/test_lambas.py index 5d286123..2659fbd3 100644 --- a/app/tests/integrations/aws/test_lambas.py +++ b/app/tests/integrations/aws/test_lambas.py @@ -1,4 +1,4 @@ -from unittest.mock import patch, MagicMock +from unittest.mock import patch from integrations.aws.lambdas import list_functions, list_layers, get_layer_version diff --git a/app/tests/modules/aws/test_aws_lambdas.py b/app/tests/modules/aws/test_aws_lambdas.py index 3256e697..c33bba33 100644 --- a/app/tests/modules/aws/test_aws_lambdas.py +++ b/app/tests/modules/aws/test_aws_lambdas.py @@ -1,7 +1,4 @@ from unittest.mock import patch, MagicMock -from slack_sdk.web import WebClient -from slack_bolt import Respond -from integrations.aws import lambdas as aws_lambdas from modules.aws import lambdas