This is an automated email from the ASF dual-hosted git repository.

pierrejeambrun pushed a commit to branch main
in repository https://gitbox.apache.org/repos/asf/airflow.git


The following commit(s) were added to refs/heads/main by this push:
     new f1e74407152 Don't fail API server startup when a plugin uses an 
unknown team (#69976)
f1e74407152 is described below

commit f1e74407152456cf174a864da0011eb79b14a9b7
Author: Pierre Jeambrun <[email protected]>
AuthorDate: Thu Jul 16 21:13:15 2026 +0200

    Don't fail API server startup when a plugin uses an unknown team (#69976)
    
    Team validation for plugins raised during app creation when any plugin 
referenced
    a team that does not exist, so a single misconfigured plugin stopped the 
whole API
    server — and every other plugin — from booting. A misconfigured plugin 
should be
    surfaced, not fatal, the same way other plugin load errors are.
    
    The team check now records each offending plugin as a plugin import error 
(surfaced
    in the plugins API and UI) and logs a warning instead of raising, and runs 
from
    get_fastapi_plugins() so it happens automatically wherever plugins are 
mounted.
    
    Follow-up of #69502.
---
 airflow-core/src/airflow/api_fastapi/app.py        |  3 --
 airflow-core/src/airflow/plugins_manager.py        | 36 +++++++++++-----------
 .../tests/unit/plugins/test_plugins_manager.py     | 17 +++++++---
 3 files changed, 30 insertions(+), 26 deletions(-)

diff --git a/airflow-core/src/airflow/api_fastapi/app.py 
b/airflow-core/src/airflow/api_fastapi/app.py
index 6e6bca84389..a4dbc3c9b72 100644
--- a/airflow-core/src/airflow/api_fastapi/app.py
+++ b/airflow-core/src/airflow/api_fastapi/app.py
@@ -132,11 +132,8 @@ def create_app(apps: str = "all") -> FastAPI:
         app.mount("/execution", task_exec_api_app)
 
     if "all" in apps_list or "core" in apps_list:
-        from airflow import plugins_manager
-
         app.state.dag_bag = dag_bag
         init_plugins(app)
-        plugins_manager.validate_plugin_teams()
         init_auth_manager(app)
         init_flask_plugins(app)
         init_views(app)  # Core views need to be the last routes added - it 
has a catch all route
diff --git a/airflow-core/src/airflow/plugins_manager.py 
b/airflow-core/src/airflow/plugins_manager.py
index f747e8d37f9..8a8ece94370 100644
--- a/airflow-core/src/airflow/plugins_manager.py
+++ b/airflow-core/src/airflow/plugins_manager.py
@@ -233,6 +233,10 @@ def get_fastapi_plugins() -> tuple[list[Any], list[Any]]:
     """Collect extension points for the API."""
     log.debug("Initialize FastAPI plugins")
 
+    # Validate here (the API-server, DB-available path) so callers cannot mount
+    # plugins without the team check running.
+    validate_plugin_teams()
+
     fastapi_apps: list[Any] = []
     fastapi_root_middlewares: list[Any] = []
     for plugin in _get_plugins()[0]:
@@ -432,30 +436,26 @@ def validate_plugin_teams() -> None:
     metadata database access (the API server) — never in the Dag processor, 
triggerer,
     or workers, which reach the database only through the Execution API.
 
-    Raises ``AirflowConfigException`` if any plugin declares a ``team_name`` 
that is not
-    present in the database, naming the offending plugins and unknown teams so 
the
-    misconfiguration can be fixed quickly.
+    A plugin that declares a ``team_name`` not present in the database is 
recorded as a
+    plugin import error (surfaced like any other plugin load failure) and 
logged, rather
+    than raising, so a single misconfigured plugin does not stop the API 
server and every
+    other plugin from starting.
     """
     if not conf.getboolean("core", "multi_team"):
         return
 
-    from airflow.exceptions import AirflowConfigException
     from airflow.models.team import Team
 
+    plugins, import_errors = _get_plugins()
     known_teams = Team.get_all_team_names()
-    unknown_by_plugin = {
-        plugin.name: plugin.team_name
-        for plugin in _get_plugins()[0]
-        if plugin.team_name is not None and plugin.team_name not in known_teams
-    }
-    if unknown_by_plugin:
-        lines = [
-            f"  - Plugin '{plugin_name}' is assigned to team '{team_name}', 
which does not exist."
-            for plugin_name, team_name in unknown_by_plugin.items()
-        ]
-        raise AirflowConfigException(
-            "Some plugins are assigned to teams that do not exist:\n"
-            + "\n".join(lines)
-            + "\n\nCreate a team with `airflow teams create <team_name>`, "
+    for plugin in plugins:
+        if plugin.team_name is None or plugin.team_name in known_teams:
+            continue
+        message = (
+            f"Plugin '{plugin.name}' is assigned to team '{plugin.team_name}', 
which does not exist. "
+            "Create a team with `airflow teams create <team_name>`, "
             "or update the plugin to use an existing team."
         )
+        log.warning(message)
+        source = str(plugin.source) if plugin.source else plugin.name or ""
+        import_errors[source] = message
diff --git a/airflow-core/tests/unit/plugins/test_plugins_manager.py 
b/airflow-core/tests/unit/plugins/test_plugins_manager.py
index cccb8d4228b..39f924e1f99 100644
--- a/airflow-core/tests/unit/plugins/test_plugins_manager.py
+++ b/airflow-core/tests/unit/plugins/test_plugins_manager.py
@@ -490,14 +490,21 @@ class TestValidatePluginTeams:
 
     @conf_vars({("core", "multi_team"): "True"})
     @mock.patch("airflow.models.team.Team.get_all_team_names", 
return_value={"team_a"})
-    def test_raises_for_unknown_team(self, mock_get_all_team_names):
+    def test_get_fastapi_plugins_records_unknown_team_import_error(self, 
mock_get_all_team_names, caplog):
         from airflow import plugins_manager
-        from airflow.exceptions import AirflowConfigException
 
         class TeamPlugin(AirflowPlugin):
             name = "team_plugin"
             team_name = "unknown_team"
 
-        with mock_plugin_manager(plugins=[TeamPlugin()]):
-            with pytest.raises(AirflowConfigException, 
match="team_plugin.*unknown_team"):
-                plugins_manager.validate_plugin_teams()
+        # get_fastapi_plugins() is what init_plugins() calls, so validation 
runs
+        # automatically: a plugin on a nonexistent team is recorded as an 
import error
+        # and warned, not raised, so the API server and every other plugin 
still start.
+        with mock_plugin_manager(plugins=[TeamPlugin()], import_errors={}):
+            with caplog.at_level(logging.WARNING, 
logger="airflow.plugins_manager"):
+                plugins_manager.get_fastapi_plugins()
+            recorded = plugins_manager.get_import_errors()
+
+        assert "unknown_team" in recorded["team_plugin"]
+        warnings = [msg for _, level, msg in caplog.record_tuples if level == 
logging.WARNING]
+        assert any("team_plugin" in msg and "unknown_team" in msg for msg in 
warnings)

Reply via email to