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:
parent
9af717ef7f
commit
2dcc654b70
|
@ -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 }}
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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:
|
||||
|
|
|
@ -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
|
||||
|
||||
|
|
|
@ -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.
|
|
@ -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)
|
|
@ -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,
|
||||
},
|
||||
},
|
||||
}
|
||||
}
|
|
@ -0,0 +1,12 @@
|
|||
from libretime_shared.config import (
|
||||
BaseConfig,
|
||||
DatabaseConfig,
|
||||
GeneralConfig,
|
||||
RabbitMQConfig,
|
||||
)
|
||||
|
||||
|
||||
class Config(BaseConfig):
|
||||
general: GeneralConfig
|
||||
database: DatabaseConfig = DatabaseConfig()
|
||||
rabbitmq: RabbitMQConfig = RabbitMQConfig()
|
|
@ -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}"
|
||||
|
|
|
@ -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(
|
||||
|
|
|
@ -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)]
|
|
@ -43,6 +43,7 @@ setup(
|
|||
"dev": [
|
||||
"model_bakery",
|
||||
"psycopg2-binary",
|
||||
f"libretime-shared @ file://localhost/{here.parent / 'shared'}#egg=libretime_shared",
|
||||
],
|
||||
},
|
||||
)
|
||||
|
|
Loading…
Reference in New Issue