Skip to content

Commit

Permalink
feat: add PgStacApiLambda
Browse files Browse the repository at this point in the history
  • Loading branch information
alukach committed Oct 26, 2022
1 parent f57928e commit a092a7a
Show file tree
Hide file tree
Showing 11 changed files with 366 additions and 10 deletions.
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -5,3 +5,4 @@ lib/**/*.d.ts
.jsii
dist
docs
__pycache__
1 change: 1 addition & 0 deletions lib/index.ts
Original file line number Diff line number Diff line change
@@ -1,2 +1,3 @@
export * from "./bootstrapper";
export * from "./database";
export * from "./stac-api";
115 changes: 115 additions & 0 deletions lib/stac-api/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,115 @@
import {
Stack,
aws_ec2 as ec2,
aws_rds as rds,
aws_lambda as lambda,
aws_secretsmanager as secretsmanager,
CfnOutput,
} from "aws-cdk-lib";
import {
PythonFunction,
PythonFunctionProps,
} from "@aws-cdk/aws-lambda-python-alpha";
import { HttpApi } from "@aws-cdk/aws-apigatewayv2-alpha";
import { HttpLambdaIntegration } from "@aws-cdk/aws-apigatewayv2-integrations-alpha";
import { Construct } from "constructs";

export class PgStacApiLambda extends Construct {
constructor(scope: Construct, id: string, props: PgStacApiLambdaProps) {
super(scope, id);

const apiCode = props.apiCode || {
entry: `${__dirname}/runtime`,
index: "src/handler.py",
handler: "handler",
};

const handler = new PythonFunction(this, "stac-api", {
...apiCode,
/**
* NOTE: Unable to use Py3.9, due to issues with hashes:
*
* ERROR: Hashes are required in --require-hashes mode, but they are missing
* from some requirements. Here is a list of those requirements along with the
* hashes their downloaded archives actually had. Add lines like these to your
* requirements files to prevent tampering. (If you did not enable
* --require-hashes manually, note that it turns on automatically when any
* package has a hash.)
* anyio==3.6.1 --hash=sha256:cb29b9c70620506a9a8f87a309591713446953302d7d995344d0d7c6c0c9a7be
* */
runtime: lambda.Runtime.PYTHON_3_8,
architecture: lambda.Architecture.X86_64,
environment: {
PGSTAC_SECRET_ARN: props.dbSecret.secretArn,
DB_MIN_CONN_SIZE: "0",
DB_MAX_CONN_SIZE: "1",
...props.apiEnv,
},
vpc: props.vpc,
vpcSubnets: props.subnetSelection,
allowPublicSubnet: true,
memorySize: 8192,
});

props.dbSecret.grantRead(handler);
handler.connections.allowTo(props.db, ec2.Port.tcp(5432));

const stacApi = new HttpApi(this, "api", {
defaultIntegration: new HttpLambdaIntegration("integration", handler),
});

new CfnOutput(this, "stac-api-output", {
exportName: `${Stack.of(this).stackName}-url`,
value: stacApi.url!,
});
}
}

export interface PgStacApiLambdaProps {
/**
* VPC into which the lambda should be deployed.
*/
readonly vpc: ec2.IVpc;

/**
* RDS Instance with installed pgSTAC.
*/
readonly db: rds.IDatabaseInstance;

/**
* Subnet into which the lambda should be deployed.
*/
readonly subnetSelection: ec2.SubnetSelection;

/**
* Secret containing connection information for pgSTAC database.
*/
readonly dbSecret: secretsmanager.ISecret;

/**
* Custom code to run for fastapi-pgstac.
*
* @default - simplified version of fastapi-pgstac
*/
readonly apiCode?: ApiEntrypoint;

/**
* Customized environment variables to send to fastapi-pgstac runtime.
*/
readonly apiEnv?: Record<string, string>;
}

