This is an automated email from the ASF dual-hosted git repository.
ephraimanierobi 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 474fa4ddfd REST API: Fix wrong plugin schema (#34858)
474fa4ddfd is described below
commit 474fa4ddfda231f336a2ae7b43fcbd3e349be5c9
Author: Ephraim Anierobi <[email protected]>
AuthorDate: Thu Oct 12 12:56:42 2023 +0100
REST API: Fix wrong plugin schema (#34858)
* REST API: Fix wrong plugin schema
We serialize some plugin's fields as dictionaries leading to errors
when accessing the `/plugins` endpoint.
Here's the error:
ValueError: dictionary update sequence element #0 has length 1; 2 is
required
The fields are lists of strings and this PR addresses it.
* fixup! REST API: Fix wrong plugin schema
* Add test at the endpoint
* fixup! Add test at the endpoint
---
airflow/api_connexion/openapi/v1.yaml | 8 +-
airflow/api_connexion/schemas/plugin_schema.py | 8 +-
airflow/www/static/js/types/api-generated.ts | 8 +-
.../endpoints/test_plugin_endpoint.py | 64 +++++++++++++--
tests/api_connexion/schemas/test_plugin_schema.py | 90 ++++++++++++++++------
5 files changed, 135 insertions(+), 43 deletions(-)
diff --git a/airflow/api_connexion/openapi/v1.yaml
b/airflow/api_connexion/openapi/v1.yaml
index 48c9e5e023..b3194f3881 100644
--- a/airflow/api_connexion/openapi/v1.yaml
+++ b/airflow/api_connexion/openapi/v1.yaml
@@ -3776,13 +3776,13 @@ components:
macros:
type: array
items:
- type: object
+ type: string
nullable: true
description: The plugin macros
flask_blueprints:
type: array
items:
- type: object
+ type: string
nullable: true
description: The flask blueprints
appbuilder_views:
@@ -3800,13 +3800,13 @@ components:
global_operator_extra_links:
type: array
items:
- type: object
+ type: string
nullable: true
description: The global operator extra links
operator_extra_links:
type: array
items:
- type: object
+ type: string
nullable: true
description: Operator extra links
source:
diff --git a/airflow/api_connexion/schemas/plugin_schema.py
b/airflow/api_connexion/schemas/plugin_schema.py
index 780fef17bf..4b62111482 100644
--- a/airflow/api_connexion/schemas/plugin_schema.py
+++ b/airflow/api_connexion/schemas/plugin_schema.py
@@ -27,12 +27,12 @@ class PluginSchema(Schema):
name = fields.String()
hooks = fields.List(fields.String())
executors = fields.List(fields.String())
- macros = fields.List(fields.Dict())
- flask_blueprints = fields.List(fields.Dict())
+ macros = fields.List(fields.String())
+ flask_blueprints = fields.List(fields.String())
appbuilder_views = fields.List(fields.Dict())
appbuilder_menu_items = fields.List(fields.Dict())
- global_operator_extra_links = fields.List(fields.Dict())
- operator_extra_links = fields.List(fields.Dict())
+ global_operator_extra_links = fields.List(fields.String())
+ operator_extra_links = fields.List(fields.String())
source = fields.String()
diff --git a/airflow/www/static/js/types/api-generated.ts
b/airflow/www/static/js/types/api-generated.ts
index ec10084385..99e8754c56 100644
--- a/airflow/www/static/js/types/api-generated.ts
+++ b/airflow/www/static/js/types/api-generated.ts
@@ -1575,17 +1575,17 @@ export interface components {
/** @description The plugin executors */
executors?: (string | null)[];
/** @description The plugin macros */
- macros?: ({ [key: string]: unknown } | null)[];
+ macros?: (string | null)[];
/** @description The flask blueprints */
- flask_blueprints?: ({ [key: string]: unknown } | null)[];
+ flask_blueprints?: (string | null)[];
/** @description The appuilder views */
appbuilder_views?: ({ [key: string]: unknown } | null)[];
/** @description The Flask Appbuilder menu items */
appbuilder_menu_items?: ({ [key: string]: unknown } | null)[];
/** @description The global operator extra links */
- global_operator_extra_links?: ({ [key: string]: unknown } | null)[];
+ global_operator_extra_links?: (string | null)[];
/** @description Operator extra links */
- operator_extra_links?: ({ [key: string]: unknown } | null)[];
+ operator_extra_links?: (string | null)[];
/** @description The plugin source */
source?: string | null;
};
diff --git a/tests/api_connexion/endpoints/test_plugin_endpoint.py
b/tests/api_connexion/endpoints/test_plugin_endpoint.py
index 26a4ea0aed..a6f67ab5a7 100644
--- a/tests/api_connexion/endpoints/test_plugin_endpoint.py
+++ b/tests/api_connexion/endpoints/test_plugin_endpoint.py
@@ -17,14 +17,60 @@
from __future__ import annotations
import pytest
+from flask import Blueprint
+from flask_appbuilder import BaseView
+from airflow.hooks.base import BaseHook
+from airflow.models.baseoperator import BaseOperatorLink
from airflow.plugins_manager import AirflowPlugin
from airflow.security import permissions
+from airflow.utils.module_loading import qualname
from tests.test_utils.api_connexion_utils import assert_401, create_user,
delete_user
from tests.test_utils.config import conf_vars
from tests.test_utils.mock_plugins import mock_plugin_manager
+class PluginHook(BaseHook):
+ ...
+
+
+def plugin_macro():
+ ...
+
+
+class MockOperatorLink(BaseOperatorLink):
+ name = "mock_operator_link"
+
+ def get_link(self, operator, *, ti_key) -> str:
+ return "mock_operator_link"
+
+
+bp = Blueprint("mock_blueprint", __name__, url_prefix="/mock_blueprint")
+
+
+class MockView(BaseView):
+ ...
+
+
+mockview = MockView()
+
+appbuilder_menu_items = {
+ "name": "mock_plugin",
+ "href": "https://example.com",
+}
+
+
+class MockPlugin(AirflowPlugin):
+ name = "mock_plugin"
+ flask_blueprints = [bp]
+ appbuilder_views = [{"view": mockview}]
+ appbuilder_menu_items = [appbuilder_menu_items]
+ global_operator_extra_links = [MockOperatorLink()]
+ operator_extra_links = [MockOperatorLink()]
+ hooks = [PluginHook]
+ macros = [plugin_macro]
+
+
@pytest.fixture(scope="module")
def configured_app(minimal_app_for_api):
app = minimal_app_for_api
@@ -54,7 +100,7 @@ class TestPluginsEndpoint:
class TestGetPlugins(TestPluginsEndpoint):
def test_get_plugins_return_200(self):
- mock_plugin = AirflowPlugin()
+ mock_plugin = MockPlugin()
mock_plugin.name = "test_plugin"
with mock_plugin_manager(plugins=[mock_plugin]):
response = self.client.get("api/v1/plugins",
environ_overrides={"REMOTE_USER": "test"})
@@ -62,14 +108,16 @@ class TestGetPlugins(TestPluginsEndpoint):
assert response.json == {
"plugins": [
{
- "appbuilder_menu_items": [],
- "appbuilder_views": [],
+ "appbuilder_menu_items": [appbuilder_menu_items],
+ "appbuilder_views": [{"view": qualname(MockView)}],
"executors": [],
- "flask_blueprints": [],
- "global_operator_extra_links": [],
- "hooks": [],
- "macros": [],
- "operator_extra_links": [],
+ "flask_blueprints": [
+ f"<{qualname(bp.__class__)}: name={bp.name!r}
import_name={bp.import_name!r}>"
+ ],
+ "global_operator_extra_links":
[f"<{qualname(MockOperatorLink().__class__)} object>"],
+ "hooks": [qualname(PluginHook)],
+ "macros": [qualname(plugin_macro)],
+ "operator_extra_links":
[f"<{qualname(MockOperatorLink().__class__)} object>"],
"source": None,
"name": "test_plugin",
}
diff --git a/tests/api_connexion/schemas/test_plugin_schema.py
b/tests/api_connexion/schemas/test_plugin_schema.py
index 2366fe7ea3..179a318fe5 100644
--- a/tests/api_connexion/schemas/test_plugin_schema.py
+++ b/tests/api_connexion/schemas/test_plugin_schema.py
@@ -16,20 +16,64 @@
# under the License.
from __future__ import annotations
+from flask import Blueprint
+from flask_appbuilder import BaseView
+
from airflow.api_connexion.schemas.plugin_schema import (
PluginCollection,
plugin_collection_schema,
plugin_schema,
)
+from airflow.hooks.base import BaseHook
+from airflow.models.baseoperator import BaseOperatorLink
from airflow.plugins_manager import AirflowPlugin
+class PluginHook(BaseHook):
+ ...
+
+
+def plugin_macro():
+ ...
+
+
+class MockOperatorLink(BaseOperatorLink):
+ name = "mock_operator_link"
+
+ def get_link(self, operator, *, ti_key) -> str:
+ return "mock_operator_link"
+
+
+bp = Blueprint("mock_blueprint", __name__, url_prefix="/mock_blueprint")
+
+
+class MockView(BaseView):
+ ...
+
+
+appbuilder_menu_items = {
+ "name": "mock_plugin",
+ "href": "https://example.com",
+}
+
+
+class MockPlugin(AirflowPlugin):
+ name = "mock_plugin"
+ flask_blueprints = [bp]
+ appbuilder_views = [{"view": MockView()}]
+ appbuilder_menu_items = [appbuilder_menu_items]
+ global_operator_extra_links = [MockOperatorLink()]
+ operator_extra_links = [MockOperatorLink()]
+ hooks = [PluginHook]
+ macros = [plugin_macro]
+
+
class TestPluginBase:
def setup_method(self) -> None:
- self.mock_plugin = AirflowPlugin()
+ self.mock_plugin = MockPlugin()
self.mock_plugin.name = "test_plugin"
- self.mock_plugin_2 = AirflowPlugin()
+ self.mock_plugin_2 = MockPlugin()
self.mock_plugin_2.name = "test_plugin_2"
@@ -37,14 +81,14 @@ class TestPluginSchema(TestPluginBase):
def test_serialize(self):
deserialized_plugin = plugin_schema.dump(self.mock_plugin)
assert deserialized_plugin == {
- "appbuilder_menu_items": [],
- "appbuilder_views": [],
+ "appbuilder_menu_items": [appbuilder_menu_items],
+ "appbuilder_views": [{"view":
self.mock_plugin.appbuilder_views[0]["view"]}],
"executors": [],
- "flask_blueprints": [],
- "global_operator_extra_links": [],
- "hooks": [],
- "macros": [],
- "operator_extra_links": [],
+ "flask_blueprints": [str(bp)],
+ "global_operator_extra_links": [str(MockOperatorLink())],
+ "hooks": [str(PluginHook)],
+ "macros": [str(plugin_macro)],
+ "operator_extra_links": [str(MockOperatorLink())],
"source": None,
"name": "test_plugin",
}
@@ -58,26 +102,26 @@ class TestPluginCollectionSchema(TestPluginBase):
assert deserialized == {
"plugins": [
{
- "appbuilder_menu_items": [],
- "appbuilder_views": [],
+ "appbuilder_menu_items": [appbuilder_menu_items],
+ "appbuilder_views": [{"view":
self.mock_plugin.appbuilder_views[0]["view"]}],
"executors": [],
- "flask_blueprints": [],
- "global_operator_extra_links": [],
- "hooks": [],
- "macros": [],
- "operator_extra_links": [],
+ "flask_blueprints": [str(bp)],
+ "global_operator_extra_links": [str(MockOperatorLink())],
+ "hooks": [str(PluginHook)],
+ "macros": [str(plugin_macro)],
+ "operator_extra_links": [str(MockOperatorLink())],
"source": None,
"name": "test_plugin",
},
{
- "appbuilder_menu_items": [],
- "appbuilder_views": [],
+ "appbuilder_menu_items": [appbuilder_menu_items],
+ "appbuilder_views": [{"view":
self.mock_plugin.appbuilder_views[0]["view"]}],
"executors": [],
- "flask_blueprints": [],
- "global_operator_extra_links": [],
- "hooks": [],
- "macros": [],
- "operator_extra_links": [],
+ "flask_blueprints": [str(bp)],
+ "global_operator_extra_links": [str(MockOperatorLink())],
+ "hooks": [str(PluginHook)],
+ "macros": [str(plugin_macro)],
+ "operator_extra_links": [str(MockOperatorLink())],
"source": None,
"name": "test_plugin_2",
},