Skip to content
New issue

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

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

Already on GitHub? Sign in to your account

Api Gateway Mount #239

Merged
merged 11 commits into from
Apr 7, 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
1 change: 1 addition & 0 deletions docs/_sidebar.md
Original file line number Diff line number Diff line change
Expand Up @@ -10,4 +10,5 @@
* [Shapes](examples/shapes.md)
* [Function URL](examples/public_url.md)
* [Schedule](examples/schedule.md)
* [API Gateway](examples/api_gateway.md)
* [Custom Images](examples/custom_images.md)
111 changes: 111 additions & 0 deletions docs/examples/api_gateway.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,111 @@
### API Gateway

[API Gateway](https://docs.aws.amazon.com/apigateway/index.html) can be used to create publicly accessible https routes by which to invoke your http based lambdas.

Sentential—via its **mount** system—provides a simple way to create an API Gateway integration for functions and mount said integration to an API route.

To demonstrate this functionality, we will write a small function to inspect our http request headers.

### Prerequisites

You have initialized the [explore project](/examples/project) and are operating in said directory.

### Develop

Create or modify...

<!-- tabs:start -->

#### **./src/app.py**

```python
from fastapi import FastAPI, Request
from mangum import Mangum

app = FastAPI()

@app.get("/")
def request_headers(request: Request):
return request.headers

handler = Mangum(app, lifespan="off")
```

#### **./src/requirements.txt**

```txt
fastapi
mangum
```

#### **./Dockerfile**

Add this `RUN` stanza to the `./Dockerfile` under the `explore` stage.

```dockerfile
RUN pip install -r requirements.txt
```

<!-- tabs:end -->

### Build

```shell
> sntl build
```

### Verify

```shell
> sntl deploy local --public-url
> sntl invoke local '{}'
> curl localhost:8999

{
"accept":"*/*",
"user-agent":"curl/7.81.0"
}
```

### Publish & Deploy

```shell
> sntl publish
> sntl deploy aws
> sntl ls

build arch status hrefs mounts
───────────────────────────────────────────────
local arm64 running [] []
0.0.2 arm64 active ['console'] []
0.0.1 arm64 [] []
```

### Mount Route

This step assumes you already have at least one [HTTP API Gateway](https://docs.aws.amazon.com/apigateway/latest/developerguide/http-api.html) launched in your AWS account.

```shell
> sntl mount route [tab][tab] # use tab to discover/autocomplete available gateway urls
> sntl mount route {api_id}.execute-api.us-west-2.amazonaws.com
> sntl ls

build arch status hrefs mounts
──────────────────────────────────────────────────────────
local arm64 [] []
0.0.2 arm64 active ['console'] ['/']
0.0.1 arm64 [] []
```

Visit the gateway url in your browser.

> :information_source:
> Within the headers of the request you will find the `X-Forwarded-Prefix`. This will contain the route under which the lambda is mounted.
> It is useful to have this in concert with the `host` header so you application can properly handle redirects etc.

### Cleanup

```shell
> sntl destroy local
> sntl destroy aws
```
2 changes: 1 addition & 1 deletion docs/examples/schedule.md
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
### Schedule
### Mount Schedule

[EventBridge Rules](https://docs.aws.amazon.com/eventbridge/latest/userguide/eb-rules.html), along with [schedule expressions](https://docs.aws.amazon.com/eventbridge/latest/userguide/eb-create-rule-schedule.html), can be used to run AWS Lambda functions on a schedule.

Expand Down
1,547 changes: 873 additions & 674 deletions poetry.lock

Large diffs are not rendered by default.

3 changes: 2 additions & 1 deletion pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -26,11 +26,12 @@ python-semantic-release = "^7.31.2"
pytest-env = "^0.8.1"
requests-mock = "^1.10.0"
docker = "^6.0.1"
moto = { git = "https://github.com/bkeane/moto.git", branch = "defaultKmsKeyIds" }
flask = "^2.2.2"
flask-cors = "^3.0.10"
pyyaml = "^6.0"
backoff = "^2.2.1"
moto = "^4.1.6"
openapi-spec-validator = "^0.5.6"

[build-system]
requires = ["poetry-core>=1.0.0"]
Expand Down
2 changes: 2 additions & 0 deletions sentential/cli/destroy.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@
from sentential.lib.drivers.local_lambda import LocalLambdaDriver
from sentential.lib.drivers.aws_lambda import AwsLambdaDriver
from sentential.lib.mounts.aws_event_schedule import AwsEventScheduleMount
from sentential.lib.mounts.aws_api_gateway import AwsApiGatewayMount
from sentential.lib.ontology import Ontology

destroy = typer.Typer()
Expand All @@ -17,4 +18,5 @@ def local():
def aws():
"""destroy lambda deployment in aws"""
AwsEventScheduleMount(Ontology()).umount()
AwsApiGatewayMount(Ontology()).umount()
AwsLambdaDriver(Ontology()).destroy()
13 changes: 13 additions & 0 deletions sentential/cli/mount.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import typer
from rich import print
from sentential.lib.mounts.aws_event_schedule import AwsEventScheduleMount
from sentential.lib.mounts.aws_api_gateway import AwsApiGatewayMount
from sentential.lib.ontology import Ontology

mount = typer.Typer()
Expand All @@ -15,3 +16,15 @@ def schedule(
):
"""mount lambda image to schedule"""
print(AwsEventScheduleMount(Ontology()).mount(schedule, payload))


@mount.command()
def route(
path: str = typer.Argument(
None,
autocompletion=AwsApiGatewayMount.autocomplete,
help="mount lambda to given path",
)
):
"""mount lambda image to api gateway"""
print(AwsApiGatewayMount(Ontology()).mount(path))
2 changes: 1 addition & 1 deletion sentential/cli/root.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
from typing import List, Optional
from typing import List
import typer
from sentential.lib.clients import clients
from sentential.lib.drivers.local_images import LocalImagesDriver
Expand Down
21 changes: 21 additions & 0 deletions sentential/cli/umount.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import typer
from rich import print
from sentential.lib.mounts.aws_event_schedule import AwsEventScheduleMount
from sentential.lib.mounts.aws_api_gateway import AwsApiGatewayMount
from sentential.lib.ontology import Ontology

umount = typer.Typer()
Expand All @@ -10,3 +11,23 @@
def schedule():
"""unmount lambda image from schedule"""
AwsEventScheduleMount(Ontology()).umount()


@umount.command()
def route(
all: bool = typer.Option(
False, "-a", "--all", help="unmount all routes integrated with lambda"
),
path: str = typer.Argument(
None,
autocompletion=AwsApiGatewayMount.autocomplete,
help="unmount lambda from given path",
),
):
"""unmount lambda image from api gateway"""
if not all and path is None:
print("you must provide either an explicit path or --all")
exit(1)

for msg in AwsApiGatewayMount(Ontology()).umount(path):
print(msg)
4 changes: 4 additions & 0 deletions sentential/lib/exceptions.py
Original file line number Diff line number Diff line change
Expand Up @@ -47,3 +47,7 @@ class ApiGatewayResourceNotFound(SntlException):

class ArchitectureDiscoveryError(SntlException):
pass


class AwsApiGatewayNotFound(Exception):
pass
30 changes: 28 additions & 2 deletions sentential/lib/joinery.py
Original file line number Diff line number Diff line change
@@ -1,11 +1,14 @@
from distutils.version import LooseVersion
from functools import lru_cache
from typing import List, Union
from typing import List, Tuple, Union
from rich.table import Table, box
from sentential.lib.clients import clients
from sentential.lib.drivers.local_bridge import LocalBridge
from sentential.lib.ontology import Ontology
from sentential.lib.shapes import (
ApiGatewayApi,
ApiGatewayIntegration,
ApiGatewayRoute,
AwsFunction,
AwsFunctionPublicUrl,
AwsManifestList,
Expand All @@ -16,6 +19,7 @@
from sentential.lib.drivers.aws_lambda import AwsLambdaDriver
from sentential.lib.drivers.local_lambda import LocalLambdaDriver
from sentential.lib.mounts.aws_event_schedule import AwsEventScheduleMount
from sentential.lib.mounts.aws_api_gateway import AwsApiGatewayMount, deproxify
from sentential.lib.shapes import CURRENT_WORKING_IMAGE_TAG
from pydantic import BaseModel
from python_on_whales.components.image.cli_wrapper import Image
Expand Down Expand Up @@ -90,6 +94,7 @@ def _published(self) -> List[Row]:
deployed_function = self._deployed_function()
deployed_url = self._deployed_url()
deployed_schedule = self._deployed_schedule()
deployed_routes = self._deployed_routes()
for manifest in self.ecr_images._manifest_lists():
if not isinstance(manifest.imageManifest, AwsManifestList):
raise JoineryError("expected AwsManifestList object")
Expand Down Expand Up @@ -119,6 +124,9 @@ def _published(self) -> List[Row]:
row["hrefs"].append(self._public_url(deployed_url.FunctionUrl))
if deployed_schedule is not None:
row["mounts"].append(self._console_schedule(deployed_schedule))
if len(deployed_routes) > 0:
row["mounts"].append(self._console_routes())

rows.append(Row(**row)) # row yer boat

return sorted(rows, key=lambda row: LooseVersion(row.build), reverse=True)
Expand Down Expand Up @@ -174,6 +182,12 @@ def _deployed_schedule(self) -> Union[None, str]:
except:
return None

@lru_cache()
def _deployed_routes(
self,
) -> List[Tuple[ApiGatewayApi, ApiGatewayRoute, ApiGatewayIntegration]]:
return AwsApiGatewayMount(Ontology())._mounts()

def _public_url(self, url: str) -> str:
return f"[link={url}]public_url[/link]"

Expand All @@ -183,7 +197,7 @@ def _console_web(self) -> str:
url = f"https://{region}.console.aws.amazon.com/lambda/home?region={region}#/functions/{function}"
return f"[link={url}]console[/link]"

def _console_schedule(self, schedule: str):
def _console_schedule(self, schedule: str) -> Union[None, str]:
region = self.ontology.context.region
function = self.ontology.context.resource_name
url = f"https://{region}.console.aws.amazon.com/events/home?region={region}#/eventbus/default/rules/{function}"
Expand All @@ -192,6 +206,18 @@ def _console_schedule(self, schedule: str):
except:
return None

def _console_routes(self) -> Union[None, str]:
region = self.ontology.context.region
links = []
for api, route, integration in self._deployed_routes():
text = deproxify(route.RouteKey.split(" ")[-1])
url = f"https://{region}.console.aws.amazon.com/apigateway/main/develop/routes?api={api.ApiId}&integration={integration.IntegrationId}&region={region}&routes={route.RouteId}"
links.append(f"[link={url}]{text}[/link]")
if links:
return ", ".join(links)
else:
return None

def _extract_arch(self, manifest: AwsManifestList) -> str:
return ", ".join([m.platform.architecture for m in manifest.manifests])

Expand Down
Loading