export interface ApiEntrypoint {
/**
* Path to the source of the function or the location for dependencies.
*/
readonly entry: PythonFunctionProps["entry"];
/**
* The path (relative to entry) to the index file containing the exported handler.
*/
readonly index: PythonFunctionProps["index"];
/**
* The name of the exported handler in the index file.
*/
readonly handler: PythonFunctionProps["handler"];
}
1 change: 1 addition & 0 deletions lib/stac-api/runtime/.dockerignore
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
*/.venv/*
8 changes: 8 additions & 0 deletions lib/stac-api/runtime/requirements.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
mangum==0.15.1
stac-fastapi.api==2.4.1
stac-fastapi.extensions==2.4.1
stac-fastapi.pgstac==2.4.1
stac-fastapi.types==2.4.1
# https://github.com/stac-utils/stac-fastapi/pull/466
pygeoif==0.7
starlette_cramjam
Empty file.
60 changes: 60 additions & 0 deletions lib/stac-api/runtime/src/app.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
"""
FastAPI application using PGStac.
"""

from fastapi.middleware.cors import CORSMiddleware
from fastapi.responses import ORJSONResponse
from stac_fastapi.api.app import StacApi
from stac_fastapi.pgstac.core import CoreCrudClient
from stac_fastapi.pgstac.db import close_db_connection, connect_to_db
from starlette_cramjam.middleware import CompressionMiddleware

from .config import (
ApiSettings,
extensions as PgStacExtensions,
get_request_model as GETModel,
post_request_model as POSTModel,
)

api_settings = ApiSettings()

api = StacApi(
title=api_settings.name,
api_version=api_settings.version,
description=api_settings.description or api_settings.name,
settings=api_settings.load_postgres_settings(),
extensions=PgStacExtensions,
client=CoreCrudClient(post_request_model=POSTModel),
search_get_request_model=GETModel,
search_post_request_model=POSTModel,
response_class=ORJSONResponse,
middlewares=[CompressionMiddleware],
)

app = api.app

# Set all CORS enabled origins
if api_settings.cors_origins:
app.add_middleware(
CORSMiddleware,
allow_origins=api_settings.cors_origins,
allow_credentials=True,
allow_methods=["GET", "POST", "OPTIONS"],
allow_headers=["*"],
)


@app.on_event("startup")
async def startup_event():
"""Connect to database on startup."""
print("Setting up DB connection...")
await connect_to_db(app)
print("DB connection setup.")


@app.on_event("shutdown")
async def shutdown_event():
"""Close database connection."""
print("Closing up DB connection...")
await close_db_connection(app)
print("DB connection closed.")
97 changes: 97 additions & 0 deletions lib/stac-api/runtime/src/config.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,97 @@
"""API settings.
Based on https://github.com/developmentseed/eoAPI/tree/master/src/eoapi/stac"""
import base64
import json
from typing import Optional

import boto3
import pydantic

from stac_fastapi.api.models import create_get_request_model, create_post_request_model

# from stac_fastapi.pgstac.extensions import QueryExtension
from stac_fastapi.extensions.core import (
ContextExtension,
FieldsExtension,
FilterExtension,
QueryExtension,
SortExtension,
TokenPaginationExtension,
)
from stac_fastapi.pgstac.config import Settings
from stac_fastapi.pgstac.types.search import PgstacSearch


def get_secret_dict(secret_name: str):
"""Retrieve secrets from AWS Secrets Manager
Args:
secret_name (str): name of aws secrets manager secret containing database connection secrets
profile_name (str, optional): optional name of aws profile for use in debugger only
Returns:
secrets (dict): decrypted secrets in dict
"""

# Create a Secrets Manager client
session = boto3.session.Session()
client = session.client(service_name="secretsmanager")

get_secret_value_response = client.get_secret_value(SecretId=secret_name)

if "SecretString" in get_secret_value_response:
return json.loads(get_secret_value_response["SecretString"])
else:
return json.loads(base64.b64decode(get_secret_value_response["SecretBinary"]))


class ApiSettings(pydantic.BaseSettings):
"""API settings"""

name: str = "asdi-stac-api"
version: str = "0.1"
description: Optional[str] = None
cors_origins: str = "*"
cachecontrol: str = "public, max-age=3600"
debug: bool = False

pgstac_secret_arn: Optional[str]

@pydantic.validator("cors_origins")
def parse_cors_origin(cls, v):
"""Parse CORS origins."""
return [origin.strip() for origin in v.split(",")]

def load_postgres_settings(self) -> "Settings":
"""Load postgres connection params from AWS secret"""

if self.pgstac_secret_arn:
secret = get_secret_dict(self.pgstac_secret_arn)

return Settings(
postgres_host_reader=secret["host"],
postgres_host_writer=secret["host"],
postgres_dbname=secret["dbname"],
postgres_user=secret["username"],
postgres_pass=secret["password"],
postgres_port=secret["port"],
)
else:
return Settings()

class Config:
"""model config"""

env_file = ".env"


extensions = [
FilterExtension(),
QueryExtension(),
SortExtension(),
FieldsExtension(),
TokenPaginationExtension(),
ContextExtension(),
]
post_request_model = create_post_request_model(extensions, base_model=PgstacSearch)
get_request_model = create_get_request_model(extensions)
9 changes: 9 additions & 0 deletions lib/stac-api/runtime/src/handler.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
"""
Handler for AWS Lambda.
"""

from mangum import Mangum

from .app import app

handler = Mangum(app)
Loading

0 comments on commit a092a7a

Please sign in to comment.