From 54e5fe88970fc446f982161c72f20fd2da40a162 Mon Sep 17 00:00:00 2001 From: gazev Date: Sat, 16 Nov 2024 18:43:55 +0000 Subject: [PATCH] chore: improve docker compose to use volumes --- .dockerignore | 2 -- .env.example | 14 ++++++-------- Dockerfile | 5 ++++- README.md | 24 +++--------------------- app/__init__.py | 16 +++++++++++----- app/api/auth/routes.py | 6 +++--- app/api/auth/utils.py | 2 +- app/config.py | 10 +++++----- app/logos/logos_handler.py | 10 +++------- docker-compose.yaml | 33 ++++++++++++++++++--------------- entrypoint.sh | 5 +++++ requirements-prod.txt | 5 +++++ 12 files changed, 64 insertions(+), 68 deletions(-) create mode 100755 entrypoint.sh diff --git a/.dockerignore b/.dockerignore index 104a7db..cb9cf24 100644 --- a/.dockerignore +++ b/.dockerignore @@ -6,5 +6,3 @@ README.md requirements.txt .env.example .env - -data/ diff --git a/.env.example b/.env.example index 738101e..7636a9f 100644 --- a/.env.example +++ b/.env.example @@ -1,12 +1,10 @@ -# Please provided relative paths with ./ or things can break :) - SESSION_DIR="data/flask_sessions/" -SESSION_LIFETIME="10800" # 3 hours +SESSION_LIFETIME="10800" -DATABASE_PATH="data/hackerschool.sqlite3" +DATABASE_PATH="data/db/hackerschool.sqlite3" ROLES_PATH="data/roles.json" -PHOTOS_DIR="data/photos/" -MAX_FILE_UPLOAD_LENGTH="16777216" # 16 MiB +STATIC_DIR="data/static/" +MAX_FILE_UPLOAD_LENGTH="16777216" LOG_LEVEL="INFO" LOGS_PATH="data/logs/app.log" @@ -14,8 +12,8 @@ LOGS_PATH="data/logs/app.log" ADMIN_USERNAME="admin" ADMIN_PASSWORD="admin" -FRONTEND_URI="http://localhost:3000" +FRONTEND_ORIGIN="http://localhost:3000" -OAUTH_CALLBACK="" +FENIX_REDIRECT_URL="" CLIENT_ID="" CLIENT_SECRET="" \ No newline at end of file diff --git a/Dockerfile b/Dockerfile index c2b5973..0bcbec0 100644 --- a/Dockerfile +++ b/Dockerfile @@ -5,4 +5,7 @@ COPY requirements-prod.txt . RUN pip install --no-cache-dir -r requirements-prod.txt COPY . . EXPOSE 8000 -CMD ["gunicorn", "--bind", "0.0.0.0:8000", "app:create_app()"] \ No newline at end of file +RUN chmod u+x entrypoint.sh + +ENTRYPOINT [ "./entrypoint.sh" ] +CMD ["gunicorn", "--bind", "0.0.0.0:8000", "app:create_app()"] \ No newline at end of file diff --git a/README.md b/README.md index d00ac9a..ffa1bc3 100644 --- a/README.md +++ b/README.md @@ -64,41 +64,23 @@ flask create-admin flask run --debug ``` - -### Docker Setup (Optional) -1. **Create all application-data folders**: -`docker compose` will mount the database and required folders into the container. For this you need to initialize the database, please refer to the [installation steps 1 to 5](#installation) to set this up. - -1. **Build the docker image**: -```bash -docker compose build -``` -2. **Run the application (with gunicorn)**: -```bash -docker compose up -``` - ## Environment Variables **TLDR**: You can just `cp .env.example .env` to get all default environment variables ready. - - `SESSION_DIR`: Where to store session files (defaults to `data/flask_sessions/`) - `SESSION_LIFETIME`: How long a session should last in seconds (defaults to 3 hours) -- `DATABASE_PATH`: Path to the `sqlite3` database file (defaults to `data/hackerschool.sqlite3`) +- `DATABASE_PATH`: Path to the `sqlite3` database file (defaults to `data/db/hackerschool.sqlite3`) +- `STATIC_DIR`: Path to the folder where user and project images will be stored (defaults to `data/static/`) - `ROLES_PATH`: Path to the roles configuration json file (defaults to `data/roles.json`) -- `PHOTOS_DIR`: Path to the folder where user and project images will be stored (defaults to `data/photos/`) - `LOG_LEVEL`: Log level (defaults to INFO) -- `LOGS_PATH` Path to logs file (deault to stdout) +- `LOGS_PATH` Path to logs file (deaults to stdout) These will only be necessary if you'll be using the `flask create-admin` command - `ADMIN_USERNAME`: Admin username - `ADMIN_PASSWORD`: Admin password - -**Note**: If you use `docker compose` you will either need the `.env` file or environment variables set (no default values will be used) because `docker compose` will need them to mount the correct volumes. - ## Project Structure The project follows a layered architecture with a controller, service and models layer. ``` diff --git a/app/__init__.py b/app/__init__.py index fb33353..30757d1 100644 --- a/app/__init__.py +++ b/app/__init__.py @@ -4,7 +4,7 @@ from flask import Flask from flask_cors import CORS -from app.config import Config +from app.config import Config, basedir from app.extensions import session from app.extensions import db from app.extensions import migrate @@ -17,6 +17,11 @@ def create_app(config_class=Config): # Initialize extensions session.init_app(flask_app) + + db_dir = os.path.dirname(flask_app.config.get("DATABASE_PATH")) + if not os.path.exists(db_dir): + os.makedirs(db_dir) + db.init_app(flask_app) migrate.init_app(flask_app, db) @@ -27,7 +32,7 @@ def create_app(config_class=Config): register_error_handlers(flask_app) register_commands(flask_app) - if (frontend_uri := flask_app.config.get("FRONTEND_URI", "")) != "": + if (frontend_uri := flask_app.config.get("FRONTEND_ORIGIN", "")) != "": CORS(flask_app, origins=[frontend_uri], supports_credentials=True) setup_logger(flask_app) @@ -71,16 +76,17 @@ def register_commands(app: Flask): register_create_admin_user_command(app) def setup_logger(app: Flask): - if app.debug or app.config.get("LOGS_PATH", "") == "": # don't set logger in debug + logs_path = app.config.get("LOGS_PATH") + if app.debug or logs_path == basedir: # don't set logger in debug or if not log file return levels = {"DEBUG": logging.DEBUG, "INFO": logging.INFO, "WARNING": logging.WARNING} - app.logger.setLevel(levels[app.config.get("LOG_LEVEL")]) - logs_path = app.config.get("LOGS_PATH") log_dir = os.path.dirname(logs_path) if not os.path.exists(log_dir): os.makedirs(log_dir) + + app.logger.setLevel(levels[app.config.get("LOG_LEVEL")]) handler = logging.FileHandler(logs_path) BASIC_FORMAT = "[%(asctime)s] %(levelname)s [%(name)s.%(funcName)s:%(lineno)d] %(message)s" handler.setFormatter(logging.Formatter(BASIC_FORMAT)) diff --git a/app/api/auth/routes.py b/app/api/auth/routes.py index aca2db0..7f315b5 100644 --- a/app/api/auth/routes.py +++ b/app/api/auth/routes.py @@ -56,7 +56,7 @@ def logout(): def fenix_auth(): if current_app.config.get("CLIENT_ID", "") == "" \ or current_app.config.get("CLIENT_SECRET", "") == "" \ - or current_app.config.get("OAUTH_CALLBACK") == "": + or current_app.config.get("FENIX_REDIRECT_URL") == "": throw_api_error(HTTPStatus.NOT_IMPLEMENTED, {"error": "Unsupported"}) state = generate_random_state() @@ -65,7 +65,7 @@ def fenix_auth(): session['state'] = state params = { "client_id": current_app.config.get("CLIENT_ID"), - "redirect_uri": current_app.config.get("OAUTH_CALLBACK"), + "redirect_uri": current_app.config.get("FENIX_REDIRECT_URL"), "state": state } return redirect("https://fenix.tecnico.ulisboa.pt/oauth/userdialog?" + urlencode(params)) @@ -85,7 +85,7 @@ def fenix_auth_callback(): params = { "client_id": current_app.config.get("CLIENT_ID"), "client_secret": current_app.config.get("CLIENT_SECRET"), - "redirect_uri": current_app.config.get("OAUTH_CALLBACK"), + "redirect_uri": current_app.config.get("FENIX_REDIRECT_URL"), "code": code, "grant_type": "authorization_code" } diff --git a/app/api/auth/utils.py b/app/api/auth/utils.py index 9aa497e..8984ea5 100644 --- a/app/api/auth/utils.py +++ b/app/api/auth/utils.py @@ -38,7 +38,7 @@ def fetch_access_token(url: str) -> str | None: return access_token -def get_user_info(access_token: str) -> Tuple[bool, Optional[str], Optional[str]]: +def get_user_info(access_token: str) -> Tuple[bool, Optional[str], Optional[str], Optional[str]]: r = requests.get( "https://fenix.tecnico.ulisboa.pt/api/fenix/v1/person?" + urlencode({"access_token": access_token}) ) diff --git a/app/config.py b/app/config.py index c00a96d..970a86e 100644 --- a/app/config.py +++ b/app/config.py @@ -4,7 +4,7 @@ from datetime import timedelta from dotenv import load_dotenv -basedir = os.path.abspath(os.path.dirname(__file__)) + "/.." # the repository folder +basedir = os.path.abspath(os.path.abspath(os.path.dirname(__file__)) + "/..") # the repository folder load_dotenv(os.path.join(basedir, ".env")) @@ -32,22 +32,22 @@ class Config: PERMANENT_SESSION_LIFETIME = _get_int_env_or_default("SESSION_LIFETIME", 3 * 60 * 60) SESSION_FILE_DIR = os.path.join(basedir, _get_env_or_default("SESSION_DIR", "data/flask_sessions/")).rstrip("/") - DATABASE_PATH = os.path.join(basedir, _get_env_or_default("DATABASE_PATH", "data/hackerschool.sqlite3")) + DATABASE_PATH = os.path.join(basedir, _get_env_or_default("DATABASE_PATH", "data/db/hackerschool.sqlite3")) SQLALCHEMY_DATABASE_URI = 'sqlite:///' + DATABASE_PATH ROLES_PATH = os.path.join(basedir, _get_env_or_default("ROLES_PATH", "data/roles.json")) - PHOTOS_DIR = os.path.join(basedir, _get_env_or_default("PHOTOS_DIR", "data/photos/")).rstrip("/") + STATIC_DIR = os.path.join(basedir, _get_env_or_default("STATIC_DIR", "data/static/")).rstrip("/") MAX_CONTENT_LENGTH = _get_int_env_or_default("MAX_FILE_UPLOAD_LENGTH", 16 * 1024 * 1024) LOGS_PATH = os.path.join(basedir, _get_env_or_default("LOGS_PATH", "")) LOG_LEVEL = _get_env_or_default("LOG_LEVEL", "INFO") - FRONTEND_URI = _get_env_or_default("FRONTEND_URI", "http://localhost:3000") + FRONTEND_ORIGIN = _get_env_or_default("FRONTEND_ORIGIN", "http://localhost:3000") ADMIN_USERNAME = _get_env_or_default("ADMIN_USERNAME", "admin") ADMIN_PASSWORD = _get_env_or_default("ADMIN_PASSWORD", "admin") CLIENT_ID = _get_env_or_default("CLIENT_ID", "") CLIENT_SECRET = _get_env_or_default("CLIENT_SECRET", "") - OAUTH_CALLBACK = _get_env_or_default("OAUTH_CALLBACK", "") + FENIX_REDIRECT_URL = _get_env_or_default("FENIX_REDIRECT_URL", "") diff --git a/app/logos/logos_handler.py b/app/logos/logos_handler.py index 0b75b67..ad8f2d1 100644 --- a/app/logos/logos_handler.py +++ b/app/logos/logos_handler.py @@ -13,7 +13,6 @@ def _is_valid_content_type(content_type: str) -> bool: class LogosHandler: def __init__(self): - self.photos_dir = None self.members_path = None self.projects_path = None @@ -88,12 +87,9 @@ def _get_logo_directory(self, logo_type: str) -> str: raise InvalidLogoTypeError(logo_type) def init_app(self, app: Flask) -> None: - self.photos_dir = app.config["PHOTOS_DIR"].rstrip("/") - self.members_path = self.photos_dir+"/members" - self.projects_path = self.photos_dir+"/projects" - - if not os.path.exists(self.photos_dir): - os.makedirs(self.photos_dir) + static_dir = app.config["STATIC_DIR"].rstrip("/") + self.members_path = static_dir+"/members" + self.projects_path = static_dir+"/projects" if not os.path.exists(self.members_path): os.makedirs(self.members_path) if not os.path.exists(self.projects_path): diff --git a/docker-compose.yaml b/docker-compose.yaml index 346419f..be9f752 100644 --- a/docker-compose.yaml +++ b/docker-compose.yaml @@ -1,22 +1,25 @@ services: - app: + hs-api: build: . ports: - "8000:8000" volumes: - - ${DATABASE_PATH}:/hs-api/data/hackerschool.sqlite3 - - ${ROLES_PATH}:/hs-api/data/roles.json - - ${SESSION_DIR}:/hs-api/data/flask_sessions/ - - ${PHOTOS_DIR}:/hs-api/data/photos - - ${LOGS_PATH}:/hs-api/data/logs/log.app + - hs_db:/hs-api/data/db + - hs_static:/hs-api/data/static/ + - hs_logs:/hs-api/data/logs/ environment: - - DATABASE_PATH=/hs-api/data/hackerschool.sqlite3 - - ROLES_PATH=/hs-api/data/roles.json - - SESSION_DIR=/hs-api/data/flask_sessions/ - - PHOTOS_DIR=/hs-api/data/photos/ - - LOGS_PATH=/hs-api/data/logs/app.log - - LOG_LEVEL=${LOG_LEVEL} - - SESSION_LIFETIME=${SESSION_LIFETIME} - - MAX_FILE_UPLOAD_LENGTH=${MAX_FILE_UPLOAD_LENGTH} - - FRONTEND_URI=${FRONTEND_URI} + - DATABASE_PATH=data/db/hackerschool.sqlite3 + - STATIC_DIR=data/static/ + - ROLES_PATH=data/roles.json + - SESSION_DIR=data/flask_sessions/ + - LOGS_PATH=data/logs/app.log + - LOG_LEVEL=INFO + - MAX_FILE_UPLOAD_LENGTH=16777216 + - CLIENT_ID= + - CLIENT_SECRET= + - FENIX_REDIRECT_URL= +volumes: + hs_db: + hs_static: + hs_logs: diff --git a/entrypoint.sh b/entrypoint.sh new file mode 100755 index 0000000..0cfa445 --- /dev/null +++ b/entrypoint.sh @@ -0,0 +1,5 @@ +#! /bin/sh + +flask db upgrade + +exec "$@" \ No newline at end of file diff --git a/requirements-prod.txt b/requirements-prod.txt index 1e61c0e..26f0c07 100644 --- a/requirements-prod.txt +++ b/requirements-prod.txt @@ -3,6 +3,8 @@ attrs==24.2.0 bcrypt==4.2.0 blinker==1.9.0 cachelib==0.13.0 +certifi==2024.8.30 +charset-normalizer==3.4.0 click==8.1.7 Flask==3.1.0 Flask-Cors==5.0.0 @@ -11,6 +13,7 @@ Flask-Session==0.8.0 Flask-SQLAlchemy==3.1.1 greenlet==3.1.1 gunicorn==23.0.0 +idna==3.10 itsdangerous==2.2.0 Jinja2==3.1.4 jsonschema==4.23.0 @@ -21,7 +24,9 @@ msgspec==0.18.6 packaging==24.2 python-dotenv==1.0.1 referencing==0.35.1 +requests==2.32.3 rpds-py==0.21.0 SQLAlchemy==2.0.36 typing_extensions==4.12.2 +urllib3==2.2.3 Werkzeug==3.1.3