From ed643b575c6b1a0fda31443f6acb80567d14be97 Mon Sep 17 00:00:00 2001 From: Tom Doyle Date: Wed, 18 Aug 2021 19:49:20 +0100 Subject: [PATCH] Inital commit --- .config/config.ini | 2 + .config/gunicorn-cfg.py | 6 +++ .gitignore | 3 ++ Dockerfile | 12 ++++++ Makefile | 21 +++++++++++ README.md | 49 +++++++++++++++++++++++++ config.yml | 14 +++++++ configure | 63 ++++++++++++++++++++++++++++++++ docker-compose-traefik-local.yml | 55 ++++++++++++++++++++++++++++ docker-compose-traefik.yml | 55 ++++++++++++++++++++++++++++ docker-compose.yml | 8 ++++ requirements.txt | 8 ++++ src/app.py | 44 ++++++++++++++++++++++ 13 files changed, 340 insertions(+) create mode 100644 .config/config.ini create mode 100644 .config/gunicorn-cfg.py create mode 100644 .gitignore create mode 100644 Dockerfile create mode 100644 Makefile create mode 100644 README.md create mode 100644 config.yml create mode 100755 configure create mode 100644 docker-compose-traefik-local.yml create mode 100644 docker-compose-traefik.yml create mode 100644 docker-compose.yml create mode 100644 requirements.txt create mode 100644 src/app.py diff --git a/.config/config.ini b/.config/config.ini new file mode 100644 index 0000000..d5889e3 --- /dev/null +++ b/.config/config.ini @@ -0,0 +1,2 @@ +[app] +secret_key = diff --git a/.config/gunicorn-cfg.py b/.config/gunicorn-cfg.py new file mode 100644 index 0000000..9a7619d --- /dev/null +++ b/.config/gunicorn-cfg.py @@ -0,0 +1,6 @@ +bind = '0.0.0.0:5005' +workers = 1 +accesslog = '-' +loglevel = 'debug' +capture_output = True +enable_stdio_inheritance = True diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..14e0a47 --- /dev/null +++ b/.gitignore @@ -0,0 +1,3 @@ +src/config/ +src/api.keys +letsencrypt/ diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 0000000..9fc46e0 --- /dev/null +++ b/Dockerfile @@ -0,0 +1,12 @@ +FROM python:3.9 + +ENV FLASK_APP app.py + +COPY requirements.txt ./ +RUN pip install -r requirements.txt + +COPY . ./ + +EXPOSE 5005 +WORKDIR src/ +CMD ["gunicorn", "--config", "config/gunicorn-cfg.py", "app:app"] diff --git a/Makefile b/Makefile new file mode 100644 index 0000000..f551fc8 --- /dev/null +++ b/Makefile @@ -0,0 +1,21 @@ +config: + ./configure + +build: + ./configure + docker-compose up -d --build + +traefik: + ./configure + docker-compose --file docker-compose-traefik.yml up -d --build + +clean: + rm -rf src/config/ + rm -rf api.keys + +local: + ./configure local + docker-compose --file docker-compose-traefik-local.yml up -d --build + +stop: + docker-compose down diff --git a/README.md b/README.md new file mode 100644 index 0000000..2603577 --- /dev/null +++ b/README.md @@ -0,0 +1,49 @@ +## Python API Template + +This repo is a template for a python api with Flask + + +### Configure & Install + +Configure `config.yml` + +**Optional** To generate only the configuration files + +```bash +make config +``` + +To run container on port 5005 + +```bash +make build +``` + +To run the container with traefik proxy and letsencrypt + +```bash +make traefik +``` + +**Note:** If you are running this locally you won't be able to get a cert from letsencrypt +You should then build for local + +```bash +make local +``` + +If you want to stop all containers + +```bash +make stop +``` + +If you want to remove all generated config files +```bash +make clean +``` + + +### Issues + +If you have any questions or issues please open an issue on this repo diff --git a/config.yml b/config.yml new file mode 100644 index 0000000..f44a881 --- /dev/null +++ b/config.yml @@ -0,0 +1,14 @@ +gunicorn: + bind: '0.0.0.0:5005' + workers: 1 + accesslog: '-' + loglevel: 'debug' + capture_output: True + enable_stdio_inheritance: True +api: + example_key: 'w8iqHcy4p1a9xlQL4dQZkxA7Qo8EmcWFretixnvSPzm1iF2wUh' +app: + secret_key: WfQ2mha43Pfzwu1qYu3k4eBaKVDMV6dA9cD54cef +traefik: + letsencrypt: + email: user@example.com diff --git a/configure b/configure new file mode 100755 index 0000000..be22546 --- /dev/null +++ b/configure @@ -0,0 +1,63 @@ +#!/bin/bash + +# +# Configure script +# + + +# +# Parse config.yml +# + +parse_yaml() { + local prefix=$2 + local s='[[:space:]]*' w='[a-zA-Z0-9_]*' fs=$(echo @|tr @ '\034') + sed -ne "s|^\($s\)\($w\)$s:$s\"\(.*\)\"$s\$|\1$fs\2$fs\3|p" \ + -e "s|^\($s\)\($w\)$s:$s\(.*\)$s\$|\1$fs\2$fs\3|p" $1 | + awk -F$fs '{ + indent = length($1)/2; + vname[indent] = $2; + for (i in vname) {if (i > indent) {delete vname[i]}} + if (length($3) > 0) { + vn=""; for (i=0; i src/config/gunicorn-cfg.py +echo "workers = $config_gunicorn_workers" >> src/config/gunicorn-cfg.py +echo "accesslog = $config_gunicorn_accesslog" >> src/config/gunicorn-cfg.py +echo "loglevel = $config_gunicorn_loglevel" >> src/config/gunicorn-cfg.py +echo "capture_output = $config_gunicorn_capture_output" >> src/config/gunicorn-cfg.py +echo "enable_stdio_inheritance = $config_gunicorn_enable_stdio_inheritance" >> src/config/gunicorn-cfg.py + +# +# Generate api.keys +# + +printf "{\n $config_api_example_key : \"example_key\"\n}" > src/api.keys + +# +# Generate config.ini +# + +cp .config/config.ini src/config/config.ini +sed -i "s/secret_key = /secret_key = ${config_app_secret_key}/" src/config/config.ini + + +# +# Generate traefik config +# + +sed -i -e "s/- \"--certificatesresolvers.myresolver.acme.email=.*\"/- \"--certificatesresolvers.myresolver.acme.email=${config_traefik_letsencrypt_email}\"/" docker-compose-traefik.yml diff --git a/docker-compose-traefik-local.yml b/docker-compose-traefik-local.yml new file mode 100644 index 0000000..acac287 --- /dev/null +++ b/docker-compose-traefik-local.yml @@ -0,0 +1,55 @@ +version: '3' +services: + example_app: + container_name: example_app + restart: always + build: . + networks: + - web + labels: + - "traefik.http.routers.example.rule=Host(`app.internal`)" + - "traefik.http.routers.example.entrypoints=web" + - "traefik.http.routers.example.service=example" + - "traefik.http.services.example.loadbalancer.server.port=5005" + - "traefik.docker.network=web" + - "traefik.http.routers.example.tls=false" + + example_traefik: + container_name: "example_traefik" + image: "traefik:latest" + restart: always + command: + - "--entrypoints.web.address=:80" + - "--entrypoints.websecure.address=:443" + - "--providers.docker=true" + - "--providers.docker.exposedbydefault=true" + - "--api.dashboard=true" + - "--certificatesresolvers.myresolver.acme.httpchallenge=true" + - "--certificatesresolvers.myresolver.acme.httpchallenge.entrypoint=web" + - "--certificatesresolvers.myresolver.acme.caserver=https://acme-v01.api.letsencrypt.org/directory" + - "--certificatesresolvers.myresolver.acme.email=thomas.doyle9@mail.dcu.ie" + - "--certificatesresolvers.myresolver.acme.storage=/letsencrypt/acme.json" + ports: + - "80:80" + - "443:443" + networks: + - web + volumes: + - "./letsencrypt:/letsencrypt" + - "/var/run/docker.sock:/var/run/docker.sock:ro" + labels: + # Dashboard + - "traefik.http.routers.traefik.rule=Host(`traefik.internal`)" + - "traefik.http.routers.traefik.service=api@internal" + - "traefik.http.routers.traefik.entrypoints=web" + - "traefik.http.routers.traefik.tls=false" + - "traefik.http.routers.traefik.tls.certresolver=myresolver" + + # global redirect to https + - "traefik.http.routers.http-catchall.rule=hostregexp(`{host:.+}`)" + - "traefik.http.routers.http-catchall.entrypoints=web" + - "traefik.http.routers.http-catchall.middlewares=redirect-to-https" + +networks: + web: + external: true diff --git a/docker-compose-traefik.yml b/docker-compose-traefik.yml new file mode 100644 index 0000000..6a807b3 --- /dev/null +++ b/docker-compose-traefik.yml @@ -0,0 +1,55 @@ +version: '3' +services: + example_app: + container_name: example_app + restart: always + build: . + networks: + - web + labels: + - "traefik.http.routers.example.rule=Host(`app.your.domain`)" + - "traefik.http.routers.example.entrypoints=websecure" + - "traefik.http.routers.example.service=example" + - "traefik.http.services.example.loadbalancer.server.port=5005" + - "traefik.docker.network=web" + - "traefik.http.routers.example.tls=true" + + example_traefik: + container_name: "example_traefik" + image: "traefik:latest" + restart: always + command: + - "--entrypoints.web.address=:80" + - "--entrypoints.websecure.address=:443" + - "--providers.docker=true" + - "--providers.docker.exposedbydefault=true" + - "--api.dashboard=true" + - "--certificatesresolvers.myresolver.acme.httpchallenge=true" + - "--certificatesresolvers.myresolver.acme.httpchallenge.entrypoint=web" + - "--certificatesresolvers.myresolver.acme.caserver=https://acme-v01.api.letsencrypt.org/directory" + - "--certificatesresolvers.myresolver.acme.email=thomas.doyle9@mail.dcu.ie" + - "--certificatesresolvers.myresolver.acme.storage=/letsencrypt/acme.json" + ports: + - "80:80" + - "443:443" + networks: + - web + volumes: + - "./letsencrypt:/letsencrypt" + - "/var/run/docker.sock:/var/run/docker.sock:ro" + labels: + # Dashboard + - "traefik.http.routers.traefik.rule=Host(`your.domain`)" + - "traefik.http.routers.traefik.service=api@internal" + - "traefik.http.routers.traefik.entrypoints=websecure" + - "traefik.http.routers.traefik.tls=true" + - "traefik.http.routers.traefik.tls.certresolver=myresolver" + + # global redirect to https + - "traefik.http.routers.http-catchall.rule=hostregexp(`{host:.+}`)" + - "traefik.http.routers.http-catchall.entrypoints=web" + - "traefik.http.routers.http-catchall.middlewares=redirect-to-https" + +networks: + web: + external: true diff --git a/docker-compose.yml b/docker-compose.yml new file mode 100644 index 0000000..27729b0 --- /dev/null +++ b/docker-compose.yml @@ -0,0 +1,8 @@ +version: '3' +services: + example_app: + container_name: example_app + restart: always + build: . + ports: + - "5005:5005" diff --git a/requirements.txt b/requirements.txt new file mode 100644 index 0000000..994f54a --- /dev/null +++ b/requirements.txt @@ -0,0 +1,8 @@ +flask +flask_login +flask_migrate +flask_wtf +flask_sqlalchemy==2.* +email_validator +python-decouple +gunicorn diff --git a/src/app.py b/src/app.py new file mode 100644 index 0000000..94223ad --- /dev/null +++ b/src/app.py @@ -0,0 +1,44 @@ +#!/usr/local/env python3.9 + +import json +import configparser +from flask import Blueprint, Flask, render_template, redirect, url_for, request, jsonify, abort +from flask_login import LoginManager, login_user, logout_user, UserMixin, current_user, login_required +from flask import render_template_string, redirect +from functools import wraps + +config = configparser.ConfigParser() +config.read('config/config.ini') + +app = Flask(__name__) +app.secret_key = config['app']['secret_key'] + +def is_validkey(key): + with open('api.keys', 'r') as f: + keys = json.load(f) + if key in keys: + return True + return False + +def require_appkey(view_function): + @wraps(view_function) + def decorated_function(*args, **kwargs): + if request.headers.get('x-api-key') and is_validkey(request.headers.get('x-api-key')): + return view_function(*args, **kwargs) + else: + abort(401) + return decorated_function + +@app.route('/', methods=['GET']) +def index(): + return {"msg": "No auth needed"} + +@app.route('/auth/', methods=['POST', 'GET']) +@require_appkey +def auth(): + if request.method == 'GET': + return {"msg": "API key Valid"} + return {"msg": "API key Not Valid"} + +if __name__ == '__main__': + app.run(debug=True)