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
81 changes: 65 additions & 16 deletions .github/workflows/deploy.yaml
Comment thread
gadomski marked this conversation as resolved.
Original file line number Diff line number Diff line change
Expand Up @@ -72,6 +72,56 @@ jobs:
run: |
cd integration_tests/cdk

print_response_body() {
local response_file="$1"

if [ -s "$response_file" ]; then
echo "--- Response body ---"
jq . "$response_file" 2>/dev/null || cat "$response_file"
echo "--- End response body ---"
else
echo "--- Response body was empty ---"
fi
}

print_cloudwatch_logs() {
echo "=== Retrieving recent CloudWatch logs for deployed Lambda functions ==="

for STACK_NAME in $DEPLOYED_STACKS; do
echo "--- Stack: $STACK_NAME ---"

FUNCTION_NAMES=$(aws cloudformation list-stack-resources \
--stack-name "$STACK_NAME" \
--query 'StackResourceSummaries[?ResourceType==`AWS::Lambda::Function`].PhysicalResourceId' \
--output text 2>/dev/null || true)

if [ -z "$FUNCTION_NAMES" ] || [ "$FUNCTION_NAMES" = "None" ]; then
echo "No Lambda functions found for $STACK_NAME"
continue
fi

for FUNCTION_NAME in $FUNCTION_NAMES; do
echo "--- CloudWatch logs for $FUNCTION_NAME ---"
aws logs tail "/aws/lambda/$FUNCTION_NAME" --since 15m --format short 2>/dev/null \
|| echo "Unable to retrieve logs for $FUNCTION_NAME"
done
done
}

fail_with_diagnostics() {
local message="$1"
local response_file="${2:-}"

echo "❌ $message"

if [ -n "$response_file" ]; then
print_response_body "$response_file"
fi

print_cloudwatch_logs
exit 1
}

echo "=== Retrieving Stack Outputs ==="

# Get list of deployed stacks
Expand Down Expand Up @@ -160,13 +210,13 @@ jobs:
if [ -n "$API_URL" ] && [ "$API_URL" != "null" ]; then
echo "Checking $API_NAME at: $API_URL"

HTTP_STATUS=$(curl -s -o /dev/null -w "%{http_code}" --max-time 30 "$API_URL" || echo "000")
RESPONSE_FILE=$(mktemp)
HTTP_STATUS=$(curl -sS -o "$RESPONSE_FILE" -w "%{http_code}" --max-time 30 "$API_URL" || echo "000")

if [ "$HTTP_STATUS" = "200" ]; then
echo "✅ $API_NAME returned 200"
else
echo "❌ $API_NAME returned $HTTP_STATUS"
exit 1
fail_with_diagnostics "$API_NAME returned $HTTP_STATUS for $API_URL" "$RESPONSE_FILE"
fi
else
echo "⚠️ $API_NAME URL not found in stack outputs"
Expand Down Expand Up @@ -212,8 +262,7 @@ jobs:
cat /tmp/collection_response.json

if [ "$COLLECTION_STATUS" != "201" ]; then
echo "❌ Failed to post collection (status $COLLECTION_STATUS)"
exit 1
fail_with_diagnostics "Failed to post collection (status $COLLECTION_STATUS)" "/tmp/collection_response.json"
fi
echo "✅ Collection posted successfully"

Expand Down Expand Up @@ -248,8 +297,7 @@ jobs:
cat /tmp/item_response.json

if [ "$ITEM_STATUS" != "201" ]; then
echo "❌ Failed to post item (status $ITEM_STATUS)"
exit 1
fail_with_diagnostics "Failed to post item (status $ITEM_STATUS)" "/tmp/item_response.json"
fi
echo "✅ Item queued for ingestion"

Expand All @@ -260,8 +308,9 @@ jobs:

while [ "$INGESTION_STATUS" = "queued" ] || [ "$INGESTION_STATUS" = "started" ]; do
if [ "$POLL_ATTEMPTS" -ge "$MAX_POLL_ATTEMPTS" ]; then
echo "❌ Timed out waiting for ingestion of $ITEM_ID (status: $INGESTION_STATUS)"
exit 1
INGESTION_RESPONSE_FILE=$(mktemp)
printf '%s\n' "$INGESTION_RESPONSE" > "$INGESTION_RESPONSE_FILE"
fail_with_diagnostics "Timed out waiting for ingestion of $ITEM_ID (status: $INGESTION_STATUS)" "$INGESTION_RESPONSE_FILE"
fi

