Skip to content

Commit 6ceb654

Browse files
committed
config: granular env-based solution for connection strings
Add logic to build connection urls from env vars if available needed for helm charts security best practices. includes: * Build db uri * Build redis url * Build mq url Partially closes: inveniosoftware/helm-invenio#112
1 parent e3bc51c commit 6ceb654

File tree

2 files changed

+183
-0
lines changed

2 files changed

+183
-0
lines changed

invenio_config/utils.py

+60
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22
#
33
# This file is part of Invenio.
44
# Copyright (C) 2015-2018 CERN.
5+
# Copyright (C) 2024 KTH Royal Institute of Technology.
56
#
67
# Invenio is free software; you can redistribute it and/or modify it
78
# under the terms of the MIT License; see LICENSE file for more details.
@@ -10,6 +11,8 @@
1011

1112
from __future__ import absolute_import, print_function
1213

14+
import os
15+
1316
from .default import InvenioConfigDefault
1417
from .entrypoint import InvenioConfigEntryPointModule
1518
from .env import InvenioConfigEnvironment
@@ -77,3 +80,60 @@ def create_conf_loader(*args, **kwargs): # pragma: no cover
7780
DeprecationWarning,
7881
)
7982
return create_config_loader(*args, **kwargs)
83+
84+
85+
def build_db_uri():
86+
"""Build the database URI."""
87+
default_db_uri = "postgresql+psycopg2://invenio-app-rdm:invenio-app-rdm@localhost/invenio-app-rdm"
88+
params = {
89+
k: os.environ.get(f"DB_{k.upper()}")
90+
for k in ["user", "password", "host", "port", "name"]
91+
}
92+
93+
if all(params.values()):
94+
uri = f"postgresql+psycopg2://{params['user']}:{params['password']}@{params['host']}:{params['port']}/{params['name']}"
95+
return uri
96+
97+
uri = os.environ.get("SQLALCHEMY_DATABASE_URI")
98+
if uri:
99+
return uri
100+
return default_db_uri
101+
102+
103+
def build_broker_url():
104+
"""Build the broker URL."""
105+
default_amqp_broker_url = "amqp://guest:guest@localhost:5672/"
106+
params = {
107+
k: os.environ.get(f"BROKER_{k.upper()}")
108+
for k in ["user", "password", "host", "port"]
109+
}
110+
111+
if all(params.values()):
112+
uri = f"amqp://{params['user']}:{params['password']}@{params['host']}:{params['port']}/"
113+
return uri
114+
115+
uri = os.environ.get("BROKER_URL")
116+
if uri:
117+
return uri
118+
119+
return default_amqp_broker_url
120+
121+
122+
def build_redis_url(db=None):
123+
"""Build the Redis broker URL."""
124+
redis_host = os.environ.get("REDIS_HOST")
125+
redis_port = os.environ.get("REDIS_PORT")
126+
redis_password = os.environ.get("REDIS_PASSWORD")
127+
db = db if db is not None else 0
128+
default_redis_broker_url = f"redis://localhost:6379/{db}"
129+
130+
if redis_host and redis_port:
131+
password = f":{redis_password}@" if redis_password else ""
132+
uri = f"redis://{password}{redis_host}:{redis_port}/{db}"
133+
return uri
134+
135+
uri = os.environ.get("BROKER_URL")
136+
if uri and uri.startswith(("redis://", "rediss://", "unix://")):
137+
return uri
138+
139+
return default_redis_broker_url

tests/test_invenio_config.py

+123
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22
#
33
# This file is part of Invenio.
44
# Copyright (C) 2015-2018 CERN.
5+
# Copyright (C) 2024 KTH Royal Institute of Technology.
56
#
67
# Invenio is free software; you can redistribute it and/or modify it
78
# under the terms of the MIT License; see LICENSE file for more details.
@@ -16,6 +17,7 @@
1617
import warnings
1718
from os.path import join
1819

20+
import pytest
1921
from flask import Flask
2022
from mock import patch
2123
from pkg_resources import EntryPoint
@@ -29,6 +31,7 @@
2931
create_config_loader,
3032
)
3133
from invenio_config.default import ALLOWED_HTML_ATTRS, ALLOWED_HTML_TAGS
34+
from invenio_config.utils import build_broker_url, build_db_uri, build_redis_url
3235

