Skip to content

Commit 85be1aa

Browse files
committed
config: granular env-based solution for connection strings
* build db uri * build redis url * build mq url partially closes: inveniosoftware/helm-invenio#112
1 parent f96a443 commit 85be1aa

File tree

3 files changed

+220
-8
lines changed

3 files changed

+220
-8
lines changed

Diff for: invenio_app_rdm/config.py

+10-8
Original file line numberDiff line numberDiff line change
@@ -167,6 +167,8 @@
167167
)
168168
from werkzeug.local import LocalProxy
169169

170+
from invenio_app_rdm.utils.utils import build_broker_url, build_db_uri, build_redis_url
171+
170172
from .theme.views import notification_settings
171173
from .users.schemas import NotificationsUserSchema, UserPreferencesNotificationsSchema
172174

@@ -214,7 +216,8 @@ def _(x):
214216
# =============
215217
# https://flask-limiter.readthedocs.io/en/stable/#configuration
216218

217-
RATELIMIT_STORAGE_URI = "redis://localhost:6379/3"
219+
RATELIMIT_STORAGE_URI = build_redis_url(db=3)
220+
218221
"""Storage for ratelimiter."""
219222

220223
# Increase defaults
@@ -380,7 +383,7 @@ def files_rest_permission_factory(obj, action):
380383
# See https://invenio-accounts.readthedocs.io/en/latest/configuration.html
381384
# See https://flask-security.readthedocs.io/en/3.0.0/configuration.html
382385

383-
ACCOUNTS_SESSION_REDIS_URL = "redis://localhost:6379/1"
386+
ACCOUNTS_SESSION_REDIS_URL = build_redis_url(db=1)
384387
"""Redis session storage URL."""
385388

386389
ACCOUNTS_USERINFO_HEADERS = True
@@ -413,7 +416,7 @@ def files_rest_permission_factory(obj, action):
413416
# See docs.celeryproject.org/en/latest/userguide/configuration.html
414417
# See https://flask-celeryext.readthedocs.io/en/latest/
415418

416-
BROKER_URL = "amqp://guest:guest@localhost:5672/"
419+
BROKER_URL = build_broker_url()
417420
"""URL of message broker for Celery 3 (default is RabbitMQ)."""
418421