sleep 15
Expand All @@ -273,22 +322,22 @@ jobs:
done

if [ "$INGESTION_STATUS" != "succeeded" ]; then
echo "❌ Ingestion failed with status: $INGESTION_STATUS"
echo "$INGESTION_RESPONSE" | jq .
exit 1
INGESTION_RESPONSE_FILE=$(mktemp)
printf '%s\n' "$INGESTION_RESPONSE" > "$INGESTION_RESPONSE_FILE"
fail_with_diagnostics "Ingestion failed with status: $INGESTION_STATUS" "$INGESTION_RESPONSE_FILE"
fi
echo "✅ Item ingested successfully"

echo "--- Verifying item in STAC API ---"
STAC_ITEM_STATUS=$(curl -s -o /dev/null -w "%{http_code}" \
STAC_ITEM_RESPONSE_FILE=$(mktemp)
STAC_ITEM_STATUS=$(curl -sS -o "$STAC_ITEM_RESPONSE_FILE" -w "%{http_code}" \
--max-time 30 \
"${STAC_API_URL}collections/$COLLECTION_ID/items/$ITEM_ID")
"${STAC_API_URL}collections/$COLLECTION_ID/items/$ITEM_ID" || echo "000")

if [ "$STAC_ITEM_STATUS" = "200" ]; then
echo "✅ Item $ITEM_ID is accessible in STAC API"
else
echo "❌ Item $ITEM_ID not found in STAC API (status $STAC_ITEM_STATUS)"
exit 1
fail_with_diagnostics "Item $ITEM_ID not found in STAC API (status $STAC_ITEM_STATUS)" "$STAC_ITEM_RESPONSE_FILE"
fi
else
echo "⚠️ INGESTOR_API_URL not found in stack outputs, skipping ingestor test"
Expand Down
2 changes: 2 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,8 @@ An [RDS](https://aws.amazon.com/rds/) instance with [pgSTAC](https://github.com/
### [STAC API](https://developmentseed.org/eoapi-cdk/#pgstacapilambda-)
A STAC API implementation using [stac-fastapi](https://github.com/stac-utils/stac-fastapi) with a [pgSTAC backend](https://github.com/stac-utils/stac-fastapi-pgstac). Packaged as a complete runtime for deployment with API Gateway and Lambda.

For runtime notes on event loops, Mangum lifespan handling, and SnapStart-aware database pool initialization, see [`lib/stac-api/runtime/README.md`](lib/stac-api/runtime/README.md).

### [pgSTAC Titiler API](https://developmentseed.org/eoapi-cdk/#titilerpgstacapilambda-)
A complete dynamic tiling API using [titiler-pgstac](https://github.com/stac-utils/titiler-pgstac) to create dynamic mosaics of assets based on [STAC Search queries](https://github.com/radiantearth/stac-api-spec/tree/master/item-search). Packaged as a complete runtime for deployment with API Gateway and Lambda and fully integrated with the pgSTAC Database construct.

Expand Down
2 changes: 1 addition & 1 deletion lib/ingestor-api/runtime/pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ dependencies = [
"boto3",
"cachetools>=5.0",
"fastapi>=0.110",
"mangum==0.19",
"mangum>=0.21.0",
"orjson>=3.9",
"psycopg[binary,pool]>=3.0",
"pydantic>=2.0",
Expand Down
31 changes: 27 additions & 4 deletions lib/ingestor-api/runtime/src/handler.py
Original file line number Diff line number Diff line change
@@ -1,9 +1,32 @@
"""
Entrypoint for Lambda execution.
"""
"""Entrypoint for Lambda execution."""

import asyncio
from typing import Any

from mangum import Mangum

from .main import app

handler = Mangum(app, lifespan="off", api_gateway_base_path=app.root_path)

def _ensure_event_loop() -> asyncio.AbstractEventLoop:
"""Return the current event loop, creating and installing one if needed."""
try:
return asyncio.get_running_loop()
except RuntimeError:
pass

try:
return asyncio.get_event_loop()
except RuntimeError:
loop = asyncio.new_event_loop()
asyncio.set_event_loop(loop)
return loop
Comment thread
gadomski marked this conversation as resolved.


_asgi_handler = Mangum(app, lifespan="off", api_gateway_base_path=app.root_path)


def handler(event: Any, context: Any) -> dict[str, Any]:
"""Handle AWS Lambda events with a guaranteed current event loop."""
_ensure_event_loop()
return _asgi_handler(event, context)
Loading
Loading