diff --git a/cdn_lambda/functions/map_to_s3.py b/cdn_lambda/functions/map_to_s3.py deleted file mode 100644 index 752cec8f..00000000 --- a/cdn_lambda/functions/map_to_s3.py +++ /dev/null @@ -1,61 +0,0 @@ -import json -import logging -import os -from datetime import datetime, timezone - -import boto3 - -LOG = logging.getLogger("map-to-s3-lambda") - - -def lambda_handler(event, context): - # pylint: disable=unused-argument - - # This AWS Lambda function must be deployed with DB_TABLE_NAME and - # DB_TABLE_REGION environment variables - table_name = os.getenv("DB_TABLE_NAME") - table_region = os.getenv("DB_TABLE_REGION") - - # Get uri from origin request event - request = event["Records"][0]["cf"]["request"] - web_uri = request["uri"] - - # Query latest item with matching uri from DynamoDB table - LOG.info("Querying '%s' table for '%s'...", table_name, web_uri) - - iso_now = datetime.now(timezone.utc).isoformat(timespec="milliseconds") - - query_result = boto3.client("dynamodb", region_name=table_region).query( - TableName=table_name, - Limit=1, - ScanIndexForward=False, - KeyConditionExpression="web_uri = :u and from_date <= :d", - ExpressionAttributeValues={ - ":u": {"S": web_uri}, - ":d": {"S": str(iso_now)}, - }, - ) - - if query_result["Items"]: - LOG.info("Item found for '%s'", web_uri) - - try: - # Update request uri to point to S3 object key - request["uri"] = query_result["Items"][0]["object_key"]["S"] - - return request - except Exception as err: - LOG.exception( - "Exception occurred while processing %s", - json.dumps(query_result["Items"][0]), - ) - - raise err - else: - LOG.info("No item for '%s'", web_uri) - - # Report 404 to prevent attempts on S3 - return { - "status": "404", - "statusDescription": "Not Found", - } diff --git a/cdn_lambda/functions/map_to_s3/__init__.py b/cdn_lambda/functions/map_to_s3/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/cdn_lambda/functions/map_to_s3/map_to_s3.json b/cdn_lambda/functions/map_to_s3/map_to_s3.json new file mode 100755 index 00000000..d9c4fed3 --- /dev/null +++ b/cdn_lambda/functions/map_to_s3/map_to_s3.json @@ -0,0 +1,6 @@ +{ + "table": { + "name": "exodus", + "region": "us-east-1" + } +} diff --git a/cdn_lambda/functions/map_to_s3/map_to_s3.py b/cdn_lambda/functions/map_to_s3/map_to_s3.py new file mode 100755 index 00000000..96d392aa --- /dev/null +++ b/cdn_lambda/functions/map_to_s3/map_to_s3.py @@ -0,0 +1,90 @@ +import json +import logging +from datetime import datetime, timezone + +import boto3 + +LOG = logging.getLogger("map-to-s3-lambda") + + +class LambdaClient(object): + def __init__(self, conf_file="map_to_s3.json"): + self._conf_file = conf_file + + self._conf = None + self._db_client = None + + @property + def conf(self): + if not self._conf: + with open(self._conf_file, "r") as json_file: + self._conf = json.load(json_file) + + return self._conf + + @property + def db_client(self): + if not self._db_client: + self._db_client = boto3.client( + "dynamodb", region_name=self.conf["table"]["region"] + ) + + return self._db_client + + def handler(self, event, context): + # pylint: disable=unused-argument + + request = event["Records"][0]["cf"]["request"] + + LOG.info( + "Querying '%s' table for '%s'...", + self.conf["table"]["name"], + request["uri"], + ) + + query_result = self.db_client.query( + TableName=self.conf["table"]["name"], + Limit=1, + ScanIndexForward=False, + KeyConditionExpression="web_uri = :u and from_date <= :d", + ExpressionAttributeValues={ + ":u": {"S": request["uri"]}, + ":d": { + "S": str( + datetime.now(timezone.utc).isoformat( + timespec="milliseconds" + ) + ) + }, + }, + ) + + if query_result["Items"]: + LOG.info("Item found for '%s'", request["uri"]) + + try: + # Update request uri to point to S3 object key + request["uri"] = ( + "/" + query_result["Items"][0]["object_key"]["S"] + ) + + return request + except Exception as err: + LOG.exception( + "Exception occurred while processing %s", + json.dumps(query_result["Items"][0]), + ) + + raise err + else: + LOG.info("No item found for '%s'", request["uri"]) + + # Report 404 to prevent attempts on S3 + return { + "status": "404", + "statusDescription": "Not Found", + } + + +# Make handler available at module level +lambda_handler = LambdaClient().handler diff --git a/docs/functions/map_to_s3.rst b/docs/functions/map_to_s3.rst index fefde8ed..272439e3 100644 --- a/docs/functions/map_to_s3.rst +++ b/docs/functions/map_to_s3.rst @@ -14,24 +14,22 @@ Deployment ---------- This function may be deployed like any other AWS Lambda function using the -following event and environment variables. +following event and configuration file. Event ^^^^^ The event for this function must be a CloudFront distribution origin-request to an S3 bucket. -Environment Variables -^^^^^^^^^^^^^^^^^^^^^ -- DB_TABLE_NAME - (Required) +Configuration +^^^^^^^^^^^^^ +The map_to_s3 function must be deployed with the map_to_s3.json configuration +file. - The name of the DynamoDB table from which to derive mapping. -- DB_TABLE_REGION - (Optional) +- table + The DynamoDB table from which to derive path to key mapping. - The AWS region in which the table resides. - If omitted, the region used is that of the function. - -`AWS Lambda Environment Variables -`_ + - name + The name of the DynamoDB table. + - region + The AWS region in which the table resides. diff --git a/tests/functions/test_map_to_s3.py b/tests/functions/test_map_to_s3.py index 30090cb7..69c14d2d 100644 --- a/tests/functions/test_map_to_s3.py +++ b/tests/functions/test_map_to_s3.py @@ -1,15 +1,15 @@ -from cdn_lambda.functions.map_to_s3 import lambda_handler -import os +from cdn_lambda.functions.map_to_s3.map_to_s3 import LambdaClient import pytest import mock import json TEST_PATH = "www.example.com/content/file.ext" MOCKED_DT = "2020-02-17T15:38:05.864+00:00" +CONF_PATH = "cdn_lambda/functions/map_to_s3/map_to_s3.json" @mock.patch("boto3.client") -@mock.patch("cdn_lambda.functions.map_to_s3.datetime") +@mock.patch("cdn_lambda.functions.map_to_s3.map_to_s3.datetime") def test_map_to_s3(mocked_datetime, mocked_boto3_client): mocked_datetime.now().isoformat.return_value = MOCKED_DT mocked_boto3_client().query.return_value = { @@ -22,32 +22,28 @@ def test_map_to_s3(mocked_datetime, mocked_boto3_client): ] } - env_vars = {"DB_TABLE_NAME": "test_table", "DB_TABLE_REGION": "us-east-1"} event = {"Records": [{"cf": {"request": {"uri": TEST_PATH}}}]} - with mock.patch.dict(os.environ, env_vars): - request = lambda_handler(event, context=None) + request = LambdaClient(conf_file=CONF_PATH).handler(event, context=None) - assert request == {"uri": "e4a3f2sum"} + assert request == {"uri": "/e4a3f2sum"} @mock.patch("boto3.client") -@mock.patch("cdn_lambda.functions.map_to_s3.datetime") +@mock.patch("cdn_lambda.functions.map_to_s3.map_to_s3.datetime") def test_map_to_s3_no_item(mocked_datetime, mocked_boto3_client): mocked_datetime.now().isoformat.return_value = MOCKED_DT mocked_boto3_client().query.return_value = {"Items": []} - env_vars = {"DB_TABLE_NAME": "test_table", "DB_TABLE_REGION": "us-east-1"} event = {"Records": [{"cf": {"request": {"uri": TEST_PATH}}}]} - with mock.patch.dict(os.environ, env_vars): - request = lambda_handler(event, context=None) + request = LambdaClient(conf_file=CONF_PATH).handler(event, context=None) assert request == {"status": "404", "statusDescription": "Not Found"} @mock.patch("boto3.client") -@mock.patch("cdn_lambda.functions.map_to_s3.datetime") +@mock.patch("cdn_lambda.functions.map_to_s3.map_to_s3.datetime") def test_map_to_s3_invalid_item(mocked_datetime, mocked_boto3_client, caplog): mocked_datetime.now().isoformat.return_value = MOCKED_DT mocked_boto3_client().query.return_value = { @@ -59,12 +55,10 @@ def test_map_to_s3_invalid_item(mocked_datetime, mocked_boto3_client, caplog): ] } - env_vars = {"DB_TABLE_NAME": "test_table", "DB_TABLE_REGION": "us-east-1"} event = {"Records": [{"cf": {"request": {"uri": TEST_PATH}}}]} - with mock.patch.dict(os.environ, env_vars): - with pytest.raises(KeyError): - lambda_handler(event, context=None) + with pytest.raises(KeyError): + LambdaClient(conf_file=CONF_PATH).handler(event, context=None) assert ( "Exception occurred while processing %s"