Skip to content

Commit 0dc16b9

Browse files
committed
feat(titiler-pgstac-api): add titiler-pgstac endpoint
* added a new service deploying a titiler-pgstac service with access to the pgstac database, and exposing its API
1 parent 86fd1a4 commit 0dc16b9

File tree

8 files changed

+216
-0
lines changed

8 files changed

+216
-0
lines changed

lib/index.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,3 +3,4 @@ export * from "./bootstrapper";
33
export * from "./database";
44
export * from "./ingestor-api";
55
export * from "./stac-api";
6+
export * from "./titiler-pgstac-api";

lib/titiler-pgstac-api/index.ts

Lines changed: 120 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,120 @@
1+
import {
2+
Stack,
3+
aws_ec2 as ec2,
4+
aws_rds as rds,
5+
aws_lambda as lambda,
6+
aws_secretsmanager as secretsmanager,
7+
CfnOutput,
8+
} from "aws-cdk-lib";
9+
import {
10+
PythonFunction,
11+
PythonFunctionProps,
12+
} from "@aws-cdk/aws-lambda-python-alpha";
13+
import { HttpApi } from "@aws-cdk/aws-apigatewayv2-alpha";
14+
import { HttpLambdaIntegration } from "@aws-cdk/aws-apigatewayv2-integrations-alpha";
15+
import { Construct } from "constructs";
16+
17+
export class TitilerPgstacApiLambda extends Construct {
18+
readonly url: string;
19+
20+
constructor(scope: Construct, id: string, props: TitilerPgStacApiLambdaProps) {
21+
super(scope, id);
22+
23+
const apiCode = props.apiCode || {
24+
entry: `${__dirname}/runtime`,
25+
index: "src/handler.py",
26+
handler: "handler",
27+
};
28+
29+
const handler = new PythonFunction(this, "titiler-pgstac-api", {
30+
...apiCode,
31+
/**
32+
* NOTE: Unable to use Py3.9, due to issues with hashes:
33+
*
34+
* ERROR: Hashes are required in --require-hashes mode, but they are missing
35+
* from some requirements. Here is a list of those requirements along with the
36+
* hashes their downloaded archives actually had. Add lines like these to your
37+
* requirements files to prevent tampering. (If you did not enable
38+
* --require-hashes manually, note that it turns on automatically when any
39+
* package has a hash.)
40+
* anyio==3.6.1 --hash=sha256:cb29b9c70620506a9a8f87a309591713446953302d7d995344d0d7c6c0c9a7be
41+
* */
42+
runtime: lambda.Runtime.PYTHON_3_8,
43+
architecture: lambda.Architecture.X86_64,
44+
environment: {
45+
PGSTAC_SECRET_ARN: props.dbSecret.secretArn,
46+
DB_MIN_CONN_SIZE: "0",
47+
DB_MAX_CONN_SIZE: "1",
48+
...props.apiEnv,
49+
},
50+
vpc: props.vpc,
51+
vpcSubnets: props.subnetSelection,
52+
allowPublicSubnet: true,
53+
memorySize: 8192,
54+
});
55+
56+
props.dbSecret.grantRead(handler);
57+
handler.connections.allowTo(props.db, ec2.Port.tcp(5432), "allow connections from titiler");
58+
59+
const stacApi = new HttpApi(this, `${Stack.of(this).stackName}-titiler-pgstac-api`, {
60+
defaultIntegration: new HttpLambdaIntegration("integration", handler),
61+
});
62+
63+
this.url = stacApi.url!;
64+
65+
new CfnOutput(this, "titiler-pgstac-api-output", {
66+
exportName: `${Stack.of(this).stackName}-url`,
67+
value: this.url,
68+
});
69+
}
70+
}
71+
72+
export interface TitilerPgStacApiLambdaProps {
73+
/**
74+
* VPC into which the lambda should be deployed.
75+
*/
76+
readonly vpc: ec2.IVpc;
77+
78+
/**
79+
* RDS Instance with installed pgSTAC.
80+
*/
81+
readonly db: rds.IDatabaseInstance;
82+
83+
/**
84+
* Subnet into which the lambda should be deployed.
85+
*/
86+
readonly subnetSelection: ec2.SubnetSelection;
87+
88+
/**
89+
* Secret containing connection information for pgSTAC database.
90+
*/
91+
readonly dbSecret: secretsmanager.ISecret;
92+
93+
/**
94+
* Custom code to run for titiler-pgstac.
95+
*
96+
* @default - simplified version of titiler-pgstac
97+
*/
98+
readonly apiCode?: TitilerApiEntrypoint;
99+
100+
/**
101+
* Customized environment variables to send to titiler-pgstac runtime.
102+
*/
103+
readonly apiEnv?: Record<string, string>;
104+
}
105+
106+
export interface TitilerApiEntrypoint {
107+
/**
108+
* Path to the source of the function or the location for dependencies.
109+
*/
110+
readonly entry: PythonFunctionProps["entry"];
111+
/**
112+
* The path (relative to entry) to the index file containing the exported handler.
113+
*/
114+
readonly index: PythonFunctionProps["index"];
115+
/**
116+
* The name of the exported handler in the index file.
117+
*/
118+
readonly handler: PythonFunctionProps["handler"];
119+
}
120+
Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
uvicorn
2+
pydantic[dotenv]
Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
titiler.pgstac==0.3.3
2+
starlette-cramjam>=0.3,<0.4
3+
importlib_resources>=1.1.0
4+
mangum>=0.15.0
5+
psycopg[pool]
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
PGSTAC_SECRET_ARN="arn:aws:secretsmanager:us-west-2:916098889494:secret:pgstac/bootstrap-pgstac-instance/093a89d4-2hB9wY"
Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
"""eoapi.raster."""
2+
3+
__version__ = "0.1.0"
Lines changed: 73 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,73 @@
1+
"""titiler pgstac API settings."""
2+
import base64
3+
import json
4+
from typing import Optional, Any
5+
import os
6+
7+
import boto3
8+
import pydantic
9+
10+
11+
def get_secret_dict(secret_name: str):
12+
"""Retrieve secrets from AWS Secrets Manager
13+
14+
Args:
15+
secret_name (str): name of aws secrets manager secret containing database connection secrets
16+
profile_name (str, optional): optional name of aws profile for use in debugger only
17+
18+
Returns:
19+
secrets (dict): decrypted secrets in dict
20+
"""
21+
22+
# Create a Secrets Manager client
23+
session = boto3.session.Session()
24+
client = session.client(service_name="secretsmanager")
25+
26+
get_secret_value_response = client.get_secret_value(SecretId=secret_name)
27+
28+
if "SecretString" in get_secret_value_response:
29+
return json.loads(get_secret_value_response["SecretString"])
30+
else:
31+
return json.loads(base64.b64decode(get_secret_value_response["SecretBinary"]))
32+
33+
34+
class ApiSettings(pydantic.BaseSettings):
35+
"""API settings"""
36+
37+
pgstac_secret_arn: str
38+
39+
name: str = "titiler pgstac api"
40+
version: str = "0.1"
41+
description: Optional[str] = None
42+
cors_origins: str = "*"
43+
cachecontrol: str = "public, max-age=3600"
44+
debug: bool = False
45+
46+
@pydantic.validator("cors_origins")
47+
def parse_cors_origin(cls, v):
48+
"""Parse CORS origins."""
49+
return [origin.strip() for origin in v.split(",")]
50+
51+
def set_postgres_settings(self):
52+
"""Export postgres connection params to environment variables from DB AWS secret"""
53+
54+
secret = get_secret_dict(self.pgstac_secret_arn)
55+
56+
def set_var(key: str, value: Any):
57+
print(key, value)
58+
os.environ[key] = str(value)
59+
60+
params = dict(
61+
postgres_host=secret["host"],
62+
postgres_dbname=secret["dbname"],
63+
postgres_user=secret["username"],
64+
postgres_pass=secret["password"],
65+
postgres_port=secret["port"],
66+
)
67+
68+
[set_var(k, v) for k, v in params.items()]
69+
70+
class Config:
71+
"""model config"""
72+
73+
env_file = ".env"
Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
"""
2+
Handler for AWS Lambda.
3+
"""
4+
5+
from mangum import Mangum
6+
from config import ApiSettings
7+
from titiler.pgstac.main import app
8+
9+
settings = ApiSettings()
10+
settings.set_postgres_settings()
11+
handler = Mangum(app)

0 commit comments

Comments
 (0)