diff --git a/.gitignore b/.gitignore index 7c28183..0b53c76 100644 --- a/.gitignore +++ b/.gitignore @@ -9,6 +9,7 @@ __pycache__/ management/ management.py tests/resources/ +db-pgdata/ # Elastic Beanstalk Files .elasticbeanstalk/ diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 0000000..df917d1 --- /dev/null +++ b/Dockerfile @@ -0,0 +1,35 @@ +FROM python:3.8.8-slim-buster + + +ARG PROJECT_NAME=${PROJECT_NAME:-kps} +ARG HOST_UID=${HOST_UID:-9000} +ARG HOST_USER=${HOST_USER:-app} + +ENV HOST_HOME=/home/$HOST_USER +ENV APP_DIR=$HOST_HOME/$PROJECT_NAME +ENV PATH $HOST_HOME/.local/bin:$PATH + +# Create a user specifically for app running +# Sets them with enough permissions in its home dir +RUN adduser --home $HOST_HOME --uid $HOST_UID $HOST_USER --quiet --system --group \ + && chown -R $HOST_UID:$HOST_UID $HOST_HOME/ \ + && chmod -R 770 $HOST_HOME \ + && chmod g+s $HOST_HOME + +# Switches to created user +USER $HOST_UID + +# Creates an app dir +RUN mkdir $APP_DIR +WORKDIR $APP_DIR + +# Copies and installs requirements +COPY requirements.txt . +RUN pip install --no-cache-dir -r requirements.txt + +# Finishes copying code +COPY . . + +EXPOSE 5000 + +CMD ["sh", "docker/entrypoint.sh"] diff --git a/Makefile b/Makefile index 09c0558..abbff5c 100644 --- a/Makefile +++ b/Makefile @@ -1,8 +1,16 @@ .phony: test +PROJECT_NAME=$(notdir $(PWD)) +HOST_NAME=${USER} +CONTAINER_UID=$(HOST_NAME)_${PROJECT_NAME} +export PROJECT_NAME $(PROJECT_NAME) -tests: +# Sanity check & removal of idle postgres images +IDLE_CONTAINERS = $(shell docker ps -aq -f name=postgres -f name=web) +UP_CONTAINERS = $(shell docker ps -q -f name=postgres -f name=web) + +test-local: @echo "*** `tests` directory should exist at project root. Stop." db-migration: @@ -18,5 +26,25 @@ test-integration: test-e2e: pytest --color=yes --showlocals --tb=short -v tests/auth/e2e -test: tests db-migration test-unit test-integration test-e2e +test-local: tests db-migration test-unit test-integration test-e2e + +build: + @docker-compose build + +test: + @docker-compose -p $(CONTAINER_UID) run --rm --use-aliases --service-ports web sh docker/test.sh + @docker kill $(PROJECT_NAME)_postgres + @docker rm $(PROJECT_NAME)_postgres + +clean: + @docker-compose -p $(CONTAINER_UID) down --remove-orphans 2>/dev/null + @[ ! -z "$(UP_CONTAINERS)" ] && docker kill $(UP_CONTAINERS) || echo "Neat." + @[ ! -z "$(IDLE_CONTAINERS)" ] && docker rm $(IDLE_CONTAINERS) || echo "Clean." + +service: + @docker-compose -p $(CONTAINER_UID) up + +prune: + docker system prune -af + diff --git a/README.md b/README.md index f892c84..693d69d 100644 --- a/README.md +++ b/README.md @@ -33,21 +33,52 @@ This is project's in its early stages, and should receive a big WIP tag. We shou ## Instructions -As it is disclaimed the project current status, running *for now* means making sure tests pass. -We are shortly improving the entire installation experience and usage. Hold tight. +There are two ways to run this: i. Locally, through a virtual pyenv; ii. Using `docker-compose`. -### Step 1: Dependencies & environment +We like `Makefile` interface and while we also don't deliver an appropriate and fancy CLI, commands will be handled there. +To find out which commands are available, `cat Makefile`. + +This projects uses `poetry` to manage dependencies. Even though `poetry` is way too slow to run `poetry install`, we find +that for managing dependencies' version compatibility is a valuable tool. + +While we don't have a fully automated build pipeline, we agree to `poetry export -f requirements.txt > requirements.txt`. + +### Local docker + +Make sure docker daemon is running. + +### Step 1: Build image + +```bash +make build +``` + +### Step 2: Serve it or test it + +```bash +make service +make test +``` + +### Step 3 (Optional): Clean containers + +```bash +make clean +``` + +### Local python + +#### Step 1: Dependencies & environment -This projects uses `poetry` to manage dependencies. Having said that, how you instantiate your virtual environment is up to you. You can do that now. Inside your blank python virtual environment: ```shell -pip install poetry && poetry install +pip install -r requirements.txt ``` -### Step 2: Prepare your database +#### Step 2: Prepare your database As there aren't any containerization being done for now, you'd need `postgres` up and running in your local machine. @@ -55,12 +86,12 @@ As there aren't any containerization being done for now, you'd need `postgres` u psql -c "create database template" ``` -### Step 3: Test it +#### Step 3: Test it Right now you should be able to run the entire test-suite properly. ```shell -make test +make test-local ``` diff --git a/docker-compose.yml b/docker-compose.yml new file mode 100644 index 0000000..f5d0933 --- /dev/null +++ b/docker-compose.yml @@ -0,0 +1,53 @@ +version: "3.9" + +x-common-variables: &common-variables + POSTGRES_USER: ${POSTGRES_USER:-postgres} + POSTGRES_PASSWORD: ${POSTGRES_PASSWORD:-postgres} + POSTGRES_DB: ${POSTGRES_DB:-template} + +services: + dbpg: + container_name: ${PROJECT_NAME}_postgres + image: postgres + environment: + <<: *common-variables + PGDATA: /data/postgres + ports: + - "5432:5432" + volumes: + - ./db-pgdata:/var/lib/postgresql/data/pgdata + networks: + - backend + restart: unless-stopped + + web: + environment: + <<: *common-variables + POSTGRES_HOST: dbpg + MAX_CONCURRENCY: 1 + HOST: "0.0.0.0" + PORT: "5000" + container_name: ${PROJECT_NAME}_web + image: kms:test + build: + context: . + args: + - HOST_UID=${HOST_UID:-9000} + - HOST_USER=${HOST_USER:-app} + ports: + - "5000:5000" + depends_on: + - dbpg + volumes: + - ./:/app + networks: + - backend + restart: unless-stopped + + +networks: + backend: + driver: bridge + +volumes: + db-pgdata: diff --git a/docker/entrypoint.sh b/docker/entrypoint.sh new file mode 100755 index 0000000..6a4b42b --- /dev/null +++ b/docker/entrypoint.sh @@ -0,0 +1,2 @@ +#!/bin/bash +sh docker/uvicorn.sh diff --git a/docker/init-user-db.sh b/docker/init-user-db.sh new file mode 100755 index 0000000..559b476 --- /dev/null +++ b/docker/init-user-db.sh @@ -0,0 +1,9 @@ +#!/bin/bash + +set -e + +psql -v ON_ERROR_STOP=1 --username "$POSTGRES_USER" --dbname "$POSTGRES_DB" <<-EOSQL + CREATE USER tester; + CREATE DATABASE court; + GRANT ALL PRIVILEGES ON DATABASE court TO tester; +EOSQL diff --git a/docker/migration.sh b/docker/migration.sh new file mode 100755 index 0000000..a2e2a5c --- /dev/null +++ b/docker/migration.sh @@ -0,0 +1,2 @@ +alembic -x data=true downgrade base +alembic -x data=true upgrade head diff --git a/docker/test.sh b/docker/test.sh new file mode 100755 index 0000000..7362247 --- /dev/null +++ b/docker/test.sh @@ -0,0 +1,5 @@ +#!/bash/sh +sh docker/migration.sh +pytest --color=yes --showlocals --tb=short -v tests/auth/unit +pytest --color=yes --showlocals --tb=short -v tests/auth/integration +pytest --color=yes --showlocals --tb=short -v tests/auth/e2e diff --git a/docker/uvicorn.sh b/docker/uvicorn.sh new file mode 100755 index 0000000..a6e1053 --- /dev/null +++ b/docker/uvicorn.sh @@ -0,0 +1 @@ +uvicorn src.server.app:app --port $PORT --host $HOST --loop uvloop --log-level info --workers $MAX_CONCURRENCY diff --git a/migrations/env.py b/migrations/env.py index bbbd0dd..daeada8 100644 --- a/migrations/env.py +++ b/migrations/env.py @@ -12,7 +12,7 @@ from alembic import context from src import config as config_app -from src.federation import init +from src.federation import init # this is the Alembic Config object, which provides # access to the values within the .ini file in use. diff --git a/src/config.py b/src/config.py index 4fd42bb..6076742 100644 --- a/src/config.py +++ b/src/config.py @@ -2,7 +2,20 @@ from typing import Any POSTGRES_URI_TEMPLATE = "postgresql://{}:{}@{}:{}/{}" -POSTGRES_DEFAULT = ("postgres", "", "localhost", 5432, "template") + +PG_USER = os.environ.get("POSTGRES_USER", "postgres") +PG_PASSWORD = os.environ.get("POSTGRES_PASSWORD", "postgres") +PG_HOST = os.environ.get("POSTGRES_HOST", "localhost") +PG_PORT = os.environ.get("POSTGRES_PORT", 5432) +PG_DB = os.environ.get("POSTGRES_DB", "template") + +POSTGRES_DEFAULT = ( + PG_USER, + PG_PASSWORD, + PG_HOST, + PG_PORT, + PG_DB +) def default_user() -> tuple: diff --git a/src/core/ports/unit_of_work.py b/src/core/ports/unit_of_work.py index a23d2e4..95381cd 100644 --- a/src/core/ports/unit_of_work.py +++ b/src/core/ports/unit_of_work.py @@ -1,20 +1,9 @@ import abc from typing import Callable, Generator -from sqlalchemy import create_engine, orm - -from src import config +from src import orm from src.core.ports import repository -DEFAULT_SESSION_FACTORY = orm.sessionmaker( - bind=create_engine( - # ISOLATION LEVEL ENSURES aggregate's version IS RESPECTED - # That is, if version differs it will raise an exception - config.get_postgres_uri(), - isolation_level="REPEATABLE_READ", - ), - autoflush=False, -) class AbstractUnitOfWork(abc.ABC): @@ -50,7 +39,7 @@ def rollback(self) -> None: class SqlAlchemyUnitOfWork(AbstractUnitOfWork): session: orm.Session - def __init__(self, session_factory: Callable = DEFAULT_SESSION_FACTORY): + def __init__(self, session_factory: Callable = orm.DEFAULT_SESSION_FACTORY): self.session_factory: Callable = session_factory def __exit__(self, *args): # type: ignore diff --git a/src/orm.py b/src/orm.py index f98818e..fbc9886 100644 --- a/src/orm.py +++ b/src/orm.py @@ -1,6 +1,18 @@ -from sqlalchemy import MetaData +from sqlalchemy.orm import Session +from sqlalchemy import create_engine, orm, MetaData + +from src import config import src.auth.adapters.orm +DEFAULT_SESSION_FACTORY: Session = orm.sessionmaker( + bind=create_engine( + # ISOLATION LEVEL ENSURES aggregate's version IS RESPECTED + # That is, if version differs it will raise an exception + config.get_postgres_uri(), + isolation_level="REPEATABLE_READ", + ), + autoflush=False, +) def start_mappers() -> MetaData: """