3336

3437
class ConfigEP(EntryPoint):
@@ -231,3 +234,123 @@ class Config(object):
231234
assert app.config["ENV"] == "env"
232235
finally:
233236
shutil.rmtree(tmppath)
237+
238+
239+
@pytest.mark.parametrize(
240+
"env_vars, expected_uri",
241+
[
242+
(
243+
{
244+
"DB_USER": "testuser",
245+
"DB_PASSWORD": "testpassword",
246+
"DB_HOST": "testhost",
247+
"DB_PORT": "5432",
248+
"DB_NAME": "testdb",
249+
},
250+
"postgresql+psycopg2://testuser:testpassword@testhost:5432/testdb",
251+
),
252+
(
253+
{"SQLALCHEMY_DATABASE_URI": "sqlite:///test.db"},
254+
"sqlite:///test.db",
255+
),
256+
(
257+
{},
258+
"postgresql+psycopg2://invenio-app-rdm:invenio-app-rdm@localhost/invenio-app-rdm",
259+
),
260+
],
261+
)
262+
def test_build_db_uri(monkeypatch, env_vars, expected_uri):
263+
"""Test building database URI."""
264+
for key in [
265+
"DB_USER",
266+
"DB_PASSWORD",
267+
"DB_HOST",
268+
"DB_PORT",
269+
"DB_NAME",
270+
"SQLALCHEMY_DATABASE_URI",
271+
]:
272+
monkeypatch.delenv(key, raising=False)
273+
for key, value in env_vars.items():
274+
monkeypatch.setenv(key, value)
275+
276+
assert build_db_uri() == expected_uri
277+
278+
279+
@pytest.mark.parametrize(
280+
"env_vars, expected_url",
281+
[
282+
(
283+
{
284+
"BROKER_USER": "testuser",
285+
"BROKER_PASSWORD": "testpassword",
286+
"BROKER_HOST": "testhost",
287+
"BROKER_PORT": "5672",
288+
},
289+
"amqp://testuser:testpassword@testhost:5672/",
290+
),
291+
(
292+
{"BROKER_URL": "amqp://guest:guest@localhost:5672/"},
293+
"amqp://guest:guest@localhost:5672/",
294+
),
295+
(
296+
{},
297+
"amqp://guest:guest@localhost:5672/",
298+
),
299+
],
300+
)
301+
def test_build_broker_url(monkeypatch, env_vars, expected_url):
302+
"""Test building broker URL."""
303+
for key in [
304+
"BROKER_USER",
305+
"BROKER_PASSWORD",
306+
"BROKER_HOST",
307+
"BROKER_PORT",
308+
"BROKER_URL",
309+
]:
310+
monkeypatch.delenv(key, raising=False)
311+
for key, value in env_vars.items():
312+
monkeypatch.setenv(key, value)
313+
314+
assert build_broker_url() == expected_url
315+
316+
317+
@pytest.mark.parametrize(
318+
"env_vars, db, expected_url",
319+
[
320+
(
321+
{
322+
"REDIS_HOST": "testhost",
323+
"REDIS_PORT": "6379",
324+
"REDIS_PASSWORD": "testpassword",
325+
},
326+
2,
327+
"redis://:testpassword@testhost:6379/2",
328+
),
329+
(
330+
{
331+
"REDIS_HOST": "testhost",
332+
"REDIS_PORT": "6379",
333+
},
334+
1,
335+
"redis://testhost:6379/1",
336+
),
337+
(
338+
{"BROKER_URL": "redis://localhost:6379/0"},
339+
None,
340+
"redis://localhost:6379/0",
341+
),
342+
(
343+
{},
344+
4,
345+
"redis://localhost:6379/4",
346+
),
347+
],
348+
)
349+
def test_build_redis_url(monkeypatch, env_vars, db, expected_url):
350+
"""Test building Redis URL."""
351+
for key in ["REDIS_HOST", "REDIS_PORT", "REDIS_PASSWORD", "BROKER_URL"]:
352+
monkeypatch.delenv(key, raising=False)
353+
for key, value in env_vars.items():
354+
monkeypatch.setenv(key, value)
355+
356+
assert build_redis_url(db=db) == expected_url

0 commit comments

Comments
 (0)