Skip to content

Commit 5c263c6

Browse files
author
Francesco Faraone
committed
LITE-28135 add support for django
1 parent 6d623a0 commit 5c263c6

File tree

12 files changed

+919
-150
lines changed

12 files changed

+919
-150
lines changed

connect/eaas/runner/config.py

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -57,6 +57,18 @@ def instance_id(self):
5757
def environment_type(self):
5858
return self.dyn_config.environment_type
5959

60+
@property
61+
def environment_runtime(self):
62+
return self.dyn_config.environment_runtime
63+
64+
@property
65+
def environment_hostname(self):
66+
return self.dyn_config.environment_hostname
67+
68+
@property
69+
def environment_domain(self):
70+
return self.dyn_config.environment_domain
71+
6072
@property
6173
def account_id(self):
6274
return self.dyn_config.logging.meta.account_id
@@ -176,6 +188,15 @@ def update_dynamic_config(self, data):
176188
self.dyn_config.environment_type = (
177189
data.environment_type or self.dyn_config.environment_type
178190
)
191+
self.dyn_config.environment_runtime = (
192+
data.environment_runtime or self.dyn_config.environment_runtime
193+
)
194+
self.dyn_config.environment_hostname = (
195+
data.environment_hostname or self.dyn_config.environment_hostname
196+
)
197+
self.dyn_config.environment_domain = (
198+
data.environment_domain or self.dyn_config.environment_domain
199+
)
179200
self.dyn_config.logging.meta.account_id = (
180201
data.logging.meta.account_id or self.dyn_config.logging.meta.account_id
181202
)

connect/eaas/runner/constants.py

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -157,3 +157,32 @@
157157

158158
EXCEL_NULL_MARKER = '#N/A'
159159
ROW_DELETED_MARKER = '#INSTRUCTION/DELETE_ROW'
160+
161+
DJANGO_NON_OVERRIDEABLE_SETTINGS = (
162+
'SECRET_KEY',
163+
'FORCE_SCRIPT_NAME',
164+
'INSTALLED_APPS',
165+
'ROOT_URLCONF',
166+
'CSRF_COOKIE_SECURE',
167+
'SECURE_CONTENT_TYPE_NOSNIFF',
168+
'SECURE_CROSS_ORIGIN_OPENER_POLICY',
169+
'SECURE_HSTS_INCLUDE_SUBDOMAINS',
170+
'SECURE_HSTS_PRELOAD',
171+
'SECURE_HSTS_SECONDS',
172+
'SECURE_PROXY_SSL_HEADER',
173+
'USE_X_FORWARDED_HOST',
174+
'USE_X_FORWARDED_PORT',
175+
'WSGI_APPLICATION',
176+
'SESSION_COOKIE_HTTPONLY',
177+
'SESSION_COOKIE_PATH',
178+
'SESSION_COOKIE_SAMESITE',
179+
'SESSION_COOKIE_SECURE',
180+
)
181+
DJANGO_REQUIRED_OVERRIDE_SETTINGS = tuple()
182+
DJANGO_ENFORCED_SETTINGS = {
183+
'CSRF_COOKIE_SECURE': False,
184+
'SECURE_PROXY_SSL_HEADER': ("HTTP_X_FORWARDED_PROTO", "https"),
185+
'USE_X_FORWARDED_HOST': True,
186+
'USE_X_FORWARDED_PORT': True,
187+
'SESSION_COOKIE_SECURE': True,
188+
}

connect/eaas/runner/handlers/base.py

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,8 @@ class ApplicationHandlerBase(ABC):
2525
def __init__(self, config):
2626
self._config = config
2727
self._logging_handler = None
28+
self._django_settings_module = self.get_django_settings_module()
29+
self._django_secret_key_variable = None
2830

2931
@property
3032
def config(self):
@@ -70,7 +72,36 @@ def features(self):
7072
def variables(self):
7173
return self.get_variables()
7274

