Skip to content

Commit 0a047bd

Browse files
AIP-68 Add loading validation to React App and External Views (#52972)
* AIP-68 Add loading validation to React App and External Views * Small adjustment
1 parent d8a3ad2 commit 0a047bd

File tree

4 files changed

+87
-2
lines changed

4 files changed

+87
-2
lines changed

airflow-core/src/airflow/api_fastapi/app.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,7 @@
3030
init_error_handlers,
3131
init_flask_plugins,
3232
init_middlewares,
33+
init_ui_plugins,
3334
init_views,
3435
)
3536
from airflow.api_fastapi.execution_api.app import create_task_execution_api_app
@@ -93,6 +94,7 @@ def create_app(apps: str = "all") -> FastAPI:
9394
init_plugins(app)
9495
init_auth_manager(app)
9596
init_flask_plugins(app)
97+
init_ui_plugins(app)
9698
init_views(app) # Core views need to be the last routes added - it has a catch all route
9799
init_error_handlers(app)
98100
init_middlewares(app)

airflow-core/src/airflow/api_fastapi/core_api/app.py

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -181,3 +181,10 @@ def init_middlewares(app: FastAPI) -> None:
181181
from airflow.api_fastapi.auth.managers.simple.middleware import SimpleAllAdminMiddleware
182182

183183
app.add_middleware(SimpleAllAdminMiddleware)
184+
185+
186+
def init_ui_plugins(app: FastAPI) -> None:
187+
"""Initialize UI plugins."""
188+
from airflow import plugins_manager
189+
190+
plugins_manager.initialize_ui_plugins()

airflow-core/src/airflow/plugins_manager.py

Lines changed: 36 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -387,12 +387,46 @@ def initialize_ui_plugins():
387387

388388
log.debug("Initialize UI plugin")
389389

390+
seen_url_route = {}
390391
external_views = []
391392
react_apps = []
392393

393394
for plugin in plugins:
394-
external_views.extend(plugin.external_views)
395-
react_apps.extend(plugin.react_apps)
395+
for external_view in plugin.external_views:
396+
url_route = external_view["url_route"]
397+
if url_route is not None and url_route in seen_url_route:
398+
log.warning(
399+
"Plugin '%s' has an external view with an URL route '%s' "
400+
"that conflicts with another plugin '%s'. The view will not be loaded.",
401+
plugin.name,
402+
url_route,
403+
seen_url_route[url_route],
404+
)
405+
# Mutate in place the plugin's external views to remove the conflicting view
406+
# because some function still access the plugin's external views and not the
407+
# global `external_views` variable. (get_plugin_info, for example)
408+
plugin.external_views.remove(external_view)
409+
continue
410+
external_views.append(external_view)
411+
seen_url_route[url_route] = plugin.name
412+
413+
for react_app in plugin.react_apps:
414+
url_route = react_app["url_route"]
415+
if url_route is not None and url_route in seen_url_route:
416+
log.warning(
417+
"Plugin '%s' has a React App with an URL route '%s' "
418+
"that conflicts with another plugin '%s'. The React App will not be loaded.",
419+
plugin.name,
420+
url_route,
421+
seen_url_route[url_route],
422+
)
423+
# Mutate in place the plugin's React Apps to remove the conflicting app
424+
# because some function still access the plugin's React Apps and not the
425+
# global `react_apps` variable. (get_plugin_info, for example)
426+
plugin.react_apps.remove(react_app)
427+
continue
428+
react_apps.append(react_app)
429+
seen_url_route[url_route] = plugin.name
396430

397431

398432
def initialize_flask_plugins():

airflow-core/tests/unit/plugins/test_plugins_manager.py

Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -151,6 +151,48 @@ class AirflowAdminMenuLinksPlugin(AirflowPlugin):
151151
),
152152
]
153153

154+
def test_should_warning_about_conflicting_url_route(self, caplog):
155+
class TestPluginA(AirflowPlugin):
156+
name = "test_plugin_a"
157+
158+
external_views = [{"url_route": "/test_route"}]
159+
160+
class TestPluginB(AirflowPlugin):
161+
name = "test_plugin_b"
162+
163+
external_views = [{"url_route": "/test_route"}]
164+
react_apps = [{"url_route": "/test_route"}]
165+
166+
with (
167+
mock_plugin_manager(plugins=[TestPluginA(), TestPluginB()]),
168+
caplog.at_level(logging.WARNING, logger="airflow.plugins_manager"),
169+
):
170+
from airflow import plugins_manager
171+
172+
plugins_manager.initialize_ui_plugins()
173+
174+
# Verify that the conflicting external view and react app are not loaded
175+
plugin_b = next(plugin for plugin in plugins_manager.plugins if plugin.name == "test_plugin_b")
176+
assert plugin_b.external_views == []
177+
assert plugin_b.react_apps == []
178+
assert len(plugins_manager.external_views) == 1
179+
assert len(plugins_manager.react_apps) == 0
180+
181+
assert caplog.record_tuples == [
182+
(
183+
"airflow.plugins_manager",
184+
logging.WARNING,
185+
"Plugin 'test_plugin_b' has an external view with an URL route '/test_route' "
186+
"that conflicts with another plugin 'test_plugin_a'. The view will not be loaded.",
187+
),
188+
(
189+
"airflow.plugins_manager",
190+
logging.WARNING,
191+
"Plugin 'test_plugin_b' has a React App with an URL route '/test_route' "
192+
"that conflicts with another plugin 'test_plugin_a'. The React App will not be loaded.",
193+
),
194+
]
195+
154196
def test_should_not_warning_about_fab_plugins(self, caplog):
155197
class AirflowAdminViewsPlugin(AirflowPlugin):
156198
name = "test_admin_views_plugin"

0 commit comments

Comments
 (0)