Skip to content

Commit 8f4b0e6

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 8f4b0e6

File tree

2 files changed

+306
-0
lines changed

2 files changed

+306
-0
lines changed

invenio_config/utils.py

+101
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,10 @@
1011

1112
from __future__ import absolute_import, print_function
1213

14+
import os
15+
16+
from click import secho
17+
1318
from .default import InvenioConfigDefault
1419
from .entrypoint import InvenioConfigEntryPointModule
1520
from .env import InvenioConfigEnvironment
@@ -77,3 +82,99 @@ def create_conf_loader(*args, **kwargs): # pragma: no cover
7782
DeprecationWarning,
7883
)
7984
return create_config_loader(*args, **kwargs)
85+
86+
87+
def _get_env_var(prefix, keys):
88+
"""Retrieve environment variables with a given prefix."""
89+
return {k: os.environ.get(f"{prefix}_{k.upper()}") for k in keys}
90+
91+
92+
def build_db_uri():
93+
"""
94+
Build database URI from environment variables or use default.
95+
96+
Priority order:
97+
1. INVENIO_SQLALCHEMY_DATABASE_URI
98+
2. SQLALCHEMY_DATABASE_URI
99+
3. INVENIO_DB_* specific environment variables
100+
4. Default URI
101+
"""
102+
default_uri = "postgresql+psycopg2://invenio-app-rdm:invenio-app-rdm@localhost/invenio-app-rdm"
103+
104+
uri = os.environ.get("INVENIO_SQLALCHEMY_DATABASE_URI") or os.environ.get(
105+
"SQLALCHEMY_DATABASE_URI"
106+
)
107+
if uri:
108+
secho(f"DB URI: {uri}", fg="blue")
109+
return uri
110+
111+
db_params = _get_env_var(
112+
"INVENIO_DB", ["user", "password", "host", "port", "name", "protocol"]
113+
)
114+
if all(db_params.values()):
115+
uri = f"{db_params['protocol']}://{db_params['user']}:{db_params['password']}@{db_params['host']}:{db_params['port']}/{db_params['name']}"
116+
secho(f"DB URI: {uri}", fg="blue")
117+
return uri
118+
119+
return default_uri
120+
121+
122+
def build_broker_url():
123+
"""
124+
Build broker URL from environment variables or use default.
125+
126+
Priority order:
127+
1. INVENIO_BROKER_URL
128+
2. BROKER_URL
129+
3. INVENIO_BROKER_* specific environment variables
130+
4. Default URL
131+
"""
132+
default_url = "amqp://guest:guest@localhost:5672/"
133+
134+
uri = os.environ.get("INVENIO_BROKER_URL") or os.environ.get("BROKER_URL")
135+
if uri:
136+
return uri
137+
138+
broker_params = _get_env_var(
139+
"INVENIO_BROKER", ["user", "password", "host", "port", "protocol"]
140+
)
141+
if all(broker_params.values()):
142+
vhost = os.environ.get("INVENIO_BROKER_VHOST", "").lstrip("/") or ""
143+
return f"{broker_params['protocol']}://{broker_params['user']}:{broker_params['password']}@{broker_params['host']}:{broker_params['port']}/{vhost}"
144+
145+
return default_url
146+
147+
148+
def build_redis_url(db=None):
149+
"""
150+
Build Redis URL from environment variables or use default.
151+
152+
Priority order:
153+
1. BROKER_URL (Redis-based)
154+
2. INVENIO_REDIS_URL
155+
3. INVENIO_REDIS_* specific environment variables
156+
4. Default URL
157+
"""
158+
db = db if db is not None else 0
159+
default_url = f"redis://localhost:6379/{db}"
160+
161+
uri = os.environ.get("BROKER_URL")
162+
if uri and uri.startswith(("redis://", "rediss://", "unix://")):
163+
return uri
164+
165+
uri = os.environ.get("INVENIO_REDIS_URL")
166+
if uri:
167+
return uri
168+
169+
redis_params = _get_env_var(
170+
"INVENIO_REDIS", ["host", "port", "password", "protocol"]
171+
)
172+
redis_params["protocol"] = redis_params.get("protocol") or "redis"
173+
174+
if redis_params["host"] and redis_params["port"]:
175+
password = (
176+
f":{redis_params['password']}@" if redis_params.get("password") else ""
177+
)
178+
return f"{redis_params['protocol']}://{password}{redis_params['host']}:{redis_params['port']}/{db}"
179+
180+
return default_url