75+
@property
76+
def django_settings_module(self):
77+
return self._django_settings_module
78+
79+
@property
80+
def django_secret_key_variable(self):
81+
if not self._django_secret_key_variable:
82+
application = self.get_application()
83+
if hasattr(application, 'get_django_secret_key_variable'):
84+
self._django_secret_key_variable = application.get_django_secret_key_variable()
85+
return self._django_secret_key_variable
86+
87+
def get_django_settings_module(self):
88+
ep = next(
89+
iter_entry_points('connect.eaas.ext', 'djsettings'),
90+
None,
91+
)
92+
if ep:
93+
get_settings = ep.load()
94+
return get_settings()
95+
return None
96+
97+
def load_django(self):
98+
if self._django_settings_module:
99+
os.environ.setdefault('DJANGO_SETTINGS_MODULE', self._django_settings_module)
100+
import django
101+
django.setup()
102+
73103
def load_application(self, name):
104+
self.load_django()
74105
ep = next(
75106
iter_entry_points('connect.eaas.ext', name),
76107
None,

connect/eaas/runner/handlers/web.py

Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@
55
import functools
66
import inspect
77
import logging
8+
import os
89

910
from fastapi import (
1011
FastAPI,
@@ -186,9 +187,41 @@ def get_asgi_application(self):
186187
static_root = self.get_application().get_static_root()
187188
if static_root:
188189
app.mount('/static', StaticFiles(directory=static_root), name='static')
190+
if self.django_settings_module:
191+
self.mount_django_apps(app)
189192

190193
return app
191194

195+
def mount_django_apps(self, app):
196+
from django.conf import settings # noqa: I001
197+
from django.core.exceptions import ImproperlyConfigured # noqa: I001
198+
from django.core.handlers.asgi import ASGIHandler # noqa: I001
199+
script_name = getattr(settings, 'FORCE_SCRIPT_NAME', None)
200+
if not (
201+
script_name
202+
and script_name.startswith('/guest/')
203+
and len(script_name) > len('/guest/')
204+
):
205+
raise ImproperlyConfigured(
206+
'`FORCE_SCRIPT_NAME` must be set and must start with "/guest/"',
207+
)
208+
static_root = self.get_application().get_static_root()
209+
if not (
210+
settings.STATIC_ROOT.startswith(static_root)
211+
and len(settings.STATIC_ROOT) > len(static_root) - 1
212+
):
213+
raise ImproperlyConfigured(
214+
f'`STATIC_ROOT` must be a directory within {static_root}',
215+
)
216+
static_path = os.path.join(script_name, settings.STATIC_URL)
217+
app.mount(
218+
static_path,
219+
StaticFiles(
220+
directory=settings.STATIC_ROOT,
221+
),
222+
)
223+
app.mount(script_name, ASGIHandler())
224+
192225
def setup_middlewares(self, app, middlewares):
193226
for middleware in middlewares:
194227
if inspect.isfunction(middleware):

connect/eaas/runner/helpers.py

Lines changed: 63 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33
#
44
# Copyright (c) 2022 Ingram Micro. All Rights Reserved.
55
#
6+
import copy
67
import logging
78
import os
89
import subprocess
@@ -47,6 +48,9 @@
4748
)
4849
from connect.eaas.runner.constants import (
4950
BACKGROUND_TASK_MAX_EXECUTION_TIME,
51+
DJANGO_ENFORCED_SETTINGS,
52+
DJANGO_NON_OVERRIDEABLE_SETTINGS,
53+
DJANGO_REQUIRED_OVERRIDE_SETTINGS,
5054
HANDLER_CLASS_TITLE,
5155
INTERACTIVE_TASK_MAX_EXECUTION_TIME,
5256
ORDINAL_SUFFIX,
@@ -618,3 +622,62 @@ def get_features_table(features):
618622
Align.center(get_tfnapp_detail_table(details)),
619623
)
620624
return table
625+
626+
627+
def enforce_and_override_django_settings(
628+
config,
629+
django_secret_key_variable,
630+
overridden_settings,
631+
):
632+
from django.conf import settings # noqa: I001
633+
from django.core.exceptions import ImproperlyConfigured # noqa: I001
634+
if not django_secret_key_variable:
635+
raise ImproperlyConfigured(
636+
'Your extension class must be decorated with the '
637+
'`@django_secret_key_variable` to specify the name of '
638+
'the environment variable that store the django `SECRET_KEY`.',
639+
)
640+
if django_secret_key_variable not in config.variables:
641+
raise ImproperlyConfigured(
642+
f'The environment variable {django_secret_key_variable} has '
643+
'not been found and it is mandatory to setup django.',
644+
)
645+
646+
settings.SECRET_KEY = config.variables[django_secret_key_variable]
647+
648+
overrides = copy.copy(overridden_settings)
649+
650+
required_overrides = list(DJANGO_REQUIRED_OVERRIDE_SETTINGS)
651+
if config.environment_runtime == 'cloud':
652+
required_overrides.append('DATABASES')
653+
for required_setting in required_overrides:
654+
if required_setting not in overrides.keys():
655+
raise ImproperlyConfigured(
656+
f'The settings `{",".join(required_overrides)}` must be overridden.',
657+
)
658+
for non_overrideable_setting in DJANGO_NON_OVERRIDEABLE_SETTINGS:
659+
if non_overrideable_setting in overrides.keys():
660+
raise ImproperlyConfigured(
661+
'The settings '
662+
f'`{",".join(DJANGO_NON_OVERRIDEABLE_SETTINGS)}` cannot be overridden.',
663+
)
664+
databases_override = overrides.pop('DATABASES', None)
665+
666+
for setting, setting_value in overrides.items():
667+
setattr(settings, setting, setting_value)
668+
669+
if databases_override:
670+
for db, db_config in databases_override.items():
671+
for prop, propvalue in db_config.items():
672+
settings.DATABASES[db][prop] = propvalue
673+
674+
for setting, setting_value in DJANGO_ENFORCED_SETTINGS.items():
675+
setattr(settings, setting, setting_value)
676+
677+
if config.environment_hostname and config.environment_domain:
678+
settings.ALLOWED_HOSTS = [
679+
f'{config.environment_hostname}.{config.environment_domain}',
680+
]
681+
682+
if config.environment_runtime == 'cloud':
683+
settings.DEBUG = False

connect/eaas/runner/workers/base.py

Lines changed: 29 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -44,6 +44,7 @@
4444
StopBackoffError,
4545
)
4646
from connect.eaas.runner.helpers import (
47+
enforce_and_override_django_settings,
4748
to_ordinal,
4849
)
4950

