Skip to content
New issue

Have a question about this project? # for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “#”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? # to your account

Metadata backend and frontend #19

Merged
merged 6 commits into from
Feb 14, 2023
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
2 changes: 1 addition & 1 deletion backend/Dockerfile
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@

FROM --platform=linux/amd64 public.ecr.aws/lambda/python:3.9

ADD backend backend

COPY requirements.txt .
RUN pip3 install -r requirements.txt --target "${LAMBDA_TASK_ROOT}"
ADD backend backend
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I am curious if this change is intentional.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes, this makes it so that we don't have to run the pip install step every time a file changes in the backend folder. Speeds up deploys a lot. If requirements.txt changes, the pip install will run.

117 changes: 117 additions & 0 deletions backend/backend/handlers/metadata/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,117 @@

import boto3
import json
import logging
import os
import traceback

# region Logging

LOG_LEVEL = os.getenv("LOG_LEVEL", "INFO")
logger = logging.getLogger()

if logger.hasHandlers():
# The Lambda environment pre-configures a handler logging to stderr. If a handler is already configured,
# `.basicConfig` does not execute. Thus we set the level directly.
logger.setLevel(LOG_LEVEL)
else:
logging.basicConfig(level=LOG_LEVEL)

# endregion

def mask_sensitive_data(event):
# remove sensitive data from request object before logging
keys_to_redact = ["authorization"]
result = {}
for k, v in event.items():
if isinstance(v, dict):
result[k] = mask_sensitive_data(v)
elif k in keys_to_redact:
result[k] = "<redacted>"
else:
result[k] = v
return result;

def build_response(http_code, body):
return {
"headers": {
"Cache-Control": "no-cache, no-store", # tell cloudfront and api gateway not to cache the response
"Content-Type": "application/json",
},
"statusCode": http_code,
"body": body,
}


region = os.environ['AWS_REGION']
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think we may not need this, in lambda boto3 automatically picks up the right region

dynamodb = boto3.resource('dynamodb', region_name=region)
table = dynamodb.Table(os.environ['METADATA_STORAGE_TABLE_NAME'])

def to_update_expr(record):

keys= record.keys()
keys_attr_names = ["#f{n}".format(n=x) for x in range(len(keys))]
values_attr_names = [":v{n}".format(n=x) for x in range(len(keys))]

keys_map = {
k: key
for k, key in zip(keys_attr_names, keys)
}
values_map = {
v1: record[v]
for v, v1 in zip(keys, values_attr_names)
}
expr = "SET " + ", ".join([
"{f} = {v}".format(f=f, v=v)
for f,v in zip(keys_attr_names, values_attr_names)
])
return keys_map, values_map, expr



def create_or_update(databaseId, assetId, metadata):
keys_map, values_map, expr = to_update_expr(metadata)
return table.update_item(
Key={
"databaseId": databaseId,
"assetId": assetId,
},
ExpressionAttributeNames=keys_map,
ExpressionAttributeValues=values_map,
UpdateExpression=expr,
)



class ValidationError(Exception):
def __init__(self, code: int, resp: object) -> None:
self.code = code
self.resp = resp


def validate_event(event):
if "pathParameters" not in event or "assetId" not in event['pathParameters']:
raise ValidationError(404, { "error": "missing path parameters"})
if "pathParameters" not in event or "databaseId" not in event['pathParameters']:
raise ValidationError(404, { "error": "missing path parameters"})


def validate_body(event):

if "body" not in event:
raise ValidationError(400, {"error": "missing request body"})

body = json.loads(event['body'])

for req_field in ["metadata", "version"]:
if req_field not in body:
raise ValidationError(400, {"error": "{f} field is missing".format(f=req_field)})

if body['version'] == "1":
for k, v in body['metadata'].items():
if not isinstance(k, str):
raise ValidationError(400, {"error": "metadata version 1 requires string keys and values"})
if not isinstance(v, str):
raise ValidationError(400, {"error": "metadata version 1 requires string keys and values"})

return body
25 changes: 25 additions & 0 deletions backend/backend/handlers/metadata/create.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
import json
import traceback

from backend.handlers.metadata import logger, mask_sensitive_data, build_response, create_or_update, validate_event, validate_body, ValidationError


def lambda_handler(event, context):
logger.info(mask_sensitive_data(event))
try:

validate_event(event)

body = validate_body(event)
databaseId = event['pathParameters']['databaseId']
assetId = event['pathParameters']['assetId']

create_or_update(databaseId, assetId, body['metadata'])

return build_response(200, json.dumps({ "status": "OK" }))
except ValidationError as ex:
logger.info(traceback.format_exc())
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

logger.error?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It's a client input validation error opposed to an application level error. Might be good for us to define how we use different log levels. I've often used conventions like these:

  • debug: chatty details that are helpful to understanding during development
  • info: any messages relating to nominal operation of the application, like a business transaction occurred, client validation failure
  • warn: something that an operator may want to know about for tuning purposes... like, free memory or disk volume usage falling below threshold for safe operation.
  • error: business transaction failure due to server-side faults, if too many of these happen, alert operators
  • fatal: this application is about to crash, wake somebody up if necessary :)

Maybe the ValidationError class could have a different name to increase some clarity.

return build_response(ex.code, json.dumps(ex.resp))
except Exception as ex:
logger.error(traceback.format_exc())
return build_response(500, "Server Error")
33 changes: 33 additions & 0 deletions backend/backend/handlers/metadata/delete.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
import os
import sys
import json
from decimal import Decimal
from backend.common.validators import validate
import traceback

from backend.handlers.metadata import logger, mask_sensitive_data, build_response, ValidationError, table, validate_event


def delete_item(databaseId, assetId):
table.delete_item(
Key={
"databaseId": databaseId,
"assetId": assetId,
},
)

def lambda_handler(event, context):
logger.info(mask_sensitive_data(event))
try:
validate_event(event)
databaseId = event['pathParameters']['databaseId']
assetId = event['pathParameters']['assetId']
delete_item(databaseId, assetId)
response = { "status": "OK", "message": "{assetId} deleted".format(assetId=assetId) }
return build_response(200, json.dumps(response))
except ValidationError as ex:
logger.info(traceback.format_exc())
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

logger.error here too

return build_response(ex.code, json.dumps(ex.resp))
except Exception as ex:
logger.error(traceback.format_exc())
return build_response(500, "Server Error")
39 changes: 39 additions & 0 deletions backend/backend/handlers/metadata/read.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
import os
import sys
import json
from decimal import Decimal
import traceback

from backend.handlers.metadata import logger, mask_sensitive_data, build_response, table, validate_event, ValidationError


def get_metadata(databaseId, assetId):
resp = table.get_item(
Key={
"databaseId": databaseId,
"assetId": assetId,
}
)
if "Item" not in resp:
raise ValidationError(404, "Item Not Found")
return resp['Item']


def lambda_handler(event, context):
logger.info(mask_sensitive_data(event))
try:
validate_event(event)
databaseId = event['pathParameters']['databaseId']
assetId = event['pathParameters']['assetId']

return build_response(200, json.dumps({
"version": "1",
"metadata": get_metadata(databaseId, assetId)
}))

except ValidationError as ex:
logger.info(traceback.format_exc())
return build_response(ex.code, json.dumps(ex.resp))
except Exception as ex:
logger.error(traceback.format_exc())
return build_response(500, "Server Error")
Loading