Skip to content

Commit

Permalink
I-ALiRT: Add query api to s3 bucket (#398)
Browse files Browse the repository at this point in the history
* I-ALiRT: Add query api to s3 bucket
  • Loading branch information
laspsandoval authored Dec 3, 2024
1 parent 1b7ae8b commit c46c59c
Show file tree
Hide file tree
Showing 5 changed files with 265 additions and 2 deletions.
86 changes: 86 additions & 0 deletions sds_data_manager/constructs/ialirt_api_manager_construct.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,86 @@
"""Configure the I-ALiRT API Manager."""

import aws_cdk as cdk
from aws_cdk import aws_iam as iam
from aws_cdk import aws_lambda as lambda_
from constructs import Construct

from .api_gateway_construct import ApiGateway


class IalirtApiManager(Construct):
"""Construct for API Management."""

def __init__(
self,
scope: Construct,
construct_id: str,
code: lambda_.Code,
api: ApiGateway,
env: cdk.Environment,
data_bucket,
vpc,
layers: list,
**kwargs,
) -> None:
"""Initialize the SdsApiManagerConstruct.
Parameters
----------
scope : obj
Parent construct
construct_id : str
A unique string identifier for this construct
code : lambda_.Code
Lambda code bundle
api : obj
The APIGateway stack
env : obj
The CDK environment
data_bucket : obj
The data bucket
vpc : obj
The VPC
layers : list
List of Lambda layers arns
kwargs : dict
Keyword arguments
"""
super().__init__(scope, construct_id, **kwargs)

s3_read_policy = iam.PolicyStatement(
effect=iam.Effect.ALLOW,
actions=["s3:ListBucket", "s3:GetObject"],
resources=[
data_bucket.bucket_arn,
f"{data_bucket.bucket_arn}/*",
],
)

# query API lambda
query_api_lambda = lambda_.Function(
self,
id="IAlirtCodeQueryAPILambda",
function_name="query-api-handler",
code=code,
handler="IAlirtCode.ialirt_query_api.lambda_handler",
runtime=lambda_.Runtime.PYTHON_3_12,
timeout=cdk.Duration.minutes(1),
memory_size=1000,
allow_public_subnet=True,
vpc=vpc,
environment={
"S3_BUCKET": data_bucket.bucket_name,
"REGION": env.region,
},
layers=layers,
architecture=lambda_.Architecture.ARM_64,
)

query_api_lambda.add_to_role_policy(s3_read_policy)

api.add_route(
route="ialirt-log-query",
http_method="GET",
lambda_function=query_api_lambda,
)
10 changes: 9 additions & 1 deletion sds_data_manager/constructs/lambda_layer_construct.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,13 +2,15 @@

import aws_cdk as cdk
from aws_cdk import aws_lambda as lambda_
from constructs import Construct


class IMAPLambdaLayer(lambda_.LayerVersion):
"""Lambda Layer."""

def __init__(
self,
scope: Construct,
id: str,
layer_dependencies_dir: str,
runtime=lambda_.Runtime.PYTHON_3_12,
Expand All @@ -22,6 +24,8 @@ def __init__(
Parameters
----------
scope : Construct
The App object in which to create this Construct
id : str
A unique string identifier for this construct
layer_dependencies_dir : str
Expand All @@ -47,5 +51,9 @@ def __init__(
)

super().__init__(
id=f"{id}-Layer", code=code_bundle, compatible_runtimes=[runtime], **kwargs
scope,
id=f"{id}-Layer",
code=code_bundle,
compatible_runtimes=[runtime],
**kwargs,
)
90 changes: 90 additions & 0 deletions sds_data_manager/lambda_code/IAlirtCode/ialirt_query_api.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,90 @@
"""Define lambda to support the download API."""

import json
import logging
import os
from datetime import datetime

import boto3
import botocore

# Logger setup
logger = logging.getLogger(__name__)
logger.setLevel(logging.INFO)


def lambda_handler(event, context):
"""Entry point to the query API lambda.
Parameters
----------
event : dict
The JSON formatted document with the data required for the
lambda function to process
context : LambdaContext
This object provides methods and properties that provide
information about the invocation, function,
and runtime environment.
Notes
-----
Based on filename flight_iois_X.log.YYYY-DOYTHH:MM:SS.ssssss.txt.
This is the log file produced by IOIS for each instance.
Example
-------
Below is an event example:
{
"queryStringParameters": {
"year": "2024",
"doy": "141",
"instance": "1"
}
}
"""
logger.info(f"Event: {event}")
logger.info(f"Context: {context}")

logger.info("Received event: " + json.dumps(event, indent=2))

query_params = event["queryStringParameters"]
year = query_params.get("year")
doy = query_params.get("doy")
instance = query_params.get("instance")

try:
day = datetime.strptime(f"{year}{doy}", "%Y%j")
except ValueError:
return {
"statusCode": 400,
"headers": {"Content-Type": "application/json"},
"body": json.dumps(
{"error": "Invalid year or day format. Use YYYY and DOY."}
),
}

prefix = day.strftime(f"logs/flight_iois_{instance}.log.%Y-%j")

bucket = os.getenv("S3_BUCKET")
region = os.getenv("REGION")

s3_client = boto3.client(
"s3",
region_name=region,
config=botocore.client.Config(signature_version="s3v4"),
)

response = s3_client.list_objects_v2(Bucket=bucket, Prefix=prefix)
files = []

for obj in response.get("Contents", []):
filename = obj["Key"].split("/")[-1]
files.append(filename)

response = {
"statusCode": 200,
"headers": {"Content-Type": "application/json"},
"body": json.dumps({"files": files}),
}

return response
33 changes: 32 additions & 1 deletion sds_data_manager/utils/stackbuilder.py
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@
data_bucket_construct,
database_construct,
efs_construct,
ialirt_api_manager_construct,
ialirt_bucket_construct,
ialirt_ingest_lambda_construct,
ialirt_processing_construct,
Expand Down Expand Up @@ -257,13 +258,43 @@ def build_sds(
efs_construct=efs_instance,
)

ialirt_stack = Stack(scope, "IalirtStack", env=env)
ialirt_stack = Stack(scope, "IalirtStack", cross_region_references=True, env=env)

# I-ALiRT IOIS S3 bucket
ialirt_bucket = ialirt_bucket_construct.IAlirtBucketConstruct(
scope=ialirt_stack, construct_id="IAlirtBucket", env=env
)

ialirt_lambda_layer = lambda_layer_construct.IMAPLambdaLayer(
scope=ialirt_stack,
id="IAlirtDependencies",
layer_dependencies_dir=str(layer_code_directory),
)

ialirt_monitoring = monitoring_construct.MonitoringConstruct(
scope=ialirt_stack,
construct_id="IAlirtMonitoringConstruct",
)

ialirt_api = api_gateway_construct.ApiGateway(
scope=ialirt_stack,
construct_id="IAlirtApiGateway",
domain_construct=domain,
certificate=root_certificate,
)
ialirt_api.deliver_to_sns(ialirt_monitoring.sns_topic_notifications)

ialirt_api_manager_construct.IalirtApiManager(
scope=ialirt_stack,
construct_id="IAlirtApiManager",
code=lambda_.Code.from_asset(str(Path(__file__).parent.parent / "lambda_code")),
api=ialirt_api,
env=env,
data_bucket=ialirt_bucket.ialirt_bucket,
vpc=networking.vpc,
layers=[ialirt_lambda_layer],
)

# All traffic to I-ALiRT is directed to listed container ports
ports = {"Primary": [1234, 1235], "Secondary": [1236]}
ialirt_secret_name = "nexus-credentials" # noqa
Expand Down
48 changes: 48 additions & 0 deletions tests/lambda_endpoints/test_ialirt_query_api.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
"""Tests for the I-ALiRT Query API."""

import json

from sds_data_manager.lambda_code.IAlirtCode import ialirt_query_api


def test_query_within_date_range(s3_client):
"""Test that the query API returns files within the specified date range."""
s3_client.create_bucket(Bucket="test-data-bucket")

# Adding files within and outside of the desired date range
s3_client.put_object(
Bucket="test-data-bucket",
Key="logs/flight_iois_1.log.2024-141T16-55-46_123456.txt",
Body=b"test",
)
s3_client.put_object(
Bucket="test-data-bucket",
Key="logs/flight_iois_1.log.2024-141T16-54-46_123456.txt",
Body=b"test",
)
s3_client.put_object(
Bucket="test-data-bucket",
Key="logs/flight_iois_1.log.2025-141T16-54-46_123456.txt",
Body=b"test",
)

event = {"queryStringParameters": {"year": "2024", "doy": "141", "instance": "1"}}

response = ialirt_query_api.lambda_handler(event=event, context=None)
response_data = json.loads(response["body"])

assert response["statusCode"] == 200
assert response_data["files"] == [
"flight_iois_1.log.2024-141T16-54-46_123456.txt",
"flight_iois_1.log.2024-141T16-55-46_123456.txt",
]


def test_invalid_date_format():
"""Test that an error is returned for invalid date formats."""
event = {"queryStringParameters": {"year": "invalid_date", "doy": "invalid_date"}}

response = ialirt_query_api.lambda_handler(event=event, context=None)

assert response["statusCode"] == 400
assert "Invalid year or day format. Use YYYY and DOY." in response["body"]

0 comments on commit c46c59c

Please sign in to comment.