tests/test_invenio_config.py

+205
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,205 @@ 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+
"INVENIO_DB_USER": "testuser",
245+
"INVENIO_DB_PASSWORD": "testpassword",
246+
"INVENIO_DB_HOST": "testhost",
247+
"INVENIO_DB_PORT": "5432",
248+
"INVENIO_DB_NAME": "testdb",
249+
"INVENIO_DB_PROTOCOL": "postgresql+psycopg2",
250+
},
251+
"postgresql+psycopg2://testuser:testpassword@testhost:5432/testdb",
252+
),
253+
(
254+
{"INVENIO_SQLALCHEMY_DATABASE_URI": "sqlite:///test.db"},
255+
"sqlite:///test.db",
256+
),
257+
(
258+
{"SQLALCHEMY_DATABASE_URI": "sqlite:///test.db"},
259+
"sqlite:///test.db",
260+
),
261+
(
262+
{},
263+
"postgresql+psycopg2://invenio-app-rdm:invenio-app-rdm@localhost/invenio-app-rdm",
264+
),
265+
],
266+
)
267+
def test_build_db_uri(monkeypatch, env_vars, expected_uri):
268+
"""Test building database URI."""
269+
for key in [
270+
"INVENIO_DB_USER",
271+
"INVENIO_DB_PASSWORD",
272+
"INVENIO_DB_HOST",
273+
"INVENIO_DB_PORT",
274+
"INVENIO_DB_NAME",
275+
"INVENIO_DB_PROTOCOL",
276+
"INVENIO_SQLALCHEMY_DATABASE_URI",
277+
"SQLALCHEMY_DATABASE_URI",
278+
]:
279+
monkeypatch.delenv(key, raising=False)
280+
for key, value in env_vars.items():
281+
monkeypatch.setenv(key, value)
282+
283+
assert build_db_uri() == expected_uri
284+
285+
286+
@pytest.mark.parametrize(
287+
"env_vars, expected_url",
288+
[
289+
(
290+
{
291+
"INVENIO_BROKER_USER": "testuser",
292+
"INVENIO_BROKER_PASSWORD": "testpassword",
293+
"INVENIO_BROKER_HOST": "testhost",
294+
"INVENIO_BROKER_PORT": "5672",
295+
"INVENIO_BROKER_PROTOCOL": "amqp",
296+
},
297+
"amqp://testuser:testpassword@testhost:5672/",
298+
),
299+
(
300+
{"INVENIO_BROKER_URL": "amqp://guest:guest@localhost:5672/"},
301+
"amqp://guest:guest@localhost:5672/",
302+
),
303+
(
304+
{"BROKER_URL": "amqp://guest:guest@localhost:5672/"},
305+
"amqp://guest:guest@localhost:5672/",
306+
),
307+
(
308+
{},
309+
"amqp://guest:guest@localhost:5672/",
310+
),
311+
],
312+
)
313+
def test_build_broker_url(monkeypatch, env_vars, expected_url):
314+
"""Test building broker URL."""
315+
for key in [
316+
"INVENIO_BROKER_USER",
317+
"INVENIO_BROKER_PASSWORD",
318+
"INVENIO_BROKER_HOST",
319+
"INVENIO_BROKER_PORT",
320+
"INVENIO_BROKER_PROTOCOL",
321+
"INVENIO_BROKER_URL",
322+
"BROKER_URL",
323+
]:
324+
monkeypatch.delenv(key, raising=False)
325+
for key, value in env_vars.items():
326+
monkeypatch.setenv(key, value)
327+
328+
assert build_broker_url() == expected_url
329+
330+
331+
@pytest.mark.parametrize(
332+
"env_vars, expected_url",
333+
[
334+
(
335+
{
336+
"INVENIO_BROKER_USER": "testuser",
337+
"INVENIO_BROKER_PASSWORD": "testpassword",
338+
"INVENIO_BROKER_HOST": "testhost",
339+
"INVENIO_BROKER_PORT": "5672",
340+
"INVENIO_BROKER_PROTOCOL": "amqp",
341+
"INVENIO_BROKER_VHOST": "/testvhost",
342+
},
343+
"amqp://testuser:testpassword@testhost:5672/testvhost",
344+
),
345+
(
346+
{
347+
"INVENIO_BROKER_USER": "testuser",
348+
"INVENIO_BROKER_PASSWORD": "testpassword",
349+
"INVENIO_BROKER_HOST": "testhost",
350+
"INVENIO_BROKER_PORT": "5672",
351+
"INVENIO_BROKER_PROTOCOL": "amqp",
352+
"INVENIO_BROKER_VHOST": "testvhost",
353+
},
354+
"amqp://testuser:testpassword@testhost:5672/testvhost",
355+
),
356+
(
357+
{"INVENIO_BROKER_URL": "amqp://guest:guest@localhost:5672/"},
358+
"amqp://guest:guest@localhost:5672/",
359+
),
360+
(
361+
{},
362+
"amqp://guest:guest@localhost:5672/",
363+
),
364+
],
365+
)
366+
def test_build_broker_url_with_vhost(monkeypatch, env_vars, expected_url):
367+
"""Test building broker URL with vhost."""
368+
for key in [
369+
"INVENIO_BROKER_USER",
370+
"INVENIO_BROKER_PASSWORD",
371+
"INVENIO_BROKER_HOST",
372+
"INVENIO_BROKER_PORT",
373+
"INVENIO_BROKER_PROTOCOL",
374+
"INVENIO_BROKER_URL",
375+
"INVENIO_BROKER_VHOST",
376+
"BROKER_URL",
377+
]:
378+
monkeypatch.delenv(key, raising=False)
379+
for key, value in env_vars.items():
380+
monkeypatch.setenv(key, value)
381+
382+
assert build_broker_url() == expected_url
383+
384+
385+
@pytest.mark.parametrize(
386+
"env_vars, db, expected_url",
387+
[
388+
(
389+
{
390+
"INVENIO_REDIS_HOST": "testhost",
391+
"INVENIO_REDIS_PORT": "6379",
392+
"INVENIO_REDIS_PASSWORD": "testpassword",
393+
"INVENIO_REDIS_PROTOCOL": "redis",
394+
},
395+
2,
396+
"redis://:testpassword@testhost:6379/2",
397+
),
398+
(
399+
{
400+
"INVENIO_REDIS_HOST": "testhost",
401+
"INVENIO_REDIS_PORT": "6379",
402+
"INVENIO_REDIS_PROTOCOL": "redis",
403+
},
404+
1,
405+
"redis://testhost:6379/1",
406+
),
407+
(
408+
{"BROKER_URL": "redis://localhost:6379/0"},
409+
None,
410+
"redis://localhost:6379/0",
411+
),
412+
(
413+
{"INVENIO_REDIS_URL": "redis://localhost:6379/3"},
414+
3,
415+
"redis://localhost:6379/3",
416+
),
417+
(
418+
{},
419+
4,
420+
"redis://localhost:6379/4",
421+
),
422+
],
423+
)
424+
def test_build_redis_url(monkeypatch, env_vars, db, expected_url):
425+
"""Test building Redis URL."""
426+
for key in [
427+
"INVENIO_REDIS_HOST",
428+
"INVENIO_REDIS_PORT",
429+
"INVENIO_REDIS_PASSWORD",
430+
"INVENIO_REDIS_PROTOCOL",
431+
"INVENIO_REDIS_URL",
432+
"BROKER_URL",
433+
]:
434+
monkeypatch.delenv(key, raising=False)
435+
for key, value in env_vars.items():
436+
monkeypatch.setenv(key, value)
437+
438+
assert build_redis_url(db=db) == expected_url

0 commit comments

Comments
 (0)