Skip to content

Commit

Permalink
config: granular env-based solution for connection strings
Browse files Browse the repository at this point in the history
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
  • Loading branch information
Samk13 committed Nov 28, 2024
1 parent e3bc51c commit e52709c
Show file tree
Hide file tree
Showing 2 changed files with 287 additions and 0 deletions.
98 changes: 98 additions & 0 deletions invenio_config/env.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@
#
# This file is part of Invenio.
# Copyright (C) 2015-2018 CERN.
# Copyright (C) 2024 KTH Royal Institute of Technology.
#
# Invenio is free software; you can redistribute it and/or modify it
# under the terms of the MIT License; see LICENSE file for more details.
Expand Down Expand Up @@ -45,3 +46,100 @@ def init_app(self, app):

# Set value
app.config[varname] = value


def _get_env_var(prefix, keys):
"""Retrieve environment variables with a given prefix."""
return {k: os.environ.get(f"{prefix}_{k.upper()}") for k in keys}


def build_db_uri():
"""
Build database URI from environment variables or use default.
Priority order:
1. INVENIO_SQLALCHEMY_DATABASE_URI
2. SQLALCHEMY_DATABASE_URI
3. INVENIO_DB_* specific environment variables
4. Default URI
Note: For option 3, to assert that the INVENIO_DB_* settings take effect,
you need to set SQLALCHEMY_DATABASE_URI="" in your environment.
"""
default_uri = "postgresql+psycopg2://invenio-app-rdm:invenio-app-rdm@localhost/invenio-app-rdm"

uri = os.environ.get("INVENIO_SQLALCHEMY_DATABASE_URI") or os.environ.get(
"SQLALCHEMY_DATABASE_URI"
)
if uri:
return uri

db_params = _get_env_var(
"INVENIO_DB", ["user", "password", "host", "port", "name", "protocol"]
)
if all(db_params.values()):
uri = f"{db_params['protocol']}://{db_params['user']}:{db_params['password']}@{db_params['host']}:{db_params['port']}/{db_params['name']}"
return uri

return default_uri


def build_broker_url():
"""
Build broker URL from environment variables or use default.
Priority order:
1. INVENIO_BROKER_URL
2. BROKER_URL
3. INVENIO_BROKER_* specific environment variables
4. Default URL
"""
default_url = "amqp://guest:guest@localhost:5672/"

uri = os.environ.get("INVENIO_BROKER_URL") or os.environ.get("BROKER_URL")
if uri:
return uri

broker_params = _get_env_var(
"INVENIO_BROKER", ["user", "password", "host", "port", "protocol"]
)
if all(broker_params.values()):
vhost = f"{os.environ.get("INVENIO_BROKER_VHOST").lstrip("/")}"
return f"{broker_params['protocol']}://{broker_params['user']}:{broker_params['password']}@{broker_params['host']}:{broker_params['port']}/{vhost}"

return default_url


def build_redis_url(db=None):
"""
Build Redis URL from environment variables or use default.
Priority order:
1. BROKER_URL (Redis-based)
2. INVENIO_REDIS_URL
3. INVENIO_REDIS_* specific environment variables
4. Default URL
"""
db = db if db is not None else 0
default_url = f"redis://localhost:6379/{db}"

uri = os.environ.get("BROKER_URL")
if uri and uri.startswith(("redis://", "rediss://", "unix://")):
return uri

uri = os.environ.get("INVENIO_REDIS_URL")
if uri:
return uri

redis_params = _get_env_var(
"INVENIO_REDIS", ["host", "port", "password", "protocol"]
)
redis_params["protocol"] = redis_params.get("protocol") or "redis"

if redis_params["host"] and redis_params["port"]:
password = (
f":{redis_params['password']}@" if redis_params.get("password") else ""
)
return f"{redis_params['protocol']}://{password}{redis_params['host']}:{redis_params['port']}/{db}"

return default_url
189 changes: 189 additions & 0 deletions tests/test_invenio_config.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@
#
# This file is part of Invenio.
# Copyright (C) 2015-2018 CERN.
# Copyright (C) 2024 KTH Royal Institute of Technology.
#
# Invenio is free software; you can redistribute it and/or modify it
# under the terms of the MIT License; see LICENSE file for more details.
Expand All @@ -16,6 +17,7 @@
import warnings
from os.path import join

import pytest
from flask import Flask
from mock import patch
from pkg_resources import EntryPoint
Expand All @@ -29,6 +31,7 @@
create_config_loader,
)
from invenio_config.default import ALLOWED_HTML_ATTRS, ALLOWED_HTML_TAGS
from invenio_config.env import build_broker_url, build_db_uri, build_redis_url


class ConfigEP(EntryPoint):
Expand Down Expand Up @@ -231,3 +234,189 @@ class Config(object):
assert app.config["ENV"] == "env"
finally:
shutil.rmtree(tmppath)


def set_env_vars(monkeypatch, env_vars):
"""Helper function to set environment variables."""
for key in env_vars:
monkeypatch.delenv(key, raising=False)
for key, value in env_vars.items():
monkeypatch.setenv(key, value)


@pytest.mark.parametrize(
"env_vars, expected_uri",
[
(
{
"INVENIO_DB_USER": "testuser",
"INVENIO_DB_PASSWORD": "testpassword",
"INVENIO_DB_HOST": "testhost",
"INVENIO_DB_PORT": "5432",
"INVENIO_DB_NAME": "testdb",
"INVENIO_DB_PROTOCOL": "postgresql+psycopg2",
},
"postgresql+psycopg2://testuser:testpassword@testhost:5432/testdb",
),
(
{
"INVENIO_SQLALCHEMY_DATABASE_URI": "postgresql+psycopg2://testuser:testpassword@testhost:5432/testdb"
},
"postgresql+psycopg2://testuser:testpassword@testhost:5432/testdb",
),
(
{
"SQLALCHEMY_DATABASE_URI": "postgresql+psycopg2://testuser:testpassword@testhost:5432/testdb"
},
"postgresql+psycopg2://testuser:testpassword@testhost:5432/testdb",
),
(
{},
"postgresql+psycopg2://invenio-app-rdm:invenio-app-rdm@localhost/invenio-app-rdm",
),
],
)
def test_build_db_uri(monkeypatch, env_vars, expected_uri):
"""Test building database URI."""
set_env_vars(monkeypatch, env_vars)
assert build_db_uri() == expected_uri


@pytest.mark.parametrize(
"env_vars, expected_url",
[
(
{
"INVENIO_BROKER_USER": "testuser",
"INVENIO_BROKER_PASSWORD": "testpassword",
"INVENIO_BROKER_HOST": "testhost",
"INVENIO_BROKER_PORT": "5672",
"INVENIO_BROKER_PROTOCOL": "amqp",
"INVENIO_BROKER_VHOST": "/testvhost",
},
"amqp://testuser:testpassword@testhost:5672/testvhost",
),
(
{
"INVENIO_BROKER_USER": "testuser",
"INVENIO_BROKER_PASSWORD": "testpassword",
"INVENIO_BROKER_HOST": "testhost",
"INVENIO_BROKER_PORT": "5672",
"INVENIO_BROKER_PROTOCOL": "amqp",
"INVENIO_BROKER_VHOST": "testvhost",
},
"amqp://testuser:testpassword@testhost:5672/testvhost",
),
(
{
"INVENIO_BROKER_USER": "testuser",
"INVENIO_BROKER_PASSWORD": "testpassword",
"INVENIO_BROKER_HOST": "testhost",
"INVENIO_BROKER_PORT": "5672",
"INVENIO_BROKER_PROTOCOL": "amqp",
"INVENIO_BROKER_VHOST": "",
},
"amqp://testuser:testpassword@testhost:5672/",
),
(
{
"INVENIO_BROKER_USER": "testuser",
"INVENIO_BROKER_PASSWORD": "testpassword",
"INVENIO_BROKER_HOST": "testhost",
"INVENIO_BROKER_PORT": "5672",
"INVENIO_BROKER_PROTOCOL": "amqp",
},
"amqp://testuser:testpassword@testhost:5672/",
),
],
)
def test_build_broker_url_with_vhost(monkeypatch, env_vars, expected_url):
"""Test building broker URL with vhost."""
set_env_vars(monkeypatch, env_vars)
assert build_broker_url() == expected_url


@pytest.mark.parametrize(
"env_vars, expected_url",
[
(
{
"INVENIO_BROKER_USER": "testuser",
"INVENIO_BROKER_PASSWORD": "testpassword",
"INVENIO_BROKER_HOST": "testhost",
"INVENIO_BROKER_PORT": "5672",
"INVENIO_BROKER_PROTOCOL": "amqp",
"INVENIO_BROKER_VHOST": "/testvhost",
},
"amqp://testuser:testpassword@testhost:5672/testvhost",
),
(
{
"INVENIO_BROKER_USER": "testuser",
"INVENIO_BROKER_PASSWORD": "testpassword",
"INVENIO_BROKER_HOST": "testhost",
"INVENIO_BROKER_PORT": "5672",
"INVENIO_BROKER_PROTOCOL": "amqp",
"INVENIO_BROKER_VHOST": "testvhost",
},
"amqp://testuser:testpassword@testhost:5672/testvhost",
),
(
{"INVENIO_BROKER_URL": "amqp://guest:guest@localhost:5672/"},
"amqp://guest:guest@localhost:5672/",
),
(
{},
"amqp://guest:guest@localhost:5672/",
),
],
)
def test_build_broker_url_with_vhost(monkeypatch, env_vars, expected_url):
"""Test building broker URL with vhost."""
set_env_vars(monkeypatch, env_vars)
assert build_broker_url() == expected_url


@pytest.mark.parametrize(
"env_vars, db, expected_url",
[
(
{
"INVENIO_REDIS_HOST": "testhost",
"INVENIO_REDIS_PORT": "6379",
"INVENIO_REDIS_PASSWORD": "testpassword",
"INVENIO_REDIS_PROTOCOL": "redis",
},
2,
"redis://:testpassword@testhost:6379/2",
),
(
{
"INVENIO_REDIS_HOST": "testhost",
"INVENIO_REDIS_PORT": "6379",
"INVENIO_REDIS_PROTOCOL": "redis",
},
1,
"redis://testhost:6379/1",
),
(
{"BROKER_URL": "redis://localhost:6379/0"},
None,
"redis://localhost:6379/0",
),
(
{"INVENIO_REDIS_URL": "redis://localhost:6379/3"},
3,
"redis://localhost:6379/3",
),
(
{},
4,
"redis://localhost:6379/4",
),
],
)
def test_build_redis_url(monkeypatch, env_vars, db, expected_url):
"""Test building Redis URL."""
set_env_vars(monkeypatch, env_vars)
assert build_redis_url(db=db) == expected_url

0 comments on commit e52709c

Please sign in to comment.