Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Dockerized dev environment #71

Open
wants to merge 18 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
30 changes: 30 additions & 0 deletions Dockerfile
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
FROM python:3.7-slim

ENV FLASK_APP=runserver.py
ENV FLASK_RUN_HOST=0.0.0.0

WORKDIR /app

# libz and libjpeg are needed for pillow (PIL)
RUN apt-get update && apt-get install -y \
gcc \
libpq-dev \
libz-dev \
libjpeg-dev

COPY requirements.txt /app/requirements.txt
COPY dev-requirements.txt /app/dev-requirements.txt
RUN pip install -r /app/requirements.txt
# Just to fix an issue installing black, uninstall typing
# https://stackoverflow.com/questions/55833509/attributeerror-type-object-callable-has-no-attribute-abc-registry
RUN pip uninstall -y typing
RUN pip install -r /app/dev-requirements.txt

# Clean some space
# RUN apt-get autoremove -y gcc

# COPY . /app
expose 5000

CMD [ "flask", "run" ]

70 changes: 70 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -34,10 +34,15 @@ A Flask webapp using Python Image Library to reconstruct and display a summary o

`DEBUG` set to True for helpful debugging; never set to True in production environment

`ASSETS_PATH` where images are stored

`LANGUAGES` List of supported languages

#### Database

`USE_SQLITE` NOTE: in testing we've moved to Postgres, so this probably doesn't work any more


##### SQLite

`DB_SQLITE`
Expand Down Expand Up @@ -93,6 +98,71 @@ FLASK_APP=runserver.py flask run

Assets for image generation go in `sdv\assets\[subfolder]`. Assets used as-is go in `sdv\static\assets\[subfolder]`.

## Run With Docker compose

### Create the DB

Build all the containers but start only the postgres one for now. Then, run the
`createadmin.py` from the webapps container:

```bash
docker compose build
docker compose up postges
docker run \
-it \
-e PYTHONPATH=. \
-v "$(pwd)"/:/app \
--network=sdv-summary_postgres \
sdv-summary_webapp \
bas -c "python sdv/createadmin.py; python sdv/createdb.py"
docker-compose down
```

### Run the webapp + database

```bash
docker compose up
```

This command:
- Download necessary docker images for python, postgres and pgAdmin, if not present.
- Creates a database `postgres` with user:password `postgres:postgres` for administration.
- Creates a database `sdv_summary_development` and a `user:password`
`sdv_summary:sdv_summary` with all rights on the `sdv_summary_development`
database.
- TODO: Creates tables?
- TODO: Inits data?
- Runs PGAdmin on port 5050, and postgre on 5432

## PgAdmin
You can access it on http://localhost:5050 with:
- Username: [email protected]
- Password: admin

Then you should add a server. Call it "docker" for example. For the connection:
- host: postgres
- port: 5432
- maintenance database: postgres
- username: admin
- password: admin

CREATE USER sdv_summary WITH PASSWORD = 'sdv_summary';
CREATE DATABASE sdv_summary_development;
GRANT ALL PRIVILEGES ON DATABASE sdv_summary_development TO sdv_summary;


When you are done for the day, just:

```bash
docker compose down
```

And if you want to nuke the databases *LOSING ALL THE DATA*:

```bash
docker compose down --volumes
```

## Code Style

