From f18d41d632d41ce5db3e7d4edc9a482bef8402df Mon Sep 17 00:00:00 2001 From: Jon Zeolla Date: Wed, 19 Aug 2020 17:20:49 -0400 Subject: [PATCH] Add API Gateway frontend and client to kick off the lambda --- Makefile | 13 +++++-- README.md | 30 +++++++++++--- client.py | 80 ++++++++++++++++++++++++++++++++++++++ lambda_function.py | 38 +++++++++++++++--- requirements-to-freeze.txt | 2 + requirements.txt | 6 +++ 6 files changed, 154 insertions(+), 15 deletions(-) create mode 100755 client.py diff --git a/Makefile b/Makefile index 19daad4..5ba74ba 100644 --- a/Makefile +++ b/Makefile @@ -1,3 +1,5 @@ +SHELL := /bin/bash + .PHONY: all all: lambda @@ -6,9 +8,6 @@ requirements: requirements-to-freeze.txt @python3 -c 'print("Updating requirements.txt...")' @docker run --rm -v $$(pwd):/usr/src/app/ python:3.8 /bin/bash -c "python -m pip install --upgrade pip && pip install -r /usr/src/app/requirements-to-freeze.txt && pip freeze > /usr/src/app/requirements.txt" -.PHONY: clean -clean: clean-python - .PHONY: clean-python clean-python: @# Prepending each recipe with - to continue regardless of errors per @@ -18,7 +17,13 @@ clean-python: @-find . -type d -name '.pytest_cache' -exec rm -rf {} + @-find . -type f -name '*.pyc' -delete +.PHONY: clean +clean: clean-python + .PHONY: lambda lambda: clean @docker run --rm -v $$(pwd):/usr/src/app/ python:3.8 /bin/bash -c "cd /usr/src/app/ && apt-get update && apt-get -y --no-install-recommends install zip && python -m pip install --upgrade pip && zip function.zip lambda_function.py && pip install --target ./package -r requirements.txt && cd package && zip -r9 ../function.zip ." - @echo "docker run --rm -it --env-file <(env | grep AWS_) -v $$(pwd):/usr/src/app/ -v ${HOME}/.aws:/root/.aws easy_infra aws lambda update-function-code --function-name release_monitor --zip-file fileb:///usr/src/app/function.zip" + +.PHONY: deploy +deploy: lambda + @docker run --rm --env-file <(env | grep AWS_) -v $$(pwd):/usr/src/app/ -v $${HOME}/.aws:/root/.aws seiso/easy_infra:latest aws lambda update-function-code --function-name release_monitor --zip-file fileb:///usr/src/app/function.zip diff --git a/README.md b/README.md index fd091f6..92616a6 100644 --- a/README.md +++ b/README.md @@ -1,13 +1,31 @@ # Release Monitor -This project uses the unauthenticated GitHub APIs to monitor a provided account/repo for a release containing a provided commit. It is meant to be run as an AWS lambda on a cron, and send an email notification when a release with the provided commit is detected. +This project uses the unauthenticated GitHub APIs to identify if the provided commit is in a stable release for the provided account/repo. It is meant to be run periodically, and send an email notification when a release with the provided commit is detected. + +## Prereqs +1. A lambda named "release_monitor", with a "lambda_function.lambda_handler" Handler, and Python 3.8 runtime. +1. An API Gateway attached to the lambda with AWS_IAM authorization required, named "release_monitor", and "Invoke with caller credentials" enabled. +1. Successful authentication to AWS with credentials that have access to lambda and API Gateway. For simplicity, we recommend using [environment variables](https://docs.aws.amazon.com/cli/latest/userguide/cli-configure-envvars.html). If your environment uses roles, you may want to consider an approach such as [this](https://github.com/JonZeolla/Configs/blob/a524d572a5426b8cfffad3e5e70d300bfd9b8c90/apple/productivity/.zshrc#L159-L186). ## Quickstart -1. Create a lambda in your AWS account. -1. Auth to AWS via environment variables. -1. Run the following: +1. Make a reusable alias +```bash +alias awsdocker="docker run --rm -it --env-file <(env | grep AWS_) -v \$(pwd):/usr/src/app/ -v \${HOME}/.aws:/root/.aws seiso/easy_infra:latest" +``` +1. Build and deploy the lambda. +```bash +make deploy +``` +1. Get your REST api ID +```bash +awsdocker "aws apigateway get-rest-apis | jq -r '.[][][\"id\"]' +``` +1. Query the lambda via API Gateway. ```bash -make -docker run --rm -it --env-file <(env | grep AWS_) -v $(pwd):/usr/src/app/ -v ${HOME}/.aws:/root/.aws seiso/easy_infra aws lambda update-function-code --function-name release_monitor --zip-file fileb:///usr/src/app/function.zip +ACCOUNT=jonzeolla +REPOSITORY=release_monitor +COMMIT=2315efa04cd4a415916c654f26570a17b9195279 +API_ID=example +./client.py --account $ACCOUNT --repository $REPOSITORY --commit $COMMIT --rest-api-id $API_ID ``` diff --git a/client.py b/client.py new file mode 100755 index 0000000..fc11aba --- /dev/null +++ b/client.py @@ -0,0 +1,80 @@ +#!/usr/bin/env python3 +""" +Client for interacting with the release monitor's API gateway trigger +""" +# pylint: disable=line-too-long + +import os +from typing import Dict +from argparse import ArgumentParser +from urllib.parse import urlencode +import requests +from aws_requests_auth.boto_utils import BotoAWSRequestsAuth + + +def main(): + """Do the thing""" + config = get_args_config() + + account = config["account"] + commit = config["commit"] + repository = config["repository"] + method = config["method"] + rest_api_id = config["rest_api_id"] + + uri = "/default/release_monitor" + querystring = { + "account": account, # pylint: disable=undefined-variable + "commit": commit, # pylint: disable=undefined-variable + "repository": repository, # pylint: disable=undefined-variable + } + querystring_encoded = urlencode(sorted(querystring.items())) + + region = "us-east-1" + host = rest_api_id + ".execute-api." + region + ".amazonaws.com" + url = "https://" + host + uri + "?" + querystring_encoded + service = "execute-api" + + # Uses boto logic for auth + auth = BotoAWSRequestsAuth( + aws_host=rest_api_id + ".execute-api." + region + ".amazonaws.com", + aws_region=region, + aws_service=service, + ) + + response = getattr(requests, method.lower())(url, auth=auth) + print("Request sent. Hope it worked :shrug:") + + +def get_args_config() -> Dict: + """ + Get the configs passed as arguments + """ + parser = create_arg_parser() + config = vars(parser.parse_args()) + return config + + +def create_arg_parser() -> ArgumentParser: + """Parse the arguments""" + parser = ArgumentParser() + parser.add_argument( + "--account", type=str, required=True, help="github account", + ) + parser.add_argument( + "--repository", type=str, required=True, help="github repository", + ) + parser.add_argument( + "--commit", type=str, required=True, help="commitish to monitor for", + ) + parser.add_argument( + "--rest-api-id", type=str, required=True, help="The AWS API Gateway REST API ID" + ) + parser.add_argument( + "--method", type=str.upper, default="GET", help="HTTP method to use", + ) + return parser + + +if __name__ == "__main__": + main() diff --git a/lambda_function.py b/lambda_function.py index 56a0b18..bc94648 100755 --- a/lambda_function.py +++ b/lambda_function.py @@ -8,11 +8,17 @@ from argparse import ArgumentParser import requests + def lambda_handler(event, context): """ AWS Lambda handler """ - check_for_commit(account=event["account"], repository=event["repository"], commitish=event["commit"]) + print(event) + check_for_commit( + account=event["queryStringParameters"]["account"], + repository=event["queryStringParameters"]["repository"], + commitish=event["queryStringParameters"]["commit"], + ) def main(): @@ -21,14 +27,24 @@ def main(): """ config = get_args_config() - check_for_commit(account=config["account"], repository=config["repository"], commitish=config["commit"]) + check_for_commit( + account=config["account"], + repository=config["repository"], + commitish=config["commit"], + ) def check_for_commit(*, account: str, repository: str, commitish: str) -> bool: """ Check a provided account/repo for a commitish """ - url = "https://api.github.com/repos/" + account + "/" + repository + "/releases/latest" + url = ( + "https://api.github.com/repos/" + + account + + "/" + + repository + + "/releases/latest" + ) headers = {"Accept": "application/vnd.github.v3+json"} response = requests.get(url, headers=headers) @@ -36,10 +52,21 @@ def check_for_commit(*, account: str, repository: str, commitish: str) -> bool: try: latest_release_sha = latest_release_json["target_commitish"] except KeyError: - print("Unable to identify the latest release commitish, are you being rate limited?") + print( + "Unable to identify the latest release commitish, are you being rate limited?" + ) sys.exit(1) - url = "https://api.github.com/repos/" + account + "/" + repository + "/compare/" + commitish + "..." + latest_release_sha + url = ( + "https://api.github.com/repos/" + + account + + "/" + + repository + + "/compare/" + + commitish + + "..." + + latest_release_sha + ) response = requests.get(url, headers=headers) github_status_json = response.json() @@ -79,5 +106,6 @@ def create_arg_parser() -> ArgumentParser: ) return parser + if __name__ == "__main__": main() diff --git a/requirements-to-freeze.txt b/requirements-to-freeze.txt index f229360..11766f7 100644 --- a/requirements-to-freeze.txt +++ b/requirements-to-freeze.txt @@ -1 +1,3 @@ requests +aws-requests-auth +botocore diff --git a/requirements.txt b/requirements.txt index ef0c170..5b3263d 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,5 +1,11 @@ +aws-requests-auth==0.4.3 +botocore==1.17.46 certifi==2020.6.20 chardet==3.0.4 +docutils==0.15.2 idna==2.10 +jmespath==0.10.0 +python-dateutil==2.8.1 requests==2.24.0 +six==1.15.0 urllib3==1.25.10