Skip to content

Commit a4ba397

Browse files
authored
Allow configuring from environment, add docker entrypoint (#229)
* Allow setting most config variables through environment * Add docker entrypoint * Update config docs * Update deployment docs * Change unit test after setting TLS to true by default * Get media base dir from env * Bump Grampsjs version in docs
1 parent 4e2a64e commit a4ba397

File tree

8 files changed

+109
-71
lines changed

8 files changed

+109
-71
lines changed

Dockerfile

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,10 @@ RUN mkdir /app/static && touch /app/static/index.html
2020
RUN mkdir /app/db && mkdir /app/media && mkdir /app/indexdir && mkdir /app/users
2121
RUN mkdir /app/thumbnail_cache
2222
RUN mkdir /app/tmp
23+
ENV USER_DB_URI=sqlite:////app/users/users.sqlite
24+
ENV MEDIA_BASE_DIR=/app/media
25+
ENV SEARCH_INDEX_DIR=/app/indexdir
26+
ENV STATIC_PATH=/app/static
2327

2428
# install gunicorn
2529
RUN python3 -m pip install --no-cache-dir --extra-index-url https://www.piwheels.org/simple \
@@ -32,4 +36,6 @@ RUN python3 -m pip install --no-cache-dir --extra-index-url https://www.piwheels
3236

3337
EXPOSE 5000
3438

39+
COPY docker-entrypoint.sh /
40+
ENTRYPOINT ["/docker-entrypoint.sh"]
3541
CMD gunicorn -w 8 -b 0.0.0.0:5000 gramps_webapi.wsgi:app --timeout 120 --limit-request-line 8190

docker-entrypoint.sh

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
#!/bin/sh
2+
set -eu
3+
4+
# Use Gramps.js frontend
5+
if [ -n "$GRAMPSJS_VERSION" ]
6+
then
7+
# Download if necessary
8+
if ! [ -d "/app/static/grampsjs-${GRAMPSJS_VERSION}" ]
9+
then
10+
wget "https://github.com/DavidMStraub/Gramps.js/releases/download/${GRAMPSJS_VERSION}/grampsjs-${GRAMPSJS_VERSION}.tar.gz"
11+
tar -xzf "grampsjs-${GRAMPSJS_VERSION}.tar.gz"
12+
mv "grampsjs-${GRAMPSJS_VERSION}" static/
13+
fi
14+
export STATIC_PATH="/app/static/grampsjs-${GRAMPSJS_VERSION}"
15+
fi
16+
17+
# Recreate search index if not exists
18+
if [ -z "$(ls -A /app/indexdir)" ]
19+
then
20+
python3 -m gramps_webapi --config /app/config/config.cfg search index-full
21+
fi
22+
23+
exec "$@"

docs/Configuration.md

Lines changed: 23 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -1,16 +1,16 @@
11
# Configuration
22

3-
A configuration file is necessary to run the Gramps Web API. Its path can either be provided on the command line when running the module as a script (`python3 -m gramps_webapi --config path/to/config.cfg ...`), or as an environment variable, `GRAMPS_API_CONFIG=path/to/config.cfg`.
3+
Some configuration is necessary to run the Gramps Web API. When using a configuration file, its path can either be provided on the command line when running the module as a script (`python3 -m gramps_webapi --config path/to/config.cfg ...`), or as an environment variable, `GRAMPS_API_CONFIG=path/to/config.cfg`. Some of the configuration options can also be set without a configuration file, using environment variables (e.g. `TREE='My Tree' python3 -m gramps_webapi ...` ).
44

5-
The following settings can be specified in the configuration file.
5+
The following configuration options exist. The last column indicates whether the option can be set from an environment variable.
66

77

88
## Required settings
99

10-
Key | Description
11-
----|------------
12-
`TREE` | The name of the family tree database to use. Show available trees with `gramps -l`
13-
`SECRET_KEY` | The secret key for flask. This must be set for use in production. The secret must not be shared publicly. Changing it will invalidate all access tokens.`
10+
Key | Description | Set from environment
11+
----|-------------|---------------------
12+
`TREE` | The name of the family tree database to use. Show available trees with `gramps -l` | yes
13+
`SECRET_KEY` | The secret key for flask. This must be set for use in production. The secret must not be shared publicly. Changing it will invalidate all access tokens | yes
1414

1515
!!! info
1616
You can generate a secure secret key e.g. with the command
@@ -21,27 +21,27 @@ Key | Description
2121

2222
## Optional settings
2323

24-
Key | Description
25-
----|------------
26-
`MEDIA_BASE_DIR` | Path to use as base directory for media files, overriding the media base directory set in Gramps
27-
`SEARCH_INDEX_DIR` | Path for the full-text search index. Defaults to `indexdir` relative to the path where the script is run
28-
`STATIC_PATH` | Path to serve static files from (e.g. a static web frontend)
29-
`BASE_URL` | Base URL where the API can be reached (e.g. `https://mygramps.mydomain.com/`). This is necessary e.g. to build correct passwort reset links
30-
`CORS_ORIGINS` | Origins where CORS requests are allowed from. By default, all are disallowed. Use `"*"` to allow requests from any domain.
31-
`EMAIL_HOST` | SMTP server host (e.g. for sending password reset e-mails)
32-
`EMAIL_PORT` | SMTP server port
33-
`EMAIL_HOST_USER` | SMTP server username
34-
`EMAIL_HOST_PASSWORD` | SMTP server password
35-
`EMAIL_USE_TLS` | Boolean, whether to use TLS for sending e-mails. Defaults to false
36-
`DEFAULT_FROM_EMAIL` | "From" address for automated e-mails
37-
`THUMBNAIL_CACHE_CONFIG` | Dictionary with settings for the thumbnail cache. See [Flask-Caching](https://flask-caching.readthedocs.io/en/latest/) for possible settings.
24+
Key | Description | Set from environment
25+
----|-------------|---------------------
26+
`MEDIA_BASE_DIR` | Path to use as base directory for media files, overriding the media base directory set in Gramps | yes
27+
`SEARCH_INDEX_DIR` | Path for the full-text search index. Defaults to `indexdir` relative to the path where the script is run | yes
28+
`STATIC_PATH` | Path to serve static files from (e.g. a static web frontend) | yes
29+
`BASE_URL` | Base URL where the API can be reached (e.g. `https://mygramps.mydomain.com/`). This is necessary e.g. to build correct passwort reset links | yes
30+
`CORS_ORIGINS` | Origins where CORS requests are allowed from. By default, all are disallowed. Use `"*"` to allow requests from any domain. | no
31+
`EMAIL_HOST` | SMTP server host (e.g. for sending password reset e-mails) | yes
32+
`EMAIL_PORT` | SMTP server port. defaults to 465 | yes
33+
`EMAIL_HOST_USER` | SMTP server username | yes
34+
`EMAIL_HOST_PASSWORD` | SMTP server password | yes
35+
`EMAIL_USE_TLS` | Boolean, whether to use TLS for sending e-mails. Defaults to true | no
36+
`DEFAULT_FROM_EMAIL` | "From" address for automated e-mails | yes
37+
`THUMBNAIL_CACHE_CONFIG` | Dictionary with settings for the thumbnail cache. See [Flask-Caching](https://flask-caching.readthedocs.io/en/latest/) for possible settings. | no
3838

3939

4040
## Settings only during development
4141

42-
Key | Description
43-
----|------------
44-
`DISABLE_AUTH` | If `True`, disable the authentication system. **Warning: never** use this in a production environment, as it will allow read and write access from the public!
42+
Key | Description | Set from environment
43+
----|-------------|---------------------
44+
`DISABLE_AUTH` | If `True`, disable the authentication system. **Warning: never** use this in a production environment, as it will allow read and write access from the public! | no
4545

4646
## Example configuration file
4747

docs/Deployment.md

Lines changed: 19 additions & 32 deletions
Original file line numberDiff line numberDiff line change
@@ -23,24 +23,7 @@ Upload your media files to the server. Make sure your Gramps database uses relat
2323
Use the "Convert media paths from absolute to relative" option in the "Media Manager" tool in Gramps' "Tools" menu if necessary.
2424

2525

26-
27-
## Step 3: Create a configuration file
28-
29-
Create a configuration and upload it to your server (we will assume the path is `~/config.cfg`). It should contain at least the following lines:
30-
31-
```python
32-
TREE="..." # set the name of your family tree
33-
SECRET_KEY="..." # set your secret key
34-
# do not change the following lines as they refer to paths within the container
35-
USER_DB_URI="sqlite:////app/users/users.sqlite"
36-
MEDIA_BASE_DIR="/app/media"
37-
SEARCH_INDEX_DIR="/app/indexdir"
38-
STATIC_PATH="/app/static"
39-
```
40-
41-
For other configuration options, see [Configuration](Configuration.md)
42-
43-
## Step 4: Docker configuration
26+
## Step 3: Docker configuration
4427

4528
Create a new file on the server named `docker-compose.yml` and insert the following contents:
4629

@@ -53,11 +36,14 @@ services:
5336
restart: always
5437
ports:
5538
- "80:5000"
39+
environment:
40+
TREE: "..." # set the name of your family tree
41+
SECRET_KEY: "..." # set your secret key
42+
BASE_URL: "http://localhost:5554"
5643
volumes:
5744
- gramps_users:/app/users
5845
- gramps_index:/app/indexdir
5946
- gramps_thumb_cache:/app/thumbnail_cache
60-
- ~/config.cfg:/app/config/config.cfg
6147
- ~/gramps_db:/root/.gramps/grampsdb
6248
- ~/gramps_media:/app/media
6349

@@ -67,14 +53,14 @@ volumes:
6753
gramps_thumb_cache:
6854
```
6955
70-
This will generate three named volumes to make sure that the user database, search index, and thumbnail cache will persist, and will mount the configuration file, Gramps database directory, and the directory holding your media files into the container (the paths on the left-hand side of the colon `:` refer to your host machine, the ones on the right hand side to the container and should not be changed).
56+
This will generate three named volumes to make sure that the user database, search index, and thumbnail cache will persist, and will mount the Gramps database directory and the directory holding your media files into the container (the paths on the left-hand side of the colon `:` refer to your host machine, the ones on the right hand side to the container and should not be changed). For more configuration options, see [Configuration](Configuration.md).
7157

7258
!!! warning
7359
The above will make the API available on port 80 of the host machine **without SSL/TLS protection**. You can use this for local testing, but do not expose this directly to the internet, it is completely insecure!
7460

7561

7662

77-
## Step 5: Secure access with SSL/TLS
63+
## Step 4: Secure access with SSL/TLS
7864

7965
The web API **must** be served to the public internet over HTTPS. There are several options, e.g.
8066

@@ -156,7 +142,7 @@ networks:
156142

157143
Please see the [acme-companion](https://github.com/nginx-proxy/acme-companion) docs for how to set up your domain.
158144

159-
## Step 6: Import database
145+
## Step 5: Import database
160146

161147
If you have copied the Gramps database directory directly in Step 1, you can skip this step.
162148

@@ -172,7 +158,7 @@ docker-compose run gramps_webapi \
172158
where `My family tree` is the name of the new family tree (must match the setting in the config file) and `my_tree.gramps` the file name of the Gramps XML export.
173159

174160

175-
## Step 7: Add users
161+
## Step 6: Add users
176162

177163
Initialize the user system by creating an administrator account with the command
178164

@@ -188,19 +174,20 @@ docker-compose run gramps_webapi \
188174
See [User system](Users.md) for more details and how to add additional users.
189175

190176

191-
## Step 8: Build the search index
177+
## Step 7: Start the server
192178

193-
Build the initial search index by running
179+
Run
194180

195-
```bash
196-
docker-compose run gramps_webapi \
197-
python3 -m gramps_webapi search index-full
181+
```
182+
docker-compose up -d
198183
```
199184
200-
## Step 9: Start the server
185+
On first run, it will build the full-text search index of the API.
201186
202-
Run
187+
## Optional step: use Gramps.js web frontend
203188
189+
If you want to add the [Gramps.js](https://github.com/DavidMStraub/Gramps.js) web frontend to your installation, simply add
190+
```yaml
191+
- GRAMPSJS_VERSION=v0.1.1
204192
```
205-
docker-compose up -d
206-
```
193+
(or any other released version) to the `environment` block in the `gramps_webapi` service section of `docker-compose.yml`.

gramps_webapi/__main__.py

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -34,11 +34,12 @@
3434

3535

3636
@click.group("cli")
37-
@click.option("--config", help="Set the path to the config file", required=True)
37+
@click.option("--config", help="Set the path to the config file")
3838
@click.pass_context
3939
def cli(ctx, config):
4040
"""Gramps web API command line interface."""
41-
os.environ[ENV_CONFIG_FILE] = os.path.abspath(config)
41+
if config:
42+
os.environ[ENV_CONFIG_FILE] = os.path.abspath(config)
4243
ctx.obj = create_app()
4344

4445

gramps_webapi/app.py

Lines changed: 21 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -51,7 +51,23 @@ def create_app(db_manager=None):
5151
app.config.from_object(DefaultConfig)
5252

5353
# overwrite with user config file
54-
app.config.from_envvar(ENV_CONFIG_FILE)
54+
if os.getenv(ENV_CONFIG_FILE):
55+
app.config.from_envvar(ENV_CONFIG_FILE)
56+
57+
# if tree is missing, try to get it from the env or fail
58+
app.config["TREE"] = app.config.get("TREE") or os.getenv("TREE")
59+
if not app.config.get("TREE"):
60+
raise ValueError("TREE must be specified")
61+
62+
# if secret key is missing, try to get it from the env or fail
63+
app.config["SECRET_KEY"] = app.config["SECRET_KEY"] or os.getenv("SECRET_KEY")
64+
if not app.config.get("SECRET_KEY"):
65+
raise ValueError("SECRET_KEY must be specified")
66+
67+
# try setting media basedir from environment
68+
app.config["MEDIA_BASE_DIR"] = app.config.get("MEDIA_BASE_DIR") or os.getenv(
69+
"MEDIA_BASE_DIR"
70+
)
5571

5672
# instantiate DB manager
5773
if db_manager is None:
@@ -69,6 +85,10 @@ def create_app(db_manager=None):
6985
JWTManager(app)
7086

7187
# instantiate and store auth provider
88+
# if DB URI is missing, try to get it from the env or fail
89+
app.config["USER_DB_URI"] = app.config.get("USER_DB_URI") or os.getenv(
90+
"USER_DB_URI"
91+
)
7292
if not app.config.get("USER_DB_URI"):
7393
raise ValueError("USER_DB_URI must be specified")
7494
app.config["AUTH_PROVIDER"] = SQLAuth(db_uri=app.config["USER_DB_URI"])

gramps_webapi/config.py

Lines changed: 10 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -20,22 +20,23 @@
2020
"""Default configuration settings."""
2121

2222
import datetime
23+
import os
2324

2425

2526
class DefaultConfig(object):
2627
"""Default configuration object."""
2728

2829
PROPAGATE_EXCEPTIONS = True
29-
SEARCH_INDEX_DIR = "indexdir"
30-
EMAIL_HOST = "localhost"
31-
EMAIL_PORT = 0
32-
EMAIL_HOST_USER = ""
33-
EMAIL_HOST_PASSWORD = ""
34-
EMAIL_USE_TLS = False
35-
DEFAULT_FROM_EMAIL = ""
36-
BASE_URL = "http://localhost/"
30+
SEARCH_INDEX_DIR = os.getenv("SEARCH_INDEX_DIR", "indexdir")
31+
EMAIL_HOST = os.getenv("EMAIL_HOST", "localhost")
32+
EMAIL_PORT = int(os.getenv("EMAIL_PORT", "465"))
33+
EMAIL_HOST_USER = os.getenv("EMAIL_HOST_USER", "")
34+
EMAIL_HOST_PASSWORD = os.getenv("EMAIL_HOST_PASSWORD", "")
35+
EMAIL_USE_TLS = True
36+
DEFAULT_FROM_EMAIL = os.getenv("DEFAULT_FROM_EMAIL", "")
37+
BASE_URL = os.getenv("BASE_URL", "http://localhost/")
3738
CORS_EXPOSE_HEADERS = ["X-Total-Count"]
38-
STATIC_PATH = "static"
39+
STATIC_PATH = os.getenv("STATIC_PATH", "static")
3940
THUMBNAIL_CACHE_CONFIG = {
4041
"CACHE_TYPE": "filesystem",
4142
"CACHE_DIR": "thumbnail_cache",

tests/test_endpoints/test_user.py

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -172,12 +172,12 @@ def test_reset_password_trigger_invalid_user(self):
172172
assert rv.status_code == 404
173173

174174
def test_reset_password_trigger_status(self):
175-
with patch("smtplib.SMTP") as mock_smtp:
175+
with patch("smtplib.SMTP_SSL") as mock_smtp:
176176
rv = self.client.post(BASE_URL + "/users/user/password/reset/trigger/")
177177
assert rv.status_code == 201
178178

179179
def test_reset_password(self):
180-
with patch("smtplib.SMTP") as mock_smtp:
180+
with patch("smtplib.SMTP_SSL") as mock_smtp:
181181
rv = self.client.post(BASE_URL + "/users/user/password/reset/trigger/")
182182
context = mock_smtp.return_value
183183
context.send_message.assert_called()
@@ -444,7 +444,7 @@ def test_add_user(self):
444444
assert rv.status_code == 200
445445

446446
def test_register_user(self):
447-
with patch("smtplib.SMTP") as mock_smtp:
447+
with patch("smtplib.SMTP_SSL") as mock_smtp:
448448
# role is not allowed
449449
rv = self.client.post(
450450
BASE_URL + "/users/new_user_2/register/",
@@ -509,7 +509,7 @@ def test_register_user(self):
509509
assert rv.status_code == 403
510510

511511
def test_confirm_email(self):
512-
with patch("smtplib.SMTP") as mock_smtp:
512+
with patch("smtplib.SMTP_SSL") as mock_smtp:
513513
rv = self.client.post(
514514
BASE_URL + "/users/new_user_3/register/",
515515
json={

0 commit comments

Comments
 (0)