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

kaxil 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 2c2dcce930e Stop one malformed provider from breaking discovery for 
all providers (#72854)
2c2dcce930e is described below

commit 2c2dcce930e7d5e27bfd18efdd49be8598c867ff
Author: Kaxil Naik <[email protected]>
AuthorDate: Thu Sep 10 14:47:00 2026 +0100

    Stop one malformed provider from breaking discovery for all providers 
(#72854)
    
    `provider_schema_validator.validate()` was called unwrapped inside the 
entry-point
    loop in provider discovery, and nothing up either call chain caught it. A 
single
    installed provider whose metadata fails validation therefore took down the 
whole
    provider list rather than just its own entry: every provider after it in 
entry-point
    order was never registered, `initialize_providers_list()` raised, and 
because
    `provider_info_cache` does not memoise failures it raised again on every 
subsequent
    access. Both the scheduler/API-server path and the worker path in the Task 
SDK share
    this loop.
    
    Only `jsonschema.ValidationError` is caught. A `SchemaError` or 
`UnknownType` means
    the schema itself is broken rather than the provider, and swallowing those 
would
    report one "provider does not conform" warning per installed provider while 
naming
    the wrong component.
    
    `package-name` is not in the runtime schema's required list, so validation 
passing
    did not guarantee it was present and the subscript below could still raise 
an
    uncaught `KeyError`. That is now the same warn-and-skip. The package-name 
mismatch
    check stays fatal, since that is a packaging error rather than unusable 
metadata.
---
 .../providers_discovery/providers_discovery.py     |  27 +++-
 .../test_providers_discovery.py                    | 142 +++++++++++++++++++++
 2 files changed, 167 insertions(+), 2 deletions(-)

diff --git 
a/shared/providers_discovery/src/airflow_shared/providers_discovery/providers_discovery.py
 
b/shared/providers_discovery/src/airflow_shared/providers_discovery/providers_discovery.py
index 0242eb42f4d..bd486775e64 100644
--- 
a/shared/providers_discovery/src/airflow_shared/providers_discovery/providers_discovery.py
+++ 
b/shared/providers_discovery/src/airflow_shared/providers_discovery/providers_discovery.py
@@ -313,6 +313,8 @@ def discover_all_providers_from_packages(
     :param provider_dict: Dictionary to populate with discovered providers
     :param provider_schema_validator: JSON schema validator for provider info
     """
+    import jsonschema
+
     for entry_point, dist in entry_points_with_dist("apache_airflow_provider"):
         if not dist.metadata:
             continue
@@ -322,8 +324,29 @@ def discover_all_providers_from_packages(
         log.debug("Loading %s from package %s", entry_point, package_name)
         version = dist.version
         provider_info = entry_point.load()()
-        provider_schema_validator.validate(provider_info)
-        provider_info_package_name = provider_info["package-name"]
+        try:
+            provider_schema_validator.validate(provider_info)
+        except jsonschema.ValidationError:
+            # One provider's malformed metadata must not stop the others from 
loading. This
+            # used to escape discovery entirely, so every provider after it 
was skipped and
+            # initialize_providers_list() raised on this and every later call.
+            # Only ValidationError is caught: a SchemaError or UnknownType 
means the schema
+            # itself is broken, which must stay loud rather than be reported 
once per provider.
+            log.warning(
+                "The provider %s does not conform to the provider_info schema 
and has been skipped.",
+                package_name,
+                exc_info=True,
+            )
+            continue
+        # 'package-name' is not in the runtime schema's required list, so 
validation passing
+        # does not guarantee it is present.
+        provider_info_package_name = provider_info.get("package-name")
+        if not provider_info_package_name:
+            log.warning(
+                "The provider %s does not declare 'package-name' in its 
provider info and has been skipped.",
+                package_name,
+            )
+            continue
         if package_name != provider_info_package_name:
             raise ValueError(
                 f"The package '{package_name}' from packaging information "
diff --git 
a/shared/providers_discovery/tests/providers_discovery/test_providers_discovery.py
 
b/shared/providers_discovery/tests/providers_discovery/test_providers_discovery.py
index 3c9504cf809..6c5713b468e 100644
--- 
a/shared/providers_discovery/tests/providers_discovery/test_providers_discovery.py
+++ 
b/shared/providers_discovery/tests/providers_discovery/test_providers_discovery.py
@@ -20,6 +20,7 @@ from __future__ import annotations
 from email.message import Message
 from unittest.mock import Mock
 
+import jsonschema
 import pytest
 
 from airflow_shared.providers_discovery import (
@@ -144,3 +145,144 @@ def 
test_discover_all_providers_preserves_commas_in_project_url(monkeypatch):
     assert 
providers["apache-airflow-providers-test"].data["documentation-url"] == (
         "https://example.invalid/docs?query=a,b";
     )
+
+
+def _provider_entry(package_name, provider_info):
+    metadata = Message()
+    metadata["Name"] = package_name
+    dist = Mock(metadata=metadata, version="1.0.0")
+    entry_point = Mock()
+    entry_point.load.return_value = lambda: provider_info
+    return entry_point, dist
+
+
+def _valid_provider_info(package_name):
+    return {"package-name": package_name, "name": package_name, "description": 
package_name}
+
+
+class TestDiscoveryIsolatesABadProvider:
+    """
+    ``provider_schema_validator.validate()`` used to be called unwrapped, so 
one provider with
+    malformed metadata aborted discovery for every provider in the process: 
everything after it
+    in entry-point order was never registered, and 
``initialize_providers_list()`` raised on
+    that and every later call.
+
+    These drive the real schema validator rather than a mock, so they cannot 
be satisfied by
+    catching an exception type that jsonschema never raises.
+    """
+
+    # Mirrors the parts of provider_info.schema.json these tests depend on: 
'package-name' is
+    # not required, and `triggers` items are closed with additionalProperties: 
false. Defined
+    # here rather than loaded from the real schema because that load imports 
airflow, which
+    # this distribution's own test environment deliberately does not install.
+    SCHEMA = {
+        "type": "object",
+        "properties": {
+            "package-name": {"type": "string"},
+            "name": {"type": "string"},
+            "description": {"type": "string"},
+            "triggers": {
+                "type": "array",
+                "items": {
+                    "type": "object",
+                    "properties": {
+                        "integration-name": {"type": "string"},
+                        "python-modules": {"type": "array", "items": {"type": 
"string"}},
+                    },
+                    "additionalProperties": False,
+                },
+            },
+        },
+        "required": ["name", "description"],
+    }
+
+    @classmethod
+    def _real_validator(cls):
+        return jsonschema.validators.validator_for(cls.SCHEMA)(cls.SCHEMA)
+
+    @classmethod
+    def _discover(cls, entries):
+        monkeypatched = pytest.MonkeyPatch()
+        monkeypatched.setattr(providers_discovery_module, 
"entry_points_with_dist", lambda group: entries)
+        providers: dict = {}
+        try:
+            discover_all_providers_from_packages(providers, 
cls._real_validator())
+        finally:
+            monkeypatched.undo()
+        return providers
+
+    def 
test_the_fixture_schema_matches_the_assumptions_these_tests_rely_on(self):
+        validator = self._real_validator()
+        # 'package-name' absent still validates, which is what makes the guard 
below reachable.
+        validator.validate({"name": "n", "description": "d"})
+        with pytest.raises(jsonschema.ValidationError):
+            validator.validate(
+                {
+                    "name": "n",
+                    "description": "d",
+                    "triggers": [{"integration-name": "x", "unrecognised-key": 
"v"}],
+                }
+            )
+
+    def 
test_schema_invalid_provider_is_skipped_and_the_others_still_load(self):
+        # An unrecognised key inside a `triggers` item is one of the few 
substructures the
+        # runtime schema closes with additionalProperties: false, so this 
really is invalid.
+        bad_info = _valid_provider_info("apache-airflow-providers-bad")
+        bad_info["triggers"] = [{"integration-name": "x", "python-modules": 
["m"], "unrecognised-key": "v"}]
+
+        providers = self._discover(
+            [
+                _provider_entry(
+                    "apache-airflow-providers-first",
+                    _valid_provider_info("apache-airflow-providers-first"),
+                ),
+                _provider_entry("apache-airflow-providers-bad", bad_info),
+                _provider_entry(
+                    "apache-airflow-providers-last",
+                    _valid_provider_info("apache-airflow-providers-last"),
+                ),
+            ]
+        )
+
+        assert sorted(providers) == [
+            "apache-airflow-providers-first",
+            "apache-airflow-providers-last",
+        ]
+
+    def test_provider_without_a_package_name_is_skipped(self):
+        # 'package-name' is not in the runtime schema's required list, so this 
validates and
+        # would otherwise reach an unguarded subscript.
+        no_name = {"name": "nameless", "description": "nameless"}
+
+        providers = self._discover(
+            [
+                _provider_entry("apache-airflow-providers-nameless", no_name),
+                _provider_entry(
+                    "apache-airflow-providers-last",
+                    _valid_provider_info("apache-airflow-providers-last"),
+                ),
+            ]
+        )
+
+        assert sorted(providers) == ["apache-airflow-providers-last"]
+
+    def 
test_a_broken_schema_is_not_reported_as_every_provider_being_malformed(self):
+        # A validator-side defect must stay loud instead of being swallowed 
once per provider.
+        validator = Mock()
+        validator.validate.side_effect = 
jsonschema.exceptions.SchemaError("bad schema")
+        monkeypatched = pytest.MonkeyPatch()
+        monkeypatched.setattr(
+            providers_discovery_module,
+            "entry_points_with_dist",
+            lambda group: [
+                _provider_entry(
+                    "apache-airflow-providers-first",
+                    _valid_provider_info("apache-airflow-providers-first"),
+                )
+            ],
+        )
+        try:
+            with pytest.raises(jsonschema.exceptions.SchemaError):
+                discover_all_providers_from_packages({}, validator)
+        finally:
+            monkeypatched.undo()

Reply via email to