diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 00000000..1deeff16 --- /dev/null +++ b/Dockerfile @@ -0,0 +1,28 @@ +FROM ghcr.io/sartography/python:3.9 + +RUN pip install poetry +RUN useradd _gunicorn --no-create-home --user-group + +RUN apt-get update && \ + apt-get install -y -q \ + gcc libssl-dev \ + curl git-core libpq-dev \ + gunicorn3 default-mysql-client + +WORKDIR /app +COPY pyproject.toml poetry.lock /app/ +RUN poetry install + +RUN set -xe \ + && apt-get remove -y gcc python3-dev libssl-dev \ + && apt-get autoremove -y \ + && apt-get clean -y \ + && rm -rf /var/lib/apt/lists/* + +COPY . /app/ + +# run poetry install again AFTER copying the app into the image +# otherwise it does not know what the main app module is +RUN poetry install + +CMD ./bin/boot_server_in_docker diff --git a/bin/boot_server_in_docker b/bin/boot_server_in_docker new file mode 100755 index 00000000..22d6eab1 --- /dev/null +++ b/bin/boot_server_in_docker @@ -0,0 +1,33 @@ +#!/usr/bin/env bash + +function error_handler() { + >&2 echo "Exited with BAD EXIT CODE '${2}' in ${0} script at line: ${1}." + exit "$2" +} +trap 'error_handler ${LINENO} $?' ERR +set -o errtrace -o errexit -o nounset -o pipefail + +# run migrations +export FLASK_APP=/app/src/spiffworkflow_backend + +if [ "${DOWNGRADE_DB:-}" = "true" ]; then + echo 'Downgrading database...' + poetry run flask db downgrade +fi + +if [ "${UPGRADE_DB:-}" = "true" ]; then + echo 'Upgrading database...' + poetry run flask db upgrade +fi + +port="${PORT0:-}" +if [[ -z "$port" ]]; then + port=7000 +fi + +# THIS MUST BE THE LAST COMMAND! +if [ "${APPLICATION_ROOT:-}" = "/" ]; then + exec poetry run gunicorn --bind "0.0.0.0:$PORT0" wsgi:app +else + exec poetry run gunicorn -e SCRIPT_NAME="$APPLICATION_ROOT" --bind "0.0.0.0:$PORT0" wsgi:app +fi diff --git a/bin/build_and_run_with_docker_compose b/bin/build_and_run_with_docker_compose new file mode 100755 index 00000000..31da780e --- /dev/null +++ b/bin/build_and_run_with_docker_compose @@ -0,0 +1,15 @@ +#!/usr/bin/env bash + +function error_handler() { + >&2 echo "Exited with BAD EXIT CODE '${2}' in ${0} script at line: ${1}." + exit "$2" +} +trap 'error_handler ${LINENO} $?' ERR +set -o errtrace -o errexit -o nounset -o pipefail + +if [[ -z "${BPMN_SPEC_ABSOLUTE_DIR:-}" ]]; then + script_dir="$( cd -- "$(dirname "$0")" >/dev/null 2>&1 ; pwd -P )" + export BPMN_SPEC_ABSOLUTE_DIR="$script_dir/../../sample-process-models" +fi + +docker compose up --build diff --git a/bin/run_server_locally b/bin/run_server_locally index 285bc146..6da98e91 100755 --- a/bin/run_server_locally +++ b/bin/run_server_locally @@ -16,4 +16,6 @@ if [[ -z "${BPMN_SPEC_ABSOLUTE_DIR:-}" ]]; then export BPMN_SPEC_ABSOLUTE_DIR="$script_dir/../../sample-process-models" fi +export FLASK_SESSION_SECRET_KEY=super_secret_key + FLASK_APP=src/spiffworkflow_backend poetry run flask run diff --git a/docker-compose.yml b/docker-compose.yml new file mode 100644 index 00000000..e10fda36 --- /dev/null +++ b/docker-compose.yml @@ -0,0 +1,43 @@ +version: "3.8" +services: + db: + container_name: db + image: mysql:8.0.29 + cap_add: + - SYS_NICE + restart: always + environment: + - MYSQL_DATABASE=spiffworkflow_backend_staging + - MYSQL_ROOT_PASSWORD=St4g3Th1515 + ports: + - '7001:3306' + volumes: + - spiffworkflow_backend:/var/lib/mysql + + spiffworkflow-backend: + container_name: spiffworkflow-backend + # command: tail -f /etc/hostname + depends_on: + - db + # image: sartography/cr-connect-workflow:dev + build: + context: . + environment: + - APPLICATION_ROOT=/ + - FLASK_ENV=staging + - FLASK_SESSION_SECRET_KEY=super_secret_key + - DEVELOPMENT=true + - LDAP_URL=mock + - PORT0=7000 + - PRODUCTION=false + - UPGRADE_DB=true + - DATABASE_URI=mysql+mysqlconnector://root:St4g3Th1515@db/spiffworkflow_backend_staging + - BPMN_SPEC_ABSOLUTE_DIR=/app/process_models + ports: + - "7000:7000" + volumes: + - ${BPMN_SPEC_ABSOLUTE_DIR}:/app/process_models + +volumes: + spiffworkflow_backend: + driver: local diff --git a/poetry.lock b/poetry.lock index a72c82c7..6712967d 100644 --- a/poetry.lock +++ b/poetry.lock @@ -384,6 +384,7 @@ jsonschema = ">=2.5.1,<5" packaging = ">=20" PyYAML = ">=5.1,<7" requests = ">=2.9.1,<3" +swagger-ui-bundle = {version = ">=0.0.2,<0.1", optional = true, markers = "extra == \"swagger-ui\""} werkzeug = ">=1.0,<3" [package.extras] @@ -765,6 +766,20 @@ python-versions = ">=2.7,!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,!=3.4.*" [package.extras] docs = ["sphinx"] +[[package]] +name = "gunicorn" +version = "20.1.0" +description = "WSGI HTTP Server for UNIX" +category = "main" +optional = false +python-versions = ">=3.5" + +[package.extras] +eventlet = ["eventlet (>=0.24.1)"] +gevent = ["gevent (>=1.4.0)"] +setproctitle = ["setproctitle"] +tornado = ["tornado (>=0.2)"] + [[package]] name = "identify" version = "2.5.1" @@ -1779,6 +1794,17 @@ python-versions = ">=3.6" [package.dependencies] pbr = ">=2.0.0,<2.1.0 || >2.1.0" +[[package]] +name = "swagger-ui-bundle" +version = "0.0.9" +description = "swagger_ui_bundle - swagger-ui files in a pip package" +category = "main" +optional = false +python-versions = "*" + +[package.dependencies] +Jinja2 = ">=2.0" + [[package]] name = "tokenize-rt" version = "4.2.1" @@ -1991,7 +2017,7 @@ testing = ["pytest (>=6)", "pytest-checkdocs (>=2.4)", "pytest-flake8", "pytest- [metadata] lock-version = "1.1" python-versions = "^3.8" -content-hash = "217b8108526defaaac72e0d98f15e82efda8b8f4698ea8b52623dc45708a8d70" +content-hash = "b9b8fb8a5f20adbc1ea2a1e8990c3d1e58e84c62d0a9e9f33e21137686630695" [metadata.files] alabaster = [ @@ -2395,6 +2421,10 @@ greenlet = [ {file = "greenlet-1.1.2-cp39-cp39-win_amd64.whl", hash = "sha256:013d61294b6cd8fe3242932c1c5e36e5d1db2c8afb58606c5a67efce62c1f5fd"}, {file = "greenlet-1.1.2.tar.gz", hash = "sha256:e30f5ea4ae2346e62cedde8794a56858a67b878dd79f7df76a0767e356b1744a"}, ] +gunicorn = [ + {file = "gunicorn-20.1.0-py3-none-any.whl", hash = "sha256:9dcc4547dbb1cb284accfb15ab5667a0e5d1881cc443e0677b4882a4067a807e"}, + {file = "gunicorn-20.1.0.tar.gz", hash = "sha256:e0a968b5ba15f8a328fdfd7ab1fcb5af4470c28aaf7e55df02a99bc13138e6e8"}, +] identify = [ {file = "identify-2.5.1-py2.py3-none-any.whl", hash = "sha256:0dca2ea3e4381c435ef9c33ba100a78a9b40c0bab11189c7cf121f75815efeaa"}, {file = "identify-2.5.1.tar.gz", hash = "sha256:3d11b16f3fe19f52039fb7e39c9c884b21cb1b586988114fbe42671f03de3e82"}, @@ -3111,6 +3141,10 @@ stevedore = [ {file = "stevedore-3.5.0-py3-none-any.whl", hash = "sha256:a547de73308fd7e90075bb4d301405bebf705292fa90a90fc3bcf9133f58616c"}, {file = "stevedore-3.5.0.tar.gz", hash = "sha256:f40253887d8712eaa2bb0ea3830374416736dc8ec0e22f5a65092c1174c44335"}, ] +swagger-ui-bundle = [ + {file = "swagger_ui_bundle-0.0.9-py3-none-any.whl", hash = "sha256:cea116ed81147c345001027325c1ddc9ca78c1ee7319935c3c75d3669279d575"}, + {file = "swagger_ui_bundle-0.0.9.tar.gz", hash = "sha256:b462aa1460261796ab78fd4663961a7f6f347ce01760f1303bbbdf630f11f516"}, +] tokenize-rt = [ {file = "tokenize_rt-4.2.1-py2.py3-none-any.whl", hash = "sha256:08a27fa032a81cf45e8858d0ac706004fcd523e8463415ddf1442be38e204ea8"}, {file = "tokenize_rt-4.2.1.tar.gz", hash = "sha256:0d4f69026fed520f8a1e0103aa36c406ef4661417f20ca643f913e33531b3b94"}, diff --git a/pyproject.toml b/pyproject.toml index f6fe3359..9f67d8ba 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -37,11 +37,12 @@ pytest-flask = "^1.2.0" pytest-flask-sqlalchemy = "^1.1.0" psycopg2 = "^2.9.3" typing-extensions = "^4.2.0" -connexion = "^2.13.1" +connexion = {extras = [ "swagger-ui",], version = "^2.13.1"} lxml = "^4.8.0" marshmallow-enum = "^1.5.1" marshmallow-sqlalchemy = "^0.28.0" PyJWT = "^2.4.0" +gunicorn = "^20.1.0" [tool.poetry.dev-dependencies] diff --git a/src/spiffworkflow_backend/__init__.py b/src/spiffworkflow_backend/__init__.py index 993af0a6..e41aeb27 100644 --- a/src/spiffworkflow_backend/__init__.py +++ b/src/spiffworkflow_backend/__init__.py @@ -26,9 +26,13 @@ def create_app() -> flask.app.Flask: ) app = connexion_app.app app.config["CONNEXION_APP"] = connexion_app - app.secret_key = "super secret key" app.config["SESSION_TYPE"] = "filesystem" + if os.environ.get("FLASK_SESSION_SECRET_KEY") is None: + raise KeyError("Cannot find the secret_key from the environment. Please set FLASK_SESSION_SECRET_KEY") + + app.secret_key = os.environ.get("FLASK_SESSION_SECRET_KEY") + setup_config(app) db.init_app(app) migrate.init_app(app, db) @@ -40,7 +44,4 @@ def create_app() -> flask.app.Flask: app.register_blueprint(admin_blueprint, url_prefix="/admin") connexion_app.add_api("api.yml", base_path="/v1.0") - for name, value in app.config.items(): - print(f"{name} = {value}") - return app diff --git a/src/spiffworkflow_backend/config/__init__.py b/src/spiffworkflow_backend/config/__init__.py index d37fc794..841f5c95 100644 --- a/src/spiffworkflow_backend/config/__init__.py +++ b/src/spiffworkflow_backend/config/__init__.py @@ -16,18 +16,21 @@ def setup_config(app: Flask) -> None: app.config["SQLALCHEMY_TRACK_MODIFICATIONS"] = False app.config.from_object("spiffworkflow_backend.config.default") - if os.environ.get("SPIFF_DATABASE_TYPE") == "sqlite": - app.config[ - "SQLALCHEMY_DATABASE_URI" - ] = f"sqlite:///{app.instance_path}/db_{app.env}.sqlite3" + if os.environ.get("DATABASE_URI") is None: + if os.environ.get("SPIFF_DATABASE_TYPE") == "sqlite": + app.config[ + "SQLALCHEMY_DATABASE_URI" + ] = f"sqlite:///{app.instance_path}/db_{app.env}.sqlite3" + else: + # use pswd to trick flake8 with hardcoded passwords + db_pswd = os.environ.get("DB_PASSWORD") + if db_pswd is None: + db_pswd = "" + app.config[ + "SQLALCHEMY_DATABASE_URI" + ] = f"mysql+mysqlconnector://root:{db_pswd}@localhost/spiffworkflow_backend_{app.env}" else: - # use pswd to trick flake8 with hardcoded passwords - mysql_pswd = os.environ.get("MYSQL_PASSWORD") - if mysql_pswd is None: - mysql_pswd = "" - app.config[ - "SQLALCHEMY_DATABASE_URI" - ] = f"mysql+mysqlconnector://root:{mysql_pswd}@localhost/spiffworkflow_backend_{app.env}" + app.config["SQLALCHEMY_DATABASE_URI"] = os.environ.get("DATABASE_URI") env_config_module = "spiffworkflow_backend.config." + app.env try: diff --git a/src/spiffworkflow_backend/config/staging.py b/src/spiffworkflow_backend/config/staging.py new file mode 100644 index 00000000..cca69ba7 --- /dev/null +++ b/src/spiffworkflow_backend/config/staging.py @@ -0,0 +1 @@ +"""Staging.""" diff --git a/src/spiffworkflow_backend/routes/admin_blueprint/static/style.css b/src/spiffworkflow_backend/routes/admin_blueprint/static/style.css index e69de29b..a34218a4 100644 --- a/src/spiffworkflow_backend/routes/admin_blueprint/static/style.css +++ b/src/spiffworkflow_backend/routes/admin_blueprint/static/style.css @@ -0,0 +1 @@ +.example { } diff --git a/wsgi.py b/wsgi.py new file mode 100644 index 00000000..8772198b --- /dev/null +++ b/wsgi.py @@ -0,0 +1,25 @@ +from werkzeug.exceptions import NotFound +from werkzeug.middleware.dispatcher import DispatcherMiddleware +from werkzeug.middleware.proxy_fix import ProxyFix + +from spiffworkflow_backend import create_app + +app = create_app() + +if __name__ == "__main__": + def no_app(environ, start_response): + return NotFound()(environ, start_response) + + # Remove trailing slash, but add leading slash + base_url = '/' + app.config['APPLICATION_ROOT'].strip('/') + routes = {'/': app.wsgi_app} + + if base_url != '/': + routes[base_url] = app.wsgi_app + + app.wsgi_app = DispatcherMiddleware(no_app, routes) + app.wsgi_app = ProxyFix(app.wsgi_app) + + flask_port = app.config['FLASK_PORT'] + + app.run(host='0.0.0.0', port=flask_port)