419422
CELERY_BEAT_SCHEDULE = {
@@ -487,16 +490,15 @@ def files_rest_permission_factory(obj, action):
487490
CELERY_BROKER_URL = BROKER_URL
488491
"""Same as BROKER_URL to support Celery 4."""
489492

490-
CELERY_RESULT_BACKEND = "redis://localhost:6379/2"
493+
CELERY_RESULT_BACKEND = build_redis_url(db=2)
491494
"""URL of backend for result storage (default is Redis)."""
492495

493496
# Flask-SQLAlchemy
494497
# ================
495498
# See https://flask-sqlalchemy.palletsprojects.com/en/2.x/config/
496499

497-
SQLALCHEMY_DATABASE_URI = (
498-
"postgresql+psycopg2://invenio-app-rdm:invenio-app-rdm@localhost/invenio-app-rdm"
499-
)
500+
SQLALCHEMY_DATABASE_URI = build_db_uri()
501+
500502
"""Database URI including user and password.
501503
502504
Default value is provided to make module testing easier.
@@ -688,7 +690,7 @@ def files_rest_permission_factory(obj, action):
688690
# =============
689691
# See https://flask-caching.readthedocs.io/en/latest/index.html#configuring-flask-caching # noqa
690692

691-
CACHE_REDIS_URL = "redis://localhost:6379/0"
693+
CACHE_REDIS_URL = build_redis_url()
692694
"""URL to connect to Redis server."""
693695

694696
CACHE_TYPE = "flask_caching.backends.redis"

Diff for: invenio_app_rdm/utils/utils.py

+86
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,86 @@
1+
# -*- coding: utf-8 -*-
2+
#
3+
# Copyright (C) 2024 CERN.
4+
# Copyright (C) 2024 KTH Royal Institute of Technology.
5+
#
6+
# Invenio App RDM is free software; you can redistribute it and/or modify it
7+
# under the terms of the MIT License; see LICENSE file for more details.
8+
9+
"""Utilities for building connection strings."""
10+
11+
import os
12+
13+
from click import secho
14+
15+
16+
def build_db_uri():
17+
"""Build the database URI."""
18+
DEFAULT_URI = "postgresql+psycopg2://invenio-app-rdm:invenio-app-rdm@localhost/invenio-app-rdm"
19+
params = {
20+
k: os.environ.get(f"DB_{k.upper()}")
21+
for k in ["user", "password", "host", "port", "name"]
22+
}
23+
24+
if all(params.values()):
25+
uri = f"postgresql+psycopg2://{params['user']}:{params['password']}@{params['host']}:{params['port']}/{params['name']}"
26+
secho(
27+
f"Constructed database URI: '{params['user']}:***@{params['host']}:{params['port']}/{params['name']}'",
28+
fg="blue",
29+
)
30+
return uri
31+
32+
uri = os.environ.get("SQLALCHEMY_DATABASE_URI")
33+
if uri:
34+
secho(f"Using SQLALCHEMY_DATABASE_URI: '{uri}'", fg="blue")
35+
return uri
36+
37+
secho(f"Falling back to the default URI: '{DEFAULT_URI}'", fg="blue")
38+
return DEFAULT_URI
39+
40+
41+
def build_broker_url():
42+
"""Build the broker URL."""
43+
DEFAULT_BROKER_URL = "amqp://guest:guest@localhost:5672/"
44+
params = {
45+
k: os.environ.get(f"BROKER_{k.upper()}")
46+
for k in ["user", "password", "host", "port"]
47+
}
48+
49+
if all(params.values()):
50+
uri = f"amqp://{params['user']}:{params['password']}@{params['host']}:{params['port']}/"
51+
secho(
52+
f"Constructed AMQP URL: '{params['user']}:***@{params['host']}:{params['port']}/'",
53+
fg="blue",
54+
)
55+
return uri
56+
57+
uri = os.environ.get("BROKER_URL")
58+
if uri:
59+
secho(f"AMQP URI: '{uri}'", fg="blue")
60+
return uri
61+
62+
secho(f"Falling back to the default URI: '{DEFAULT_BROKER_URL}'", fg="blue")
63+
return DEFAULT_BROKER_URL
64+
65+
66+
def build_redis_url(db=None):
67+
"""Build the Redis broker URL."""
68+
redis_host = os.environ.get("REDIS_HOST")
69+
redis_port = os.environ.get("REDIS_PORT")
70+
redis_password = os.environ.get("REDIS_PASSWORD")
71+
db = db if db is not None else 0
72+
DEFAULT_BROKER_URL = f"redis://localhost:6379/{db}"
73+
74+
if redis_host and redis_port:
75+
password = f":{redis_password}@" if redis_password else ""
76+
uri = f"redis://{password}{redis_host}:{redis_port}/{db}"
77+
secho(f"Constructed Redis URL: '{uri}'", fg="blue")
78+
return uri
79+
80+
uri = os.environ.get("BROKER_URL")
81+
if uri and uri.startswith(("redis://", "rediss://", "unix://")):
82+
secho(f"Using Redis BROKER_URL: '{uri}'", fg="blue")
83+
return uri
84+
85+
secho(f"Falling back to the default Redis URL: '{DEFAULT_BROKER_URL}'", fg="blue")
86+
return DEFAULT_BROKER_URL

Diff for: tests/test_utils.py

+124
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
# -*- coding: utf-8 -*-
22
#
33
# Copyright (C) 2021 TU Wien.
4+
# Copyright (C) 2024 KTH Royal Institute of Technology.
45
#
56
# Invenio App RDM is free software; you can redistribute it and/or modify it
67
# under the terms of the MIT License; see LICENSE file for more details.
@@ -9,7 +10,10 @@
910

1011
from datetime import datetime
1112

13+
import pytest
14+
1215
from invenio_app_rdm.records_ui.utils import set_default_value
16+
from invenio_app_rdm.utils.utils import build_broker_url, build_db_uri, build_redis_url
1317

1418

1519
def test_set_default_value__value():
@@ -65,3 +69,123 @@ def test_set_default_value__explicit_and_automatic_prefix():
6569
dict1["metadata"]["publication_date"] == dict2["metadata"]["publication_date"]
6670
)
6771
assert dict1["metadata"]["publication_date"] == value
72+
73+
74+
@pytest.mark.parametrize(
75+
"env_vars, expected_uri",
76+
[
77+
(
78+
{
79+
"DB_USER": "testuser",
80+
"DB_PASSWORD": "testpassword",
81+
"DB_HOST": "testhost",
82+
"DB_PORT": "5432",
83+
"DB_NAME": "testdb",
84+
},
85+
"postgresql+psycopg2://testuser:testpassword@testhost:5432/testdb",
86+
),
87+
(
88+
{"SQLALCHEMY_DATABASE_URI": "sqlite:///test.db"},
89+
"sqlite:///test.db",
90+
),
91+
(
92+
{},
93+
"postgresql+psycopg2://invenio-app-rdm:invenio-app-rdm@localhost/invenio-app-rdm",
94+
),
95+
],
96+
)
97+
def test_build_db_uri(monkeypatch, env_vars, expected_uri):
98+
"""Test building database URI."""
99+
for key in [
100+
"DB_USER",
101+
"DB_PASSWORD",
102+
"DB_HOST",
103+
"DB_PORT",
104+
"DB_NAME",
105+
"SQLALCHEMY_DATABASE_URI",
106+
]:
107+
monkeypatch.delenv(key, raising=False)
108+
for key, value in env_vars.items():
109+
monkeypatch.setenv(key, value)
110+
111+
assert build_db_uri() == expected_uri
112+
113+
114+
@pytest.mark.parametrize(
115+
"env_vars, expected_url",
116+
[
117+
(
118+
{
119+
"BROKER_USER": "testuser",
120+
"BROKER_PASSWORD": "testpassword",
121+
"BROKER_HOST": "testhost",
122+
"BROKER_PORT": "5672",
123+
},
124+
"amqp://testuser:testpassword@testhost:5672/",
125+
),
126+
(
127+
{"BROKER_URL": "amqp://guest:guest@localhost:5672/"},
128+
"amqp://guest:guest@localhost:5672/",
129+
),
130+
(
131+
{},
132+
"amqp://guest:guest@localhost:5672/",
133+
),
134+
],
135+
)
136+
def test_build_broker_url(monkeypatch, env_vars, expected_url):
137+
"""Test building broker URL."""
138+
for key in [
139+
"BROKER_USER",
140+
"BROKER_PASSWORD",
141+
"BROKER_HOST",
142+
"BROKER_PORT",
143+
"BROKER_URL",
144+
]:
145+
monkeypatch.delenv(key, raising=False)
146+
for key, value in env_vars.items():
147+
monkeypatch.setenv(key, value)
148+
149+
assert build_broker_url() == expected_url
150+
151+
152+
@pytest.mark.parametrize(
153+
"env_vars, db, expected_url",
154+
[
155+
(
156+
{
157+
"REDIS_HOST": "testhost",
158+
"REDIS_PORT": "6379",
159+
"REDIS_PASSWORD": "testpassword",
160+
},
161+
2,
162+
"redis://:testpassword@testhost:6379/2",
163+
),
164+
(
165+
{
166+
"REDIS_HOST": "testhost",
167+
"REDIS_PORT": "6379",
168+
},
169+
1,
170+
"redis://testhost:6379/1",
171+
),
172+
(
173+
{"BROKER_URL": "redis://localhost:6379/0"},
174+
None,
175+
"redis://localhost:6379/0",
176+
),
177+
(
178+
{},
179+
4,
180+
"redis://localhost:6379/4",
181+
),
182+
],
183+
)
184+
def test_build_redis_url(monkeypatch, env_vars, db, expected_url):
185+
"""Test building Redis URL."""
186+
for key in ["REDIS_HOST", "REDIS_PORT", "REDIS_PASSWORD", "BROKER_URL"]:
187+
monkeypatch.delenv(key, raising=False)
188+
for key, value in env_vars.items():
189+
monkeypatch.setenv(key, value)
190+
191+
assert build_redis_url(db=db) == expected_url

0 commit comments

Comments
 (0)