In order to keep the code style consistent, this project is formatted using [Black](https://github.com/psf/black).
Expand Down
71 changes: 71 additions & 0 deletions docker-compose.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,71 @@
# With help from https://github.com/khezen/compose-postgres/blob/master/docker-compose.yml
version: '3.5'

services:

postgres:
container_name: postgres
image: postgres
restart: always
environment:
POSTGRES_DB: ${POSTGRES_DB:-postgres}
POSTGRES_USER: ${POSTGRES_USER:-admin}
POSTGRES_PASSWORD: ${POSTGRES_PASSWORD:-admin}
POSTGRES_HOST_AUTH_METHOD: trust
# PGDATA: /data/postgres
volumes:
- db-data:/var/lib/postgresql/data
- ./init.sql:/docker-entrypoint-initdb.d/10-init.sql
ports:
- 5432:5432
networks:
- postgres

pgadmin:
container_name: pgadmin
image: dpage/pgadmin4
environment:
PGADMIN_DEFAULT_EMAIL: ${PGADMIN_DEFAULT_EMAIL:[email protected]}
PGADMIN_DEFAULT_PASSWORD: ${PGADMIN_DEFAULT_PASSWORD:-admin}
volumes:
- pgadmin:/root/.pgadmin
ports:
- "${PGADMIN_PORT:-5050}:80"
networks:
- postgres
restart: unless-stopped

adminer:
image: adminer
restart: always
ports:
- 8080:8080
networks:
- postgres

webapp:
container_name: webapp
restart: always
build: .
depends_on:
- "postgres"
ports:
- 5000:5000
volumes:
- .:/app
networks:
- postgres
environment:
PYTHONPATH: .:sdv
# FLASK_APP: ./runserver.py
FLASK_RUN_HOST: 0.0.0.0
FLASK_ENV: development


networks:
postgres:
driver: bridge

volumes:
db-data:
pgadmin:
8 changes: 8 additions & 0 deletions init.sql
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
-- Initialization script
-- Creates the database and tables

-- This is not needed as the suer and database are created from the docker
-- compose environment variables
CREATE USER sdv_summary WITH PASSWORD 'sdv_summary';
CREATE DATABASE sdv_summary_development;
GRANT ALL PRIVILEGES ON DATABASE sdv_summary_development TO sdv_summary;
3 changes: 1 addition & 2 deletions requirements.txt
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ blinker==1.4
certifi==2017.4.17
cffi==1.11.5
chardet==3.0.4
Click==7.0
Click>=7.1
colorama==0.3.9
cryptography==2.3.1
defusedxml==0.5.0
Expand Down Expand Up @@ -52,7 +52,6 @@ six==1.11.0
speaklater==1.3
SQLAlchemy==1.2.14
termcolor==1.1.0
typing==3.6.6
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What required typing? Why can we remove it? (open to believing we can, not sure why it was there in the first place though)

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Again I don't remember all the details sadly, but I think it was because by enforcing the typing version black would refuse to install. And typing is a dependency of the other libraries anyway.

urllib3==1.24.1
webassets==0.12.1
Werkzeug==0.14.1
Expand Down
3 changes: 2 additions & 1 deletion runserver.py
Original file line number Diff line number Diff line change
@@ -1,10 +1,11 @@
import os

os.chdir(os.path.join(os.path.dirname(__file__), "sdv"))
# os.chdir(os.path.join(os.path.dirname(__file__), "sdv"))
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I haven't got a local dev env set up so can't test this, but I feel like things won't work properly without this. It's possibly more of a patch for a legacy bug than needed for a new dev env though, so I'm not sure what the best approach is. Will need to test this.

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I should have added a comment there. I don't remember the details but I couldn't make either the container or the test to work with this, I had to fix this by adding sdv to the PYTHONPATH variable (see docker-compose.yml), which IMHO it's a better approach. With export PYTHONPATH=.:sdv you should not have problems, but that's a preference and if we want to keep this line I'll make the PR to work around this.


import sys

sys.path.insert(0, "./")
sys.path.insert(1, "./sdv/")

# for some reason on Python 3.4 on Linux Mint, using runserver.py crashes on first reload if os.chdir() is used
# so to avoid this (and break some of the site, but oh well...) remove os.chdir and add sdv to path
Expand Down
12 changes: 4 additions & 8 deletions sdv/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,7 @@

from sdv.utils.log import app_logger
from sdv.utils.helpers import random_id
from sdv.utils.postgres import get_db_connection_string

# from sdv.playerInfo import playerInfo
from sdv.playerinfo2 import GameInfo
Expand Down Expand Up @@ -116,14 +117,9 @@ def connect_db():

else:
logger.info("Application set to use Postgres")
app.database = (
"dbname="
+ app.config["DB_NAME"]
+ " user="
+ app.config["DB_USER"]
+ " password="
+ app.config["DB_PASSWORD"]
)

connstr = get_db_connection_string(app.config)
app.database = (connstr)
app.sqlesc = "%s"

return app
Expand Down
31 changes: 26 additions & 5 deletions sdv/config.py.sample
Original file line number Diff line number Diff line change
@@ -1,11 +1,32 @@
#!/usr/bin/env python

class DevelopmentConfig():
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I know this is DevelopmentConfig but I've left comments on it as though it were production as it's the only sample config we provide, so it's probably sensible to have some safeguards

DEBUG = True
USE_SQLITE = False
DB_NAME = 'sdv_summary_development'
DB_USER = ''
DB_PASSWORD = ''

# Mandatory settings
UPLOAD_FOLDER = 'uploads'
SECRET_KEY = 'changeme'
MAX_CONTENT_LENGTH = 16*1024*1024
PASSWORD_ATTEMPTS_LIMIT = None
PASSWORD_MIN_LENGTH = 6
IMGUR_CLIENTID = ''
IMGUR_SECRET = ''
IMGUR_DIRECT_UPLOAD = True
RECAPTCHA_ENABLED = False
RECAPTCHA_SITE_KEY = None
RECAPTCHA_SECRET_KEY = None
ANALYTICS_ID = ''
DEBUG = True
ASSET_PATH = 'sdv/assets'
LANGUAGES = ['en']

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This possibly also needs

IMAGE_FOLDER = './static/images'
IMAGE_MAX_PER_FOLDER = 30000 # We use this as some filesystems have a limit on the number of subfolders per folder (I think ext2?)
RENDER_FOLDER = './static/renders'

MAIL_SERVER = None
MAIL_PORT = 465
MAIL_USE_SS = True
MAIL_DEFAULT_SENDER = ('sender name', '[email protected]')
MAIL_USERNAME = None
MAIL_PASSWORD

LANGUAGES = ['en', 'es', 'pt_BR', 'zh_Hans_CN']


API_V1_UPLOAD_ZIP_TIME_PER_USER = 3600
API_V1_UPLOAD_ZIP_LIMIT_PER_USER = 10

API_V1_PLAN_TIME = 60  # time interval (seconds)
API_V1_PLAN_LIMIT = 10  # number of successful submissions in time 
API_V1_PLAN_MAX_RENDERS = 3
API_V1_PLAN_APPROVED_SOURCES = ['stardew.info']

and may need:

BASE_DIR = os.path.dirname(os.path.realpath(__file__))

though this was used for fixing some relative directories I think that might not apply in dockerized dev

There's also a

LEGACY_ROOT_FOLDER = './sdv'

Which might not be required

# Database
USE_SQLITE = False
DB_NAME = 'sdv_summary_development'
DB_USER = 'sdv_summary'
DB_PASSWORD = 'sdv_summary'
# NOTE: Leave commented to use unix socket
# Or specify Host if DB is not in the same machine
# DB_HOST = 'postgres'

config = {
'development': DevelopmentConfig
Expand Down
12 changes: 3 additions & 9 deletions sdv/createadmin.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
import os
import sys
from werkzeug.security import generate_password_hash
from sdv.utils.postgres import get_db_connection_string

app = Flask(__name__)
config_name = os.environ.get("SDV_APP_SETTINGS", "development")
Expand All @@ -27,15 +28,8 @@ def connect_db():
connection = sqlite3.connect(app.config["DB_SQLITE"])
else:
import psycopg2

connection = psycopg2.connect(
"dbname="
+ app.config["DB_NAME"]
+ " user="
+ app.config["DB_USER"]
+ " password="
+ app.config["DB_PASSWORD"]
)
connstr = get_db_connection_string(app.config)
connection = psycopg2.connect(connstr)
return connection


Expand Down
14 changes: 4 additions & 10 deletions sdv/createdb.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,9 +5,10 @@
import getpass
from werkzeug import check_password_hash
from config import config
from sdv.utils.postgres import get_db_connection_string

app = Flask(__name__)
config_name = os.environ.get("SDV_APP_SETTINGS", None)
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Possibly a bad idea from a security point of view? Defaulting to dev could introduce risk in a production env

config_name = os.environ.get("SDV_APP_SETTINGS", 'development')
app.config.from_object(config[config_name])

database_structure_dict = {
Expand Down Expand Up @@ -267,15 +268,8 @@ def connect_db():
connection = sqlite3.connect(app.config["DB_SQLITE"])
else:
import psycopg2

connection = psycopg2.connect(
"dbname="
+ app.config["DB_NAME"]
+ " user="
+ app.config["DB_USER"]
+ " password="
+ app.config["DB_PASSWORD"]
)
connstr = get_db_connection_string(app.config)
connection = psycopg2.connect(connstr)
return connection


Expand Down
Empty file added sdv/utils/__init__.py
Empty file.
15 changes: 15 additions & 0 deletions sdv/utils/postgres.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@

def get_db_connection_string(config):
"""Given app.config, returns the connection string for postgres"""
params = dict(
dbname=config["DB_NAME"],
user=config["DB_USER"],
)
# Host is optional.
# If not present, use unix sockets
if 'DB_PASSWORD' in config:
params['password'] = config["DB_PASSWORD"]
if 'DB_HOST' in config:
params['host'] = config["DB_HOST"]
connstr = ' '.join(f'{key}={value}' for key, value in params.items())
return connstr
Empty file added tests/utils/__init__.py
Empty file.
30 changes: 30 additions & 0 deletions tests/utils/test_postgres.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
import pytest
from sdv.utils.postgres import get_db_connection_string

class TestGetDBConnectionString:

def test_empty_raises_exception(self):
with pytest.raises(KeyError):
get_db_connection_string({})

def test_db_name_and_user(self):
assert get_db_connection_string(dict(
DB_NAME="mydb",
DB_USER="myusr",
)) == "dbname=mydb user=myusr"

def test_db_name_user_password(self):
assert get_db_connection_string(dict(
DB_NAME="mydb",
DB_USER="myusr",
DB_PASSWORD="mypass",
)) == "dbname=mydb user=myusr password=mypass"

def test_all_attributes(self):
assert get_db_connection_string(dict(
DB_HOST="thehost",
DB_NAME="mydb",
DB_USER="myusr",
DB_PASSWORD="mypass",
)) == "dbname=mydb user=myusr password=mypass host=thehost"