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)