diff --git a/.github/workflows/deploy-docker.yml b/.github/workflows/deploy-docker.yml new file mode 100644 index 0000000..2b590dd --- /dev/null +++ b/.github/workflows/deploy-docker.yml @@ -0,0 +1,48 @@ +# Docs for the Azure Web Apps Deploy action: https://github.com/Azure/webapps-deploy +# More GitHub Actions for Azure: https://github.com/Azure/actions + +name: Build and deploy container app to Azure Web App - spotbot-docker + +on: + workflow_dispatch: + +jobs: + build: + runs-on: 'ubuntu-latest' + + steps: + - uses: actions/checkout@v2 + + - name: Set up Docker Buildx + uses: docker/setup-buildx-action@v2 + + - name: Log in to registry + uses: docker/login-action@v2 + with: + registry: https://index.docker.io/v1/ + username: ${{ secrets.AzureAppService_ContainerUsername_630053f317634033aaa0862db4b07802 }} + password: ${{ secrets.AzureAppService_ContainerPassword_71079aba47ee4c1196a01eaf134b3447 }} + + - name: Build and push container image to registry + uses: docker/build-push-action@v3 + with: + push: true + tags: index.docker.io/${{ secrets.AzureAppService_ContainerUsername_630053f317634033aaa0862db4b07802 }}/python:${{ github.sha }} + file: ./Dockerfile + + deploy: + runs-on: ubuntu-latest + needs: build + environment: + name: 'production' + url: ${{ steps.deploy-to-webapp.outputs.webapp-url }} + + steps: + - name: Deploy to Azure Web App + id: deploy-to-webapp + uses: azure/webapps-deploy@v2 + with: + app-name: 'spotbot-docker' + slot-name: 'production' + publish-profile: ${{ secrets.AzureAppService_PublishProfile_e744ed7c6ffe4564a93b0e3c07ec1ed1 }} + images: 'index.docker.io/${{ secrets.AzureAppService_ContainerUsername_630053f317634033aaa0862db4b07802 }}/python:${{ github.sha }}' \ No newline at end of file diff --git a/.github/workflows/main_hamalertspotbot.yml b/.github/workflows/main_hamalertspotbot.yml deleted file mode 100644 index 4e9d513..0000000 --- a/.github/workflows/main_hamalertspotbot.yml +++ /dev/null @@ -1,73 +0,0 @@ -# Docs for the Azure Web Apps Deploy action: https://github.com/azure/functions-action -# More GitHub Actions for Azure: https://github.com/Azure/actions -# More info on Python, GitHub Actions, and Azure Functions: https://aka.ms/python-webapps-actions - -name: Build and deploy Python project to Azure Function App - hamalertspotbot - -on: - workflow_dispatch: - -env: - AZURE_FUNCTIONAPP_PACKAGE_PATH: './' # set this to the path to your web app project, defaults to the repository root - PYTHON_VERSION: '3.11' # set this to the python version to use (supports 3.6, 3.7, 3.8) - -jobs: - build: - runs-on: ubuntu-latest - steps: - - name: Checkout repository - uses: actions/checkout@v4 - - - name: Setup Python version - uses: actions/setup-python@v1 - with: - python-version: ${{ env.PYTHON_VERSION }} - - - name: Create and start virtual environment - run: | - python -m venv venv - source venv/bin/activate - - - name: Install dependencies - run: pip install -r requirements.txt - - - name: Run Python unit tests - run: python -u -m unittest test.py - - - name: Zip artifact for deployment - run: zip release.zip ./* -r - - - name: Upload artifact for deployment job - uses: actions/upload-artifact@v4 - with: - name: python-app - path: | - release.zip - !venv/ - - deploy: - runs-on: ubuntu-latest - needs: build - environment: - name: 'staging' - url: ${{ steps.deploy-to-function.outputs.webapp-url }} - - steps: - - name: Download artifact from build job - uses: actions/download-artifact@v4 - with: - name: python-app - - - name: Unzip artifact for deployment - run: unzip release.zip - - - name: 'Deploy to Azure Functions' - uses: Azure/functions-action@v1 - id: deploy-to-function - with: - app-name: 'hamalertspotbot' - slot-name: 'staging' - package: ${{ env.AZURE_FUNCTIONAPP_PACKAGE_PATH }} - publish-profile: ${{ secrets.AZUREAPPSERVICE_PUBLISHPROFILE_STAGING }} - scm-do-build-during-deployment: true - enable-oryx-build: true diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 0000000..3cc1fb4 --- /dev/null +++ b/Dockerfile @@ -0,0 +1,9 @@ +# syntax=docker/dockerfile:1 + +FROM python:3.13 +WORKDIR / +COPY requirements.txt . +RUN pip3 install -r requirements.txt +COPY . . +EXPOSE 50505 +ENTRYPOINT ["gunicorn", "app:app"] \ No newline at end of file diff --git a/README.md b/README.md index 8deca0c..0f8f06c 100644 --- a/README.md +++ b/README.md @@ -2,7 +2,7 @@ ## Description -Spotbot is an Azure Function App to convert [HamAlert](https://hamalert.org/) alerts from [POTA](https://pota.app) or [SOTA](https://www.sota.org.uk/) in to a message format that can be forwarded on to a Discord channel webhook. +Spotbot is an application to convert [HamAlert](https://hamalert.org/) alerts from [POTA](https://pota.app) or [SOTA](https://www.sota.org.uk/) in to a message format that can be forwarded on to a Discord channel webhook. | HamAlert Configuration | | -- | @@ -19,15 +19,17 @@ You can find a live, working version of this bot in the [Cascadia Radio](https:/ ## Config -The Azure Function App expects three environment variables: +The app expects four environment variables: - `TARGET_URL` - the webhook URL from the target Discord channel. - `LOOKBACK_SECONDS` - the number of seconds to look backwards for previous messages to update instead of posting a new one - `TABLE_NAME` - the name of the table in the Azure Storage Account where the last messageIds will be stored for each callsign +- `AzureWebJobsStorage` - the full connection string to the Azure Storage Account +- `SECRET_ENDPOINT` - the name of the endpoint, kept secret to prevent abuse / unwanted messages ## Deploy Notes - Some basic tests run in `tests.py` on the creation of a new PR -- To deploy, manually run `main_hamalertspotbot.yml` +- To deploy, manually run `deploy-docker.yml` - This will deploy to the staging slot for testing. - The staging `TARGET_URL` points to my private Discord server - `LOOKBACK_SECONDS` is set to only 300 (instead of 7200) for easier testing. \ No newline at end of file diff --git a/app.py b/app.py new file mode 100644 index 0000000..dd0387b --- /dev/null +++ b/app.py @@ -0,0 +1,28 @@ +import os +from flask import Flask, request, make_response +import logging +import spotbot as sb +import tables +import discord_http +app = Flask(__name__) +endpoint = os.environ.get('SECRET_ENDPOINT') + +@app.route(f'/{endpoint}', methods=["POST"]) +def run(): + try: + sb.SpotBot(request, tables.HamAlertTable(), discord_http.DiscordHttp()).process() + except Exception as _excpt: + logging.error(f"Exception occurred: {_excpt}") + return make_response(f"Exception occurred: {_excpt}", 500) + else: + return make_response("Accepted", 202) + +''' +Empty endpoint used for keeping the container on and loaded +''' +@app.route('/', methods=["GET"]) +def always_on(): + return make_response("OK", 200) + +if __name__ == "__main__": + app.run() \ No newline at end of file diff --git a/cleanup.py b/cleanup.py deleted file mode 100644 index e4f77a1..0000000 --- a/cleanup.py +++ /dev/null @@ -1,4 +0,0 @@ -import logging - -def cleanup(): - logging.info("Executing table cleanup function") \ No newline at end of file diff --git a/function_app.py b/function_app.py deleted file mode 100644 index 897b555..0000000 --- a/function_app.py +++ /dev/null @@ -1,29 +0,0 @@ -import logging -import azure.functions as func -import spotbot as sb -import tables -import discord_http -import cleanup - -app = func.FunctionApp(http_auth_level=func.AuthLevel.FUNCTION) - -@app.route(route="spotbot", methods=[func.HttpMethod.POST]) -def spotbot(req: func.HttpRequest) -> func.HttpResponse: - try: - sb.SpotBot(req, tables.HamAlertTable(), discord_http.DiscordHttp()).process() - except Exception as _excpt: - logging.error(f"Exception occurred: {_excpt}") - return func.HttpResponse(body=f"Exception occurred: {_excpt}", status_code=500) - else: - return func.HttpResponse(status_code=202) - -@app.route(route="manual_cleanup", methods=[func.HttpMethod.POST]) -def manual_cleanup(req: func.HttpRequest) -> func.HttpResponse: - cleanup.cleanup() - return func.HttpResponse(status_code=202) - -@app.schedule(schedule="0 0 * * *", - arg_name="timer", - run_on_startup=False) -def timer_cleanup(timer: func.TimerRequest) -> None: - cleanup.cleanup() \ No newline at end of file diff --git a/gunicorn_config.py b/gunicorn_config.py new file mode 100644 index 0000000..694feaf --- /dev/null +++ b/gunicorn_config.py @@ -0,0 +1,8 @@ +import os + +workers = int(os.environ.get('GUNICORN_PROCESSES', '2')) +threads = int(os.environ.get('GUNICORN_THREADS', '4')) +# timeout = int(os.environ.get('GUNICORN_TIMEOUT', '120')) +bind = os.environ.get('GUNICORN_BIND', '0.0.0.0:50505') +forwarded_allow_ips = '*' +secure_scheme_headers = { 'X-Forwarded-Proto': 'https' } \ No newline at end of file diff --git a/host.json b/host.json deleted file mode 100644 index dccc50a..0000000 --- a/host.json +++ /dev/null @@ -1,7 +0,0 @@ -{ - "version": "2.0", - "extensionBundle": { - "id": "Microsoft.Azure.Functions.ExtensionBundle", - "version": "[4.*, 5.0.0)" - } - } \ No newline at end of file diff --git a/requirements.txt b/requirements.txt index 92739e6..d3aa0c9 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,4 +1,5 @@ -azure-functions azure-data-tables requests -pytz \ No newline at end of file +pytz +flask +gunicorn \ No newline at end of file