@@ -177,7 +178,11 @@ async def run(self): # noqa: CCR001
177178
message = await self.receive()
178179
if not message:
179180
continue
181+
if self.handler.django_settings_module:
182+
await self.close_django_old_connections()
180183
await self.process_message(message)
184+
if self.handler.django_settings_module:
185+
await self.close_django_old_connections()
181186
except (ConnectionClosedOK, StopBackoffError) as exc:
182187
self.stop()
183188
if isinstance(exc, ConnectionClosedOK):
@@ -221,6 +226,12 @@ async def process_setup_response(self, data):
221226
reconfigured, then restart the tasks manager.
222227
"""
223228
self.config.update_dynamic_config(data)
229+
if self.handler.django_settings_module:
230+
enforce_and_override_django_settings(
231+
self.config,
232+
self.handler.django_secret_key_variable,
233+
await self.invoke_hook('get_django_settings'),
234+
)
224235
await self.trigger_event('on_startup')
225236
logger.info('Extension configuration has been updated.')
226237

@@ -235,19 +246,29 @@ async def trigger_event(self, event):
235246
return
236247
self.on_shutdown_fired.value = 1
237248

249+
await self.invoke_hook(event)
250+
251+
async def close_django_old_connections(self):
252+
from django.db import close_old_connections # noqa: I001
253+
return await asyncio.get_event_loop().run_in_executor(
254+
None,
255+
close_old_connections,
256+
)
257+
258+
async def invoke_hook(self, hook_name):
238259
application = self.handler.get_application()
239-
event_handler = getattr(application, event, None)
260+
hook = getattr(application, hook_name, None)
240261
if (
241-
event_handler
242-
and inspect.ismethod(event_handler)
243-
and event_handler.__self__ is application
262+
hook
263+
and inspect.ismethod(hook)
264+
and hook.__self__ is application
244265
):
245-
if inspect.iscoroutinefunction(event_handler):
246-
await event_handler(self.handler.get_logger(), self.config.variables)
266+
if inspect.iscoroutinefunction(hook):
267+
return await hook(self.handler.get_logger(), self.config.variables)
247268
else:
248-
await asyncio.get_event_loop().run_in_executor(
269+
return await asyncio.get_event_loop().run_in_executor(
249270
None,
250-
event_handler,
271+
hook,
251272
self.handler.get_logger(),
252273
self.config.variables,
253274
)

0 commit comments

Comments
 (0)