Skip to content

Commit a54c0ea

Browse files
authored
✨ Is2805/can disable exporter with envs (⚠️ devops) (ITISFoundation#2814)
1 parent 171d5f5 commit a54c0ea

File tree

7 files changed

+83
-14
lines changed

7 files changed

+83
-14
lines changed

services/web/server/src/simcore_service_webserver/application_settings.py

Lines changed: 27 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
import logging
2-
from typing import Any, Dict, Optional
2+
from typing import Any, Dict, List, Optional
33

44
from aiohttp import web
55
from models_library.basic_types import (
@@ -152,8 +152,27 @@ class Config(BaseCustomSettings.Config):
152152

153153
# HELPERS --------------------------------------------------------
154154

155-
def is_enabled(self, plugin_name: str):
156-
return getattr(self, f"WEBSERVER_{plugin_name.upper()}", None) is not None
155+
def is_enabled(self, field_name: str) -> bool:
156+
return getattr(self, field_name, None) is not None
157+
158+
def is_plugin(self, field_name: str) -> bool:
159+
if field := self.__fields__.get(field_name):
160+
if "auto_default_from_env" in field.field_info.extra and field.allow_none:
161+
return True
162+
return False
163+
164+
def _get_disabled_public_plugins(self) -> List[str]:
165+
plugins_disabled = []
166+
# NOTE: this list is limited for security reasons. An unbounded list
167+
# might reveal critical info on the settings of a deploy to the client.
168+
PUBLIC_PLUGIN_CANDIDATES = [
169+
"WEBSERVER_EXPORTER",
170+
"WEBSERVER_SCICRUNCH",
171+
]
172+
for field_name in PUBLIC_PLUGIN_CANDIDATES:
173+
if self.is_plugin(field_name) and not self.is_enabled(field_name):
174+
plugins_disabled.append(field_name)
175+
return plugins_disabled
157176

158177
def public_dict(self) -> Dict[str, Any]:
159178
"""Data publicaly available"""
@@ -182,6 +201,8 @@ def to_client_statics(self) -> Dict[str, Any]:
182201
exclude_none=True,
183202
by_alias=True,
184203
)
204+
data["plugins_disabled"] = self._get_disabled_public_plugins()
205+
185206
# Alias in addition MUST be camelcase here
186207
return {snake_to_camel(k): v for k, v in data.items()}
187208

@@ -317,9 +338,9 @@ def convert_to_app_config(app_settings: ApplicationSettings) -> Dict[str, Any]:
317338
),
318339
},
319340
"clusters": {"enabled": True},
320-
"computation": {"enabled": app_settings.is_enabled("COMPUTATION")},
321-
"diagnostics": {"enabled": app_settings.is_enabled("DIAGNOSTICS")},
322-
"director-v2": {"enabled": app_settings.is_enabled("DIRECTOR_V2")},
341+
"computation": {"enabled": app_settings.is_enabled("WEBSERVER_COMPUTATION")},
342+
"diagnostics": {"enabled": app_settings.is_enabled("WEBSERVER_DIAGNOSTICS")},
343+
"director-v2": {"enabled": app_settings.is_enabled("WEBSERVER_DIRECTOR_V2")},
323344
"exporter": {"enabled": app_settings.WEBSERVER_EXPORTER is not None},
324345
"groups": {"enabled": True},
325346
"meta_modeling": {"enabled": True},

