diff --git a/Dockerfile b/Dockerfile index a32c3aa..96cb07b 100644 --- a/Dockerfile +++ b/Dockerfile @@ -52,7 +52,11 @@ RUN groupadd --gid $APP_GID $APP_GROUP && \ # Don't buffer stdout and stderr as it breaks realtime logging ENV PYTHONUNBUFFERED 1 -# TODO(tylerchristie): django flexi settings +# Install application configuration using flexi-settings +ENV DJANGO_SETTINGS_MODULE flexi_settings.settings +ENV DJANGO_FLEXI_SETTINGS_ROOT /etc/coral-credits/settings.py +COPY ./etc/coral-credits /etc/coral-credits +RUN mkdir -p /etc/coral-credits/settings.d # By default, serve the app on port 8080 using the app user EXPOSE 8080 diff --git a/charts/files/settings/01-django.yaml b/charts/files/settings/01-django.yaml new file mode 100644 index 0000000..41a569f --- /dev/null +++ b/charts/files/settings/01-django.yaml @@ -0,0 +1,2 @@ +SECRET_KEY: {{ .Values.settings.secretKey | default (randAlphaNum 64) }} +DEBUG: {{ .Values.settings.debug }} \ No newline at end of file diff --git a/charts/templates/deployment.yaml b/charts/templates/deployment.yaml index 7f2d636..2a9537b 100644 --- a/charts/templates/deployment.yaml +++ b/charts/templates/deployment.yaml @@ -26,6 +26,9 @@ spec: volumeMounts: - name: data mountPath: /data + - name: runtime-settings + mountPath: /etc/coral-credits/settings.d + readOnly: true containers: - name: api securityContext: {{ toYaml .Values.securityContext | nindent 12 }} @@ -48,6 +51,9 @@ spec: volumeMounts: - name: data mountPath: /data + - name: runtime-settings + mountPath: /etc/coral-credits/settings.d + readOnly: true {{- with .Values.nodeSelector }} nodeSelector: {{ toYaml . | nindent 8 }} {{- end }} @@ -60,4 +66,7 @@ spec: volumes: - name: data persistentVolumeClaim: - claimName: {{ include "coral-credits.fullname" . }} \ No newline at end of file + claimName: {{ include "coral-credits.fullname" . }} + - name: runtime-settings + secret: + secretName: {{ include "coral-credits.fullname" . }} \ No newline at end of file diff --git a/charts/templates/settings.yaml b/charts/templates/settings.yaml new file mode 100644 index 0000000..5ac5a9f --- /dev/null +++ b/charts/templates/settings.yaml @@ -0,0 +1,11 @@ +apiVersion: v1 +kind: Secret +metadata: + name: {{ include "coral-credits.fullname" . }} + labels: {{ include "coral-credits.labels" . | nindent 4 }} +type: Opaque +# Use data because of https://github.com/helm/helm/issues/10010 +# Not doing so means that AWX-related keys are not removed on transition to the CRD +data: + 01-django.yaml: | + {{- tpl (.Files.Get "files/settings/01-django.yaml") . | b64enc | nindent 4 }} \ No newline at end of file diff --git a/charts/values.yaml b/charts/values.yaml index 0697e96..b49090c 100644 --- a/charts/values.yaml +++ b/charts/values.yaml @@ -67,6 +67,15 @@ securityContext: drop: [ALL] readOnlyRootFilesystem: true +# Django settings +settings: + # The Django secret key + # If not given, a randomly generated key will be used + # However this will be different on each deployment which may cause sessions to be terminated + secretKey: + # Use debug mode (recommended false in production) + debug: false + # Resource requests and limits for the containers resources: {} diff --git a/coral_credits/settings.py b/coral_credits/settings.py deleted file mode 100644 index b839c05..0000000 --- a/coral_credits/settings.py +++ /dev/null @@ -1,145 +0,0 @@ -""" -Django settings for coral_credits project. - -Generated by 'django-admin startproject' using Django 5.0.1. - -For more information on this file, see -https://docs.djangoproject.com/en/5.0/topics/settings/ - -For the full list of settings and their values, see -https://docs.djangoproject.com/en/5.0/ref/settings/ -""" - -from pathlib import Path - -# Build paths inside the project like this: BASE_DIR / 'subdir'. -BASE_DIR = Path(__file__).resolve().parent.parent - - -# Quick-start development settings - unsuitable for production -# See https://docs.djangoproject.com/en/5.0/howto/deployment/checklist/ - -# SECURITY WARNING: keep the secret key used in production secret! -SECRET_KEY = "django-insecure--%%s+1^nkk97qur-52c0tbi4g(wxgtwuaau14etjrhvhqo0+i#" - -# SECURITY WARNING: don't run with debug turned on in production! -DEBUG = False - -# In a Docker container, ALLOWED_HOSTS is always '*' - let the proxy worry about hosts -ALLOWED_HOSTS = ["*"] - - -# Application definition - -INSTALLED_APPS = [ - "django.contrib.admin", - "django.contrib.auth", - "django.contrib.contenttypes", - "django.contrib.sessions", - "django.contrib.messages", - "django.contrib.staticfiles", - "django_extensions", - "drf_spectacular", - "rest_framework", - "auditlog", - "coral_credits.api", -] - -AUDITLOG_INCLUDE_ALL_MODELS = True - -MIDDLEWARE = [ - "django.middleware.security.SecurityMiddleware", - "django.contrib.sessions.middleware.SessionMiddleware", - "django.middleware.common.CommonMiddleware", - "django.middleware.csrf.CsrfViewMiddleware", - "django.contrib.auth.middleware.AuthenticationMiddleware", - "django.contrib.messages.middleware.MessageMiddleware", - "django.middleware.clickjacking.XFrameOptionsMiddleware", -] - -REST_FRAMEWORK = { - "DEFAULT_SCHEMA_CLASS": "drf_spectacular.openapi.AutoSchema", -} - -ROOT_URLCONF = "coral_credits.urls" - -SPECTACULAR_SETTINGS = { - "TITLE": "Coral Credits API", - "DESCRIPTION": 'Coral credits is a resource management system that helps build a \ - "coral reef style" fixed capacity cloud, cooperatively sharing community \ - resources through interfaces such as: Azimuth, OpenStack Blazar and Slurm.', - "VERSION": "0.1.0", - "SERVE_INCLUDE_SCHEMA": False, - # OTHER SETTINGS -} - -TEMPLATES = [ - { - "BACKEND": "django.template.backends.django.DjangoTemplates", - "DIRS": [], - "APP_DIRS": True, - "OPTIONS": { - "context_processors": [ - "django.template.context_processors.debug", - "django.template.context_processors.request", - "django.contrib.auth.context_processors.auth", - "django.contrib.messages.context_processors.messages", - ], - }, - }, -] - -WSGI_APPLICATION = "coral_credits.wsgi.application" - - -# Database -# https://docs.djangoproject.com/en/5.0/ref/settings/#databases - -DATABASES = { - "default": { - "ENGINE": "django.db.backends.sqlite3", - "NAME": "/data/db.sqlite3", - } -} - - -# Password validation -# https://docs.djangoproject.com/en/5.0/ref/settings/#auth-password-validators - -AUTH_PASSWORD_VALIDATORS = [ - { - "NAME": "django.contrib.auth.password_validation.UserAttributeSimilarityValidator", # noqa: E501 - }, - { - "NAME": "django.contrib.auth.password_validation.MinimumLengthValidator", - }, - { - "NAME": "django.contrib.auth.password_validation.CommonPasswordValidator", - }, - { - "NAME": "django.contrib.auth.password_validation.NumericPasswordValidator", - }, -] - - -# Internationalization -# https://docs.djangoproject.com/en/5.0/topics/i18n/ - -LANGUAGE_CODE = "en-us" - -TIME_ZONE = "UTC" - -USE_I18N = True - -USE_TZ = True - - -# Static files (CSS, JavaScript, Images) -# https://docs.djangoproject.com/en/5.0/howto/static-files/ - -STATIC_URL = "static/" - -# Default primary key field type -# https://docs.djangoproject.com/en/5.0/ref/settings/#default-auto-field - -DEFAULT_AUTO_FIELD = "django.db.models.BigAutoField" diff --git a/etc/coral-credits/app.py b/etc/coral-credits/app.py new file mode 100644 index 0000000..0088236 --- /dev/null +++ b/etc/coral-credits/app.py @@ -0,0 +1,64 @@ +""" +Application-specific settings. +""" + +from pathlib import Path + +# Build paths inside the project like this: BASE_DIR / 'subdir'. +BASE_DIR = Path(__file__).resolve().parent.parent + +# Database +# https://docs.djangoproject.com/en/5.0/ref/settings/#databases + +DATABASES = { + "default": { + "ENGINE": "django.db.backends.sqlite3", + "NAME": "/data/db.sqlite3", + } +} + +# Application definition + +INSTALLED_APPS = [ + "django.contrib.admin", + "django.contrib.auth", + "django.contrib.contenttypes", + "django.contrib.sessions", + "django.contrib.messages", + "django.contrib.staticfiles", + "django_extensions", + "drf_spectacular", + "rest_framework", + "auditlog", + "coral_credits.api", +] + +AUDITLOG_INCLUDE_ALL_MODELS = True + +MIDDLEWARE = [ + "django.middleware.security.SecurityMiddleware", + "django.contrib.sessions.middleware.SessionMiddleware", + "django.middleware.common.CommonMiddleware", + "django.middleware.csrf.CsrfViewMiddleware", + "django.contrib.auth.middleware.AuthenticationMiddleware", + "django.contrib.messages.middleware.MessageMiddleware", + "django.middleware.clickjacking.XFrameOptionsMiddleware", +] + +REST_FRAMEWORK = { + "DEFAULT_SCHEMA_CLASS": "drf_spectacular.openapi.AutoSchema", +} + +ROOT_URLCONF = "coral_credits.urls" + +SPECTACULAR_SETTINGS = { + "TITLE": "Coral Credits API", + "DESCRIPTION": 'Coral credits is a resource management system that helps build a \ + "coral reef style" fixed capacity cloud, cooperatively sharing community \ + resources through interfaces such as: Azimuth, OpenStack Blazar and Slurm.', + "VERSION": "0.1.0", + "SERVE_INCLUDE_SCHEMA": False, + # OTHER SETTINGS +} + +WSGI_APPLICATION = "coral_credits.wsgi.application" \ No newline at end of file diff --git a/etc/coral-credits/defaults.py b/etc/coral-credits/defaults.py new file mode 100644 index 0000000..f532bba --- /dev/null +++ b/etc/coral-credits/defaults.py @@ -0,0 +1,125 @@ +""" +Django settings for coral_credits project. + +For more information on this file, see +https://docs.djangoproject.com/en/5.0/topics/settings/ + +For the full list of settings and their values, see +https://docs.djangoproject.com/en/5.0/ref/settings/ +""" + +import os + +from django.core.management.utils import get_random_secret_key + +# By default, don't run in DEBUG mode +DEBUG = False + +# In a Docker container, ALLOWED_HOSTS is always '*' - let the proxy worry about hosts +ALLOWED_HOSTS = ['*'] + +# Make sure Django interprets the script name correctly if set +if 'SCRIPT_NAME' in os.environ: + FORCE_SCRIPT_NAME = os.environ['SCRIPT_NAME'] + +# Set a default random secret key +# This can be overridden by files included later if desired +SECRET_KEY = get_random_secret_key() + +# All logging should go to stdout/stderr to be collected +import logging +LOG_FORMAT = '[%(levelname)s] [%(asctime)s] [%(name)s:%(lineno)s] [%(threadName)s] %(message)s' +LOGGING = { + 'version' : 1, + 'disable_existing_loggers' : False, + 'formatters' : { + 'default' : { + 'format' : LOG_FORMAT, + }, + }, + 'filters' : { + # Logging filter that only accepts records with a level < WARNING + # This allows us to log level >= WARNING to stderr and level < WARNING to stdout + 'less_than_warning' : { + '()': 'django.utils.log.CallbackFilter', + 'callback': lambda record: record.levelno < logging.WARNING, + }, + }, + 'handlers' : { + 'stdout' : { + 'class' : 'logging.StreamHandler', + 'stream' : 'ext://sys.stdout', + 'formatter' : 'default', + 'filters': ['less_than_warning'], + }, + 'stderr' : { + 'class' : 'logging.StreamHandler', + 'stream' : 'ext://sys.stderr', + 'formatter' : 'default', + 'level' : 'WARNING', + }, + }, + 'loggers' : { + '' : { + 'handlers' : ['stdout', 'stderr'], + 'level' : 'DEBUG' if DEBUG else 'INFO', + 'propogate' : True, + }, + }, +} + +TEMPLATES = [ + { + "BACKEND": "django.template.backends.django.DjangoTemplates", + "DIRS": [], + "APP_DIRS": True, + "OPTIONS": { + "context_processors": [ + "django.template.context_processors.debug", + "django.template.context_processors.request", + "django.contrib.auth.context_processors.auth", + "django.contrib.messages.context_processors.messages", + ], + }, + }, +] + +# Password validation +# https://docs.djangoproject.com/en/5.0/ref/settings/#auth-password-validators + +AUTH_PASSWORD_VALIDATORS = [ + { + "NAME": "django.contrib.auth.password_validation.UserAttributeSimilarityValidator", # noqa: E501 + }, + { + "NAME": "django.contrib.auth.password_validation.MinimumLengthValidator", + }, + { + "NAME": "django.contrib.auth.password_validation.CommonPasswordValidator", + }, + { + "NAME": "django.contrib.auth.password_validation.NumericPasswordValidator", + }, +] + +# Internationalization +# https://docs.djangoproject.com/en/5.0/topics/i18n/ + +LANGUAGE_CODE = "en-us" + +TIME_ZONE = "UTC" + +USE_I18N = True + +USE_TZ = True + + +# Static files (CSS, JavaScript, Images) +# https://docs.djangoproject.com/en/5.0/howto/static-files/ + +STATIC_URL = "static/" + +# Default primary key field type +# https://docs.djangoproject.com/en/5.0/ref/settings/#default-auto-field + +DEFAULT_AUTO_FIELD = "django.db.models.BigAutoField" \ No newline at end of file diff --git a/etc/coral-credits/settings.py b/etc/coral-credits/settings.py new file mode 100644 index 0000000..1ac9e27 --- /dev/null +++ b/etc/coral-credits/settings.py @@ -0,0 +1,20 @@ +""" +Root settings file + +Includes settings files from a sibling directory called settings.d. +""" + +import os +from pathlib import Path + +from flexi_settings import include, include_dir + + +base_dir = Path(__file__).resolve().parent + +# First, include the defaults +include(base_dir / "defaults.py") +# Then include the application-level settings +include(base_dir / "app.py") +# Then include the runtime settings from a directory +include_dir(base_dir / "settings.d") \ No newline at end of file diff --git a/requirements.txt b/requirements.txt index e878124..232fd73 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,5 +1,7 @@ django-auditlog==3.0.0 Django==5.0.6 +django-flexi-settings @ git+https://github.com/stackhpc/django-flexi-settings.git@079359cc1e2d380a15ae6149ebffbcdae8094276 +django-settings-object @ git+https://github.com/cedadev/django-settings-object.git@2b66c0fc5eae92972df5210b4bc43f7d95ad9ceb djangorestframework==3.15.2 django-extensions==3.2.3 drf-spectacular==0.27.2