feat(api): load config using shared helpers

- add django settings module documentation
- use default for previously required fields

BREAKING CHANGE: The API command line interface require the
configuration file to be present. The default configuration file path is
`/etc/airtime/airtime.conf`
This commit is contained in:
jo 2022-03-01 13:22:24 +01:00 committed by Kyle Robbertze
parent 9af717ef7f
commit 2dcc654b70
12 changed files with 119 additions and 146 deletions

View File

@ -192,6 +192,7 @@ jobs:
postgres:
image: postgres
env:
POSTGRES_USER: libretime
POSTGRES_PASSWORD: libretime
options: >-
--health-cmd pg_isready
@ -204,7 +205,8 @@ jobs:
run:
shell: bash
env:
LIBRETIME_CONFIG_FILEPATH: /tmp/libretime-test.conf
LIBRETIME_GENERAL_API_KEY: test_key
LIBRETIME_DATABASE_HOST: postgres
steps:
- uses: actions/checkout@v2
@ -216,20 +218,6 @@ jobs:
restore-keys: |
${{ runner.os }}-pip-${{ matrix.context }}
- name: Setup libretime configuration
run: |
cat <<EOF > $LIBRETIME_CONFIG_FILEPATH
[general]
api_key = test_key
[database]
host = postgres
port = 5432
name = libretime
user = postgres
password = libretime
EOF
cat $LIBRETIME_CONFIG_FILEPATH
- name: Test
run: make test
working-directory: ${{ matrix.context }}

View File

@ -7,6 +7,7 @@ NotifyAccess=all
KillSignal=SIGQUIT
Environment=LIBRETIME_LOG_FILEPATH=/var/log/libretime/api.log
Environment=LIBRETIME_CONFIG_FILEPATH=/etc/airtime/airtime.conf
ExecStart=/usr/bin/uwsgi /etc/airtime/libretime-api.ini
User=libretime-api

View File

@ -7,6 +7,7 @@ import sys
def main():
os.environ.setdefault("DJANGO_SETTINGS_MODULE", "libretime_api.settings")
os.environ.setdefault("LIBRETIME_CONFIG_FILEPATH", "/etc/airtime/airtime.conf")
try:
from django.core.management import execute_from_command_line
except ImportError as exc:

View File

@ -55,7 +55,7 @@ def check_authorization_header(request):
if auth_header.startswith("Api-Key"):
token = auth_header.split()[1]
if token == settings.CONFIG.get("general", "api_key"):
if token == settings.CONFIG.general.api_key:
return True
return False

View File

@ -0,0 +1,7 @@
# Django settings
The structure of the django settings module is the following:
- the `__init__.py` (`libretime_api.settings`) module is the django settings entrypoint. The module contains bindings between the user configuration and the django settings. **Advanced users** may edit this file to better integrate the LibreTime API in their setup.
- the `_internal.py` module contains application settings for django.
- the `_schema.py` module contains the schema for the user configuration parsing and validation.

View File

@ -0,0 +1,46 @@
# pylint: disable=unused-import
from os import getenv
from ._internal import (
AUTH_PASSWORD_VALIDATORS,
AUTH_USER_MODEL,
DEBUG,
INSTALLED_APPS,
MIDDLEWARE,
REST_FRAMEWORK,
ROOT_URLCONF,
TEMPLATES,
TEST_RUNNER,
WSGI_APPLICATION,
setup_logger,
)
from ._schema import Config
API_VERSION = "2.0.0"
LIBRETIME_LOG_FILEPATH = getenv("LIBRETIME_LOG_FILEPATH")
LIBRETIME_CONFIG_FILEPATH = getenv("LIBRETIME_CONFIG_FILEPATH")
CONFIG = Config(filepath=LIBRETIME_CONFIG_FILEPATH)
SECRET_KEY = CONFIG.general.api_key
ALLOWED_HOSTS = ["*"]
DATABASES = {
"default": {
"ENGINE": "django.db.backends.postgresql",
"HOST": CONFIG.database.host,
"PORT": CONFIG.database.port,
"NAME": CONFIG.database.name,
"USER": CONFIG.database.user,
"PASSWORD": CONFIG.database.password,
}
}
LANGUAGE_CODE = "en-us"
TIME_ZONE = "UTC"
USE_I18N = True
USE_L10N = True
USE_TZ = True
LOGGING = setup_logger(LIBRETIME_LOG_FILEPATH)

View File