services/web/server/src/simcore_service_webserver/exporter/file_downloader.py

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,10 @@ async def append_file(self, link: str, download_path: Path) -> None:
2828
async def download_files(self, app: Application) -> None:
2929
"""starts the download and waits for all files to finish"""
3030
exporter_settings = get_settings(app)
31+
assert ( # nosec
32+
exporter_settings is not None
33+
), "this call was not expected with a disabled plugin" # nosec
34+
3135
results = await self.downloader.run_download(
3236
timeouts={
3337
"total": exporter_settings.EXPORTER_DOWNLOADER_MAX_TIMEOUT_SECONDS,

services/web/server/src/simcore_service_webserver/exporter/module_setup.py

Lines changed: 16 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,14 +1,19 @@
11
import logging
22

33
from aiohttp import web
4-
from servicelib.aiohttp.application_setup import ModuleCategory, app_module_setup
4+
from servicelib.aiohttp.application_setup import (
5+
ModuleCategory,
6+
SkipModuleSetup,
7+
app_module_setup,
8+
)
59
from servicelib.aiohttp.rest_routing import (
610
iter_path_operations,
711
map_handlers_with_operations,
812
)
913

1014
from .._constants import APP_OPENAPI_SPECS_KEY
1115
from .request_handlers import rest_handler_functions
16+
from .settings import get_settings
1217

1318
logger = logging.getLogger(__name__)
1419

@@ -20,6 +25,16 @@
2025
)
2126
def setup_exporter(app: web.Application) -> bool:
2227

28+
# TODO: Implements temporary plugin disabling mechanims until new settings are fully integrated in servicelib.aiohttp.app_module_setup
29+
try:
30+
if get_settings(app) is None:
31+
raise SkipModuleSetup(
32+
reason="{__name__} plugin was explictly disabled in the app settings"
33+
)
34+
except KeyError as err:
35+
# This will happen if app[APP_SETTINGS_KEY] raises
36+
raise SkipModuleSetup(reason="{__name__} plugin settings undefined") from err
37+
2338
# Rest-API routes: maps handlers with routes tags with "viewer" based on OAS operation_id
2439
specs = app[APP_OPENAPI_SPECS_KEY]
2540
rest_routes = map_handlers_with_operations(

services/web/server/src/simcore_service_webserver/exporter/request_handlers.py

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -88,6 +88,10 @@ async def import_project(request: web.Request):
8888
# bumping this requests's max size
8989
# pylint: disable=protected-access
9090
exporter_settings = get_settings(request.app)
91+
assert ( # nosec
92+
exporter_settings is not None
93+
), "this call was not expected with a disabled plugin" # nosec
94+
9195
request._client_max_size = exporter_settings.EXPORTER_MAX_UPLOAD_FILE_SIZE * ONE_GB
9296

9397
post_contents = await request.post()

services/web/server/src/simcore_service_webserver/exporter/settings.py

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,5 @@
1+
from typing import Optional
2+
13
from aiohttp.web import Application
24
from pydantic import Field, PositiveInt
35
from servicelib.aiohttp.application_keys import APP_SETTINGS_KEY
@@ -23,7 +25,6 @@ class ExporterSettings(BaseCustomSettings):
2325
)
2426

2527

26-
def get_settings(app: Application) -> ExporterSettings:
28+
def get_settings(app: Application) -> Optional[ExporterSettings]:
2729
settings = app[APP_SETTINGS_KEY].WEBSERVER_EXPORTER
28-
assert settings # nosec
2930
return settings

services/web/server/tests/integration/01/test_exporter.py

Lines changed: 11 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -67,6 +67,9 @@
6767
from simcore_service_webserver.db_models import projects
6868
from simcore_service_webserver.exporter.async_hashing import Algorithm, checksum
6969
from simcore_service_webserver.exporter.file_downloader import ParallelDownloader
70+
from simcore_service_webserver.exporter.settings import (
71+
get_settings as get_exporter_settings,
72+
)
7073
from simcore_service_webserver.scicrunch.submodule_setup import (
7174
setup_scicrunch_submodule,
7275
)
@@ -141,19 +144,24 @@ def client(
141144
mock_orphaned_services: mock.Mock,
142145
monkeypatch_setenv_from_app_config: Callable,
143146
):
147+
# test config & env vars ----------------------
144148
cfg = deepcopy(app_config)
145-
146149
assert cfg["rest"]["version"] == API_VERSION
147150
assert cfg["rest"]["enabled"]
151+
148152
cfg["projects"]["enabled"] = True
149153
cfg["director"]["enabled"] = True
154+
cfg["exporter"]["enabled"] = True
150155

151-
# fake config
152156
monkeypatch_setenv_from_app_config(cfg)
157+
158+
# app setup ----------------------------------
153159
app = create_safe_application(cfg)
154160

155161
# activates only security+restAPI sub-modules
156162
setup_settings(app)
163+
assert get_exporter_settings(app) is not None, "Should capture defaults"
164+
157165
setup_db(app)
158166
setup_session(app)
159167
setup_security(app)
@@ -164,7 +172,7 @@ def client(
164172
setup_projects(app)
165173
setup_director(app)
166174
setup_director_v2(app)
167-
setup_exporter(app)
175+
setup_exporter(app) # <---- under test
168176
setup_storage(app)
169177
setup_products(app)
170178
setup_catalog(app)

services/web/server/tests/unit/isolated/test_application_settings.py

Lines changed: 18 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -208,7 +208,10 @@ def test_settings_constructs(app_settings: ApplicationSettings):
208208

209209

210210
def test_settings_to_client_statics(app_settings: ApplicationSettings):
211+
211212
statics = app_settings.to_client_statics()
213+
# can jsonify
214+
print(json.dumps(statics, indent=1))
212215

213216
# all key in camelcase
214217
assert all(
@@ -218,9 +221,22 @@ def test_settings_to_client_statics(app_settings: ApplicationSettings):
218221

219222
# special alias
220223
assert statics["stackName"] == "master-simcore"
224+
assert not statics["pluginsDisabled"]
221225

222-
# can jsonify
223-
print(json.dumps(statics))
226+
227+
def test_settings_to_client_statics_plugins(
228+
mock_webserver_service_environment, monkeypatch
229+
):
230+
disable_plugins = {"WEBSERVER_EXPORTER", "WEBSERVER_SCICRUNCH"}
231+
for name in disable_plugins:
232+
monkeypatch.setenv(name, "null")
233+
234+
settings = ApplicationSettings()
235+
statics = settings.to_client_statics()
236+
237+
print(json.dumps(statics, indent=1))
238+
239+
assert set(statics["pluginsDisabled"]) == disable_plugins
224240

225241

226242
def test_avoid_sensitive_info_in_public(app_settings: ApplicationSettings):

0 commit comments

Comments
 (0)