Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
61 changes: 61 additions & 0 deletions cdn_lambda/functions/map_to_s3.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,61 @@
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",
}
8 changes: 8 additions & 0 deletions docs/function-reference.rst
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
Function Reference
==================

.. toctree::
:maxdepth: 1
:caption: Contents:

functions/map_to_s3
37 changes: 37 additions & 0 deletions docs/functions/map_to_s3.rst
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
map_to_s3
=========

This function provides mapping from a web URI, delivered by an AWS CloudFront
event, to the key of an object in an AWS S3 bucket.

The mapping between the URI and the object key is defined by the provided AWS
DynamoDB table this function is deployed with.

Required schemas for the DynamoDB table and S3 bucket can be found in the
:doc:`schema reference <../schema-reference>`.

Deployment
----------

This function may be deployed like any other AWS Lambda function using the
following event and environment variables.

Event
^^^^^
The event for this function must be a CloudFront distribution origin-request to
an S3 bucket.

Environment Variables
^^^^^^^^^^^^^^^^^^^^^
- DB_TABLE_NAME
(Required)

The name of the DynamoDB table from which to derive mapping.
- DB_TABLE_REGION
(Optional)

The AWS region in which the table resides.
If omitted, the region used is that of the function.

`AWS Lambda Environment Variables
<https://docs.aws.amazon.com/lambda/latest/dg/configuration-envvars.html>`_
5 changes: 4 additions & 1 deletion docs/index.rst
Original file line number Diff line number Diff line change
Expand Up @@ -5,4 +5,7 @@ AWS Lambda functions for Red Hat's Content Delivery Network

.. toctree::
:maxdepth: 2
:caption: Contents:
:caption: Contents:

function-reference
schema-reference
8 changes: 8 additions & 0 deletions docs/schema-reference.rst
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
Schema Reference
================

.. toctree::
:maxdepth: 2
:caption: Contents:

schemas/map_to_s3-schema
41 changes: 41 additions & 0 deletions docs/schemas/map_to_s3-schema.rst
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
map_to_s3 Schemas
=================

The following schemas are required of AWS S3 bucket objects and DynamoDB table
items for use with the map_to_s3 AWS Lambda function.

S3 Bucket Schema
----------------

Objects should be stored in the origin S3 bucket using the file’s sha256
checksum as their key.

DynamoDB Table Schema
---------------------

DynamoDB table items must possess the following keys and attributes.

Additional attributes are supported by the no-SQL model and may be used as
needed.

Keys
^^^^
- web_uri
(Primary)

A logical path to the desired content, excluding the hostname,
i.e., "/content/place/somepic.png".

- from_date
(Sort)

The datetime at which the content is made available, i.e.,
"2020-02-17T20:48:13.037+00:00".

Only content sooner than or equal to the current date and time may be
retrieved from the origin.

Attributes
^^^^^^^^^^
- object_key
The key of the file object stored in the origin S3 bucket.
1 change: 1 addition & 0 deletions requirements.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
boto3
3 changes: 2 additions & 1 deletion test-requirements.txt
Original file line number Diff line number Diff line change
@@ -1 +1,2 @@
pytest
pytest
mock
78 changes: 78 additions & 0 deletions tests/functions/test_map_to_s3.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,78 @@
from cdn_lambda.functions.map_to_s3 import lambda_handler
import os
import pytest
import mock
import json

TEST_PATH = "www.example.com/content/file.ext"
MOCKED_DT = "2020-02-17T15:38:05.864+00:00"


@mock.patch("boto3.client")
@mock.patch("cdn_lambda.functions.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 = {
"Items": [
{
"web_uri": {"S": TEST_PATH},
"from_date": {"S": "2020-02-17T00:00:00.000+00:00"},
"object_key": {"S": "e4a3f2sum"},
}
]
}

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)

assert request == {"uri": "e4a3f2sum"}


@mock.patch("boto3.client")
@mock.patch("cdn_lambda.functions.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)

assert request == {"status": "404", "statusDescription": "Not Found"}


@mock.patch("boto3.client")
@mock.patch("cdn_lambda.functions.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 = {
"Items": [
{
"web_uri": {"S": TEST_PATH},
"from_date": {"S": "2020-02-17T00:00:00.000+00:00"},
}
]
}

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)

assert (
"Exception occurred while processing %s"
% json.dumps(
{
"web_uri": {"S": TEST_PATH},
"from_date": {"S": "2020-02-17T00:00:00.000+00:00"},
}
)
in caplog.text
)