@ -1,32 +1,8 @@
import os
from .utils import get_random_string, read_config_file
API_VERSION = "2.0.0"
LIBRETIME_LOG_FILEPATH = os.getenv("LIBRETIME_LOG_FILEPATH")
LIBRETIME_CONFIG_FILEPATH = os.getenv(
"LIBRETIME_CONFIG_FILEPATH",
"/etc/airtime/airtime.conf",
)
LIBRETIME_STATIC_ROOT = os.getenv(
"LIBRETIME_STATIC_ROOT",
"/usr/share/airtime/api",
)
CONFIG = read_config_file(LIBRETIME_CONFIG_FILEPATH)
# Quick-start development settings - unsuitable for production
# See https://docs.djangoproject.com/en/3.0/howto/deployment/checklist/
# SECURITY WARNING: keep the secret key used in production secret!
SECRET_KEY = get_random_string(CONFIG.get("general", "api_key", fallback=""))
from os import getenv
from typing import Optional
# SECURITY WARNING: don't run with debug turned on in production!
DEBUG = os.getenv("LIBRETIME_DEBUG", False)
ALLOWED_HOSTS = ["*"]
DEBUG = getenv("LIBRETIME_DEBUG")
# Application definition
@ -72,22 +48,6 @@ TEMPLATES = [
WSGI_APPLICATION = "libretime_api.wsgi.application"
# Database
# https://docs.djangoproject.com/en/3.0/ref/settings/#databases
DATABASES = {
"default": {
"ENGINE": "django.db.backends.postgresql",
"NAME": CONFIG.get("database", "name", fallback="libretime"),
"USER": CONFIG.get("database", "user", fallback="libretime"),
"PASSWORD": CONFIG.get("database", "password", fallback="libretime"),
"HOST": CONFIG.get("database", "host", fallback="localhost"),
"PORT": CONFIG.get("database", "port", fallback="5432"),
}
}
# Password validation
# https://docs.djangoproject.com/en/3.0/ref/settings/#auth-password-validators
@ -129,66 +89,53 @@ REST_FRAMEWORK = {
"URL_FIELD_NAME": "item_url",
}
# Internationalization
# https://docs.djangoproject.com/en/3.0/topics/i18n/
LANGUAGE_CODE = "en-us"
TIME_ZONE = "UTC"
USE_I18N = True
USE_L10N = True
USE_TZ = True
AUTH_USER_MODEL = "libretime_api.User"
TEST_RUNNER = "libretime_api.tests.runners.ManagedModelTestRunner"
LOGGING_HANDLERS = {
"console": {
"level": "INFO",
"class": "logging.StreamHandler",
"formatter": "simple",
},
}
if LIBRETIME_LOG_FILEPATH is not None:
LOGGING_HANDLERS["file"] = {
"level": "DEBUG",
"class": "logging.FileHandler",
"filename": LIBRETIME_LOG_FILEPATH,
"formatter": "verbose",
# Logging
def setup_logger(log_filepath: Optional[str]):
logging_handlers = {
"console": {
"level": "INFO",
"class": "logging.StreamHandler",
"formatter": "simple",
},
}
LOGGING = {
"version": 1,
"disable_existing_loggers": False,
"formatters": {
"simple": {
"format": "{levelname} {message}",
"style": "{",
if log_filepath is not None:
logging_handlers["file"] = {
"level": "DEBUG",
"class": "logging.FileHandler",
"filename": log_filepath,
"formatter": "verbose",
}
return {
"version": 1,
"disable_existing_loggers": False,
"formatters": {
"simple": {
"format": "{levelname} {message}",
"style": "{",
},
"verbose": {
"format": "{asctime} {module} {levelname} {message}",
"style": "{",
},
},
"verbose": {
"format": "{asctime} {module} {levelname} {message}",
"style": "{",
"handlers": logging_handlers,
"loggers": {
"django": {
"handlers": logging_handlers.keys(),
"level": "INFO",
"propagate": True,
},
"libretime_api": {
"handlers": logging_handlers.keys(),
"level": "INFO",
"propagate": True,
},
},
},
"handlers": LOGGING_HANDLERS,
"loggers": {
"django": {
"handlers": LOGGING_HANDLERS.keys(),
"level": "INFO",
"propagate": True,
},
"libretime_api": {
"handlers": LOGGING_HANDLERS.keys(),
"level": "INFO",
"propagate": True,
},
},
}
}

View File

@ -0,0 +1,12 @@
from libretime_shared.config import (
BaseConfig,
DatabaseConfig,
GeneralConfig,
RabbitMQConfig,
)
class Config(BaseConfig):
general: GeneralConfig
database: DatabaseConfig = DatabaseConfig()
rabbitmq: RabbitMQConfig = RabbitMQConfig()

View File

@ -33,7 +33,7 @@ class TestIsSystemTokenOrUser(APITestCase):
self.assertFalse(allowed)
def test_token_correct(self):
token = settings.CONFIG.get("general", "api_key")
token = settings.CONFIG.general.api_key
request = APIRequestFactory().get(self.path)
request.user = AnonymousUser()
request.META["Authorization"] = f"Api-Key {token}"

View File

@ -14,7 +14,7 @@ class TestFileViewSet(APITestCase):
@classmethod
def setUpTestData(cls):
cls.path = "/api/v2/files/{id}/download/"
cls.token = settings.CONFIG.get("general", "api_key")
cls.token = settings.CONFIG.general.api_key
def test_invalid(self):
path = self.path.format(id="a")
@ -49,7 +49,7 @@ class TestScheduleViewSet(APITestCase):
@classmethod
def setUpTestData(cls):
cls.path = "/api/v2/schedule/"
cls.token = settings.CONFIG.get("general", "api_key")
cls.token = settings.CONFIG.general.api_key
def test_schedule_item_full_length(self):
music_dir = baker.make(

View File

@ -1,30 +0,0 @@
import random
import string
import sys
from configparser import ConfigParser
def read_config_file(config_filepath):
"""Parse the application's config file located at config_path."""
config = ConfigParser()
try:
with open(config_filepath, encoding="utf-8") as config_file:
config.read_file(config_file)
except OSError as error:
print(
f"Unable to read config file at {config_filepath}: {error.strerror}",
file=sys.stderr,
)
return ConfigParser()
except Exception as error:
print(error, file=sys.stderr)
raise error
return config
def get_random_string(seed):
"""Generates a random string based on the given seed"""
choices = string.ascii_letters + string.digits + string.punctuation
seed = seed.encode("utf-8")
rand = random.Random(seed)
return [rand.choice(choices) for i in range(16)]

View File

@ -43,6 +43,7 @@ setup(
"dev": [
"model_bakery",
"psycopg2-binary",
f"libretime-shared @ file://localhost/{here.parent / 'shared'}#egg=libretime_shared",
],
},
)