Skip to content

Commit

Permalink
feat: Api Gateway Mount (#239)
Browse files Browse the repository at this point in the history
* weekend implementation

* code automatically formatted

Signed-off-by: sentential[bot] <bot@sentential>

* add some docs, fix some stuff

* code automatically formatted

Signed-off-by: sentential[bot] <bot@sentential>

* fixed some behaviors, got tests beganed

* code automatically formatted

Signed-off-by: sentential[bot] <bot@sentential>

* -a option, trailing slashes, and tests

* code automatically formatted

Signed-off-by: sentential[bot] <bot@sentential>

* add link to appropriate gateway type in docs

---------

Signed-off-by: sentential[bot] <bot@sentential>
Co-authored-by: sentential[bot] <bot@sentential>
  • Loading branch information
bkeane and sentential[bot] committed Apr 7, 2023
1 parent baae739 commit 735f4a2
Show file tree
Hide file tree
Showing 18 changed files with 1,430 additions and 726 deletions.
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

0 comments on commit 735f4a2

Please sign in to comment.