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 113528cbb20 AIP-84 Add plugin error endpoint (#49436)
113528cbb20 is described below
commit 113528cbb206bbf44d04282861ed96a789bcbfbd
Author: Pierre Jeambrun <[email protected]>
AuthorDate: Fri Apr 18 17:32:49 2025 +0200
AIP-84 Add plugin error endpoint (#49436)
---
.../api_fastapi/core_api/datamodels/plugins.py | 14 ++++++
.../core_api/openapi/v1-rest-api-generated.yaml | 57 ++++++++++++++++++++++
.../api_fastapi/core_api/routes/public/plugins.py | 27 ++++++++--
.../src/airflow/ui/openapi-gen/queries/common.ts | 10 ++++
.../ui/openapi-gen/queries/ensureQueryData.ts | 10 ++++
.../src/airflow/ui/openapi-gen/queries/prefetch.ts | 10 ++++
.../src/airflow/ui/openapi-gen/queries/queries.ts | 18 +++++++
.../src/airflow/ui/openapi-gen/queries/suspense.ts | 18 +++++++
.../airflow/ui/openapi-gen/requests/schemas.gen.ts | 37 ++++++++++++++
.../ui/openapi-gen/requests/services.gen.ts | 17 +++++++
.../airflow/ui/openapi-gen/requests/types.gen.ts | 36 ++++++++++++++
.../core_api/routes/public/test_plugins.py | 32 ++++++++++++
12 files changed, 283 insertions(+), 3 deletions(-)
diff --git
a/airflow-core/src/airflow/api_fastapi/core_api/datamodels/plugins.py
b/airflow-core/src/airflow/api_fastapi/core_api/datamodels/plugins.py
index 1aab230ac3d..ad62e7b840a 100644
--- a/airflow-core/src/airflow/api_fastapi/core_api/datamodels/plugins.py
+++ b/airflow-core/src/airflow/api_fastapi/core_api/datamodels/plugins.py
@@ -98,3 +98,17 @@ class PluginCollectionResponse(BaseModel):
plugins: list[PluginResponse]
total_entries: int
+
+
+class PluginImportErrorResponse(BaseModel):
+ """Plugin Import Error serializer for responses."""
+
+ source: str
+ error: str
+
+
+class PluginImportErrorCollectionResponse(BaseModel):
+ """Plugin Import Error Collection serializer."""
+
+ import_errors: list[PluginImportErrorResponse]
+ total_entries: int
diff --git
a/airflow-core/src/airflow/api_fastapi/core_api/openapi/v1-rest-api-generated.yaml
b/airflow-core/src/airflow/api_fastapi/core_api/openapi/v1-rest-api-generated.yaml
index 8e54659005b..c590ea0080b 100644
---
a/airflow-core/src/airflow/api_fastapi/core_api/openapi/v1-rest-api-generated.yaml
+++
b/airflow-core/src/airflow/api_fastapi/core_api/openapi/v1-rest-api-generated.yaml
@@ -3673,6 +3673,33 @@ paths:
application/json:
schema:
$ref: '#/components/schemas/HTTPValidationError'
+ /api/v2/plugins/importErrors:
+ get:
+ tags:
+ - Plugin
+ summary: Import Errors
+ operationId: import_errors
+ responses:
+ '200':
+ description: Successful Response
+ content:
+ application/json:
+ schema:
+ $ref:
'#/components/schemas/PluginImportErrorCollectionResponse'
+ '401':
+ description: Unauthorized
+ content:
+ application/json:
+ schema:
+ $ref: '#/components/schemas/HTTPExceptionResponse'
+ '403':
+ description: Forbidden
+ content:
+ application/json:
+ schema:
+ $ref: '#/components/schemas/HTTPExceptionResponse'
+ security:
+ - OAuth2PasswordBearer: []
/api/v2/pools/{pool_name}:
delete:
tags:
@@ -9043,6 +9070,36 @@ components:
- total_entries
title: PluginCollectionResponse
description: Plugin Collection serializer.
+ PluginImportErrorCollectionResponse:
+ properties:
+ import_errors:
+ items:
+ $ref: '#/components/schemas/PluginImportErrorResponse'
+ type: array
+ title: Import Errors
+ total_entries:
+ type: integer
+ title: Total Entries
+ type: object
+ required:
+ - import_errors
+ - total_entries
+ title: PluginImportErrorCollectionResponse
+ description: Plugin Import Error Collection serializer.
+ PluginImportErrorResponse:
+ properties:
+ source:
+ type: string
+ title: Source
+ error:
+ type: string
+ title: Error
+ type: object
+ required:
+ - source
+ - error
+ title: PluginImportErrorResponse
+ description: Plugin Import Error serializer for responses.
PluginResponse:
properties:
name:
diff --git
a/airflow-core/src/airflow/api_fastapi/core_api/routes/public/plugins.py
b/airflow-core/src/airflow/api_fastapi/core_api/routes/public/plugins.py
index 04ca85be433..fe700178f6b 100644
--- a/airflow-core/src/airflow/api_fastapi/core_api/routes/public/plugins.py
+++ b/airflow-core/src/airflow/api_fastapi/core_api/routes/public/plugins.py
@@ -21,12 +21,16 @@ from typing import cast
from fastapi import Depends
+from airflow import plugins_manager
from airflow.api_fastapi.auth.managers.models.resource_details import
AccessView
from airflow.api_fastapi.common.parameters import QueryLimit, QueryOffset
from airflow.api_fastapi.common.router import AirflowRouter
-from airflow.api_fastapi.core_api.datamodels.plugins import
PluginCollectionResponse, PluginResponse
+from airflow.api_fastapi.core_api.datamodels.plugins import (
+ PluginCollectionResponse,
+ PluginImportErrorCollectionResponse,
+ PluginResponse,
+)
from airflow.api_fastapi.core_api.security import requires_access_view
-from airflow.plugins_manager import get_plugin_info
plugins_router = AirflowRouter(tags=["Plugin"], prefix="/plugins")
@@ -39,8 +43,25 @@ def get_plugins(
limit: QueryLimit,
offset: QueryOffset,
) -> PluginCollectionResponse:
- plugins_info = sorted(get_plugin_info(), key=lambda x: x["name"])
+ plugins_info = sorted(plugins_manager.get_plugin_info(), key=lambda x:
x["name"])
return PluginCollectionResponse(
plugins=cast("list[PluginResponse]", plugins_info[offset.value :][:
limit.value]),
total_entries=len(plugins_info),
)
+
+
+@plugins_router.get(
+ "/importErrors",
+ dependencies=[Depends(requires_access_view(AccessView.PLUGINS))],
+)
+def import_errors() -> PluginImportErrorCollectionResponse:
+ plugins_manager.ensure_plugins_loaded() # make sure import_errors are
loaded
+
+ return PluginImportErrorCollectionResponse.model_validate(
+ {
+ "import_errors": [
+ {"source": source, "error": error} for source, error in
plugins_manager.import_errors.items()
+ ],
+ "total_entries": len(plugins_manager.import_errors),
+ }
+ )
diff --git a/airflow-core/src/airflow/ui/openapi-gen/queries/common.ts
b/airflow-core/src/airflow/ui/openapi-gen/queries/common.ts
index a3fcabb4c27..ec6efb8c19d 100644
--- a/airflow-core/src/airflow/ui/openapi-gen/queries/common.ts
+++ b/airflow-core/src/airflow/ui/openapi-gen/queries/common.ts
@@ -1334,6 +1334,16 @@ export const UsePluginServiceGetPluginsKeyFn = (
} = {},
queryKey?: Array<unknown>,
) => [usePluginServiceGetPluginsKey, ...(queryKey ?? [{ limit, offset }])];
+export type PluginServiceImportErrorsDefaultResponse =
Awaited<ReturnType<typeof PluginService.importErrors>>;
+export type PluginServiceImportErrorsQueryResult<
+ TData = PluginServiceImportErrorsDefaultResponse,
+ TError = unknown,
+> = UseQueryResult<TData, TError>;
+export const usePluginServiceImportErrorsKey = "PluginServiceImportErrors";
+export const UsePluginServiceImportErrorsKeyFn = (queryKey?: Array<unknown>)
=> [
+ usePluginServiceImportErrorsKey,
+ ...(queryKey ?? []),
+];
export type PoolServiceGetPoolDefaultResponse = Awaited<ReturnType<typeof
PoolService.getPool>>;
export type PoolServiceGetPoolQueryResult<
TData = PoolServiceGetPoolDefaultResponse,
diff --git a/airflow-core/src/airflow/ui/openapi-gen/queries/ensureQueryData.ts
b/airflow-core/src/airflow/ui/openapi-gen/queries/ensureQueryData.ts
index cd06c2bff30..29323892621 100644
--- a/airflow-core/src/airflow/ui/openapi-gen/queries/ensureQueryData.ts
+++ b/airflow-core/src/airflow/ui/openapi-gen/queries/ensureQueryData.ts
@@ -1845,6 +1845,16 @@ export const ensureUsePluginServiceGetPluginsData = (
queryKey: Common.UsePluginServiceGetPluginsKeyFn({ limit, offset }),
queryFn: () => PluginService.getPlugins({ limit, offset }),
});
+/**
+ * Import Errors
+ * @returns PluginImportErrorCollectionResponse Successful Response
+ * @throws ApiError
+ */
+export const ensureUsePluginServiceImportErrorsData = (queryClient:
QueryClient) =>
+ queryClient.ensureQueryData({
+ queryKey: Common.UsePluginServiceImportErrorsKeyFn(),
+ queryFn: () => PluginService.importErrors(),
+ });
/**
* Get Pool
* Get a pool.
diff --git a/airflow-core/src/airflow/ui/openapi-gen/queries/prefetch.ts
b/airflow-core/src/airflow/ui/openapi-gen/queries/prefetch.ts
index 14649a703b1..d9328eab70b 100644
--- a/airflow-core/src/airflow/ui/openapi-gen/queries/prefetch.ts
+++ b/airflow-core/src/airflow/ui/openapi-gen/queries/prefetch.ts
@@ -1845,6 +1845,16 @@ export const prefetchUsePluginServiceGetPlugins = (
queryKey: Common.UsePluginServiceGetPluginsKeyFn({ limit, offset }),
queryFn: () => PluginService.getPlugins({ limit, offset }),
});
+/**
+ * Import Errors
+ * @returns PluginImportErrorCollectionResponse Successful Response
+ * @throws ApiError
+ */
+export const prefetchUsePluginServiceImportErrors = (queryClient: QueryClient)
=>
+ queryClient.prefetchQuery({
+ queryKey: Common.UsePluginServiceImportErrorsKeyFn(),
+ queryFn: () => PluginService.importErrors(),
+ });
/**
* Get Pool
* Get a pool.
diff --git a/airflow-core/src/airflow/ui/openapi-gen/queries/queries.ts
b/airflow-core/src/airflow/ui/openapi-gen/queries/queries.ts
index ae20d5e3d76..c05706beec8 100644
--- a/airflow-core/src/airflow/ui/openapi-gen/queries/queries.ts
+++ b/airflow-core/src/airflow/ui/openapi-gen/queries/queries.ts
@@ -2193,6 +2193,24 @@ export const usePluginServiceGetPlugins = <
queryFn: () => PluginService.getPlugins({ limit, offset }) as TData,
...options,
});
+/**
+ * Import Errors
+ * @returns PluginImportErrorCollectionResponse Successful Response
+ * @throws ApiError
+ */
+export const usePluginServiceImportErrors = <
+ TData = Common.PluginServiceImportErrorsDefaultResponse,
+ TError = unknown,
+ TQueryKey extends Array<unknown> = unknown[],
+>(
+ queryKey?: TQueryKey,
+ options?: Omit<UseQueryOptions<TData, TError>, "queryKey" | "queryFn">,
+) =>
+ useQuery<TData, TError>({
+ queryKey: Common.UsePluginServiceImportErrorsKeyFn(queryKey),
+ queryFn: () => PluginService.importErrors() as TData,
+ ...options,
+ });
/**
* Get Pool
* Get a pool.
diff --git a/airflow-core/src/airflow/ui/openapi-gen/queries/suspense.ts
b/airflow-core/src/airflow/ui/openapi-gen/queries/suspense.ts
index 0bb328cbb24..3180bd87b93 100644
--- a/airflow-core/src/airflow/ui/openapi-gen/queries/suspense.ts
+++ b/airflow-core/src/airflow/ui/openapi-gen/queries/suspense.ts
@@ -2170,6 +2170,24 @@ export const usePluginServiceGetPluginsSuspense = <
queryFn: () => PluginService.getPlugins({ limit, offset }) as TData,
...options,
});
+/**
+ * Import Errors
+ * @returns PluginImportErrorCollectionResponse Successful Response
+ * @throws ApiError
+ */
+export const usePluginServiceImportErrorsSuspense = <
+ TData = Common.PluginServiceImportErrorsDefaultResponse,
+ TError = unknown,
+ TQueryKey extends Array<unknown> = unknown[],
+>(
+ queryKey?: TQueryKey,
+ options?: Omit<UseQueryOptions<TData, TError>, "queryKey" | "queryFn">,
+) =>
+ useSuspenseQuery<TData, TError>({
+ queryKey: Common.UsePluginServiceImportErrorsKeyFn(queryKey),
+ queryFn: () => PluginService.importErrors() as TData,
+ ...options,
+ });
/**
* Get Pool
* Get a pool.
diff --git a/airflow-core/src/airflow/ui/openapi-gen/requests/schemas.gen.ts
b/airflow-core/src/airflow/ui/openapi-gen/requests/schemas.gen.ts
index 7c9f8b2bf5f..142cc6db7b5 100644
--- a/airflow-core/src/airflow/ui/openapi-gen/requests/schemas.gen.ts
+++ b/airflow-core/src/airflow/ui/openapi-gen/requests/schemas.gen.ts
@@ -3450,6 +3450,43 @@ export const $PluginCollectionResponse = {
description: "Plugin Collection serializer.",
} as const;
+export const $PluginImportErrorCollectionResponse = {
+ properties: {
+ import_errors: {
+ items: {
+ $ref: "#/components/schemas/PluginImportErrorResponse",
+ },
+ type: "array",
+ title: "Import Errors",
+ },
+ total_entries: {
+ type: "integer",
+ title: "Total Entries",
+ },
+ },
+ type: "object",
+ required: ["import_errors", "total_entries"],
+ title: "PluginImportErrorCollectionResponse",
+ description: "Plugin Import Error Collection serializer.",
+} as const;
+
+export const $PluginImportErrorResponse = {
+ properties: {
+ source: {
+ type: "string",
+ title: "Source",
+ },
+ error: {
+ type: "string",
+ title: "Error",
+ },
+ },
+ type: "object",
+ required: ["source", "error"],
+ title: "PluginImportErrorResponse",
+ description: "Plugin Import Error serializer for responses.",
+} as const;
+
export const $PluginResponse = {
properties: {
name: {
diff --git a/airflow-core/src/airflow/ui/openapi-gen/requests/services.gen.ts
b/airflow-core/src/airflow/ui/openapi-gen/requests/services.gen.ts
index 385f8256e7f..b9c97a08128 100644
--- a/airflow-core/src/airflow/ui/openapi-gen/requests/services.gen.ts
+++ b/airflow-core/src/airflow/ui/openapi-gen/requests/services.gen.ts
@@ -154,6 +154,7 @@ import type {
GetJobsResponse,
GetPluginsData,
GetPluginsResponse,
+ ImportErrorsResponse,
DeletePoolData,
DeletePoolResponse,
GetPoolData,
@@ -2674,6 +2675,22 @@ export class PluginService {
},
});
}
+
+ /**
+ * Import Errors
+ * @returns PluginImportErrorCollectionResponse Successful Response
+ * @throws ApiError
+ */
+ public static importErrors(): CancelablePromise<ImportErrorsResponse> {
+ return __request(OpenAPI, {
+ method: "GET",
+ url: "/api/v2/plugins/importErrors",
+ errors: {
+ 401: "Unauthorized",
+ 403: "Forbidden",
+ },
+ });
+ }
}
export class PoolService {
diff --git a/airflow-core/src/airflow/ui/openapi-gen/requests/types.gen.ts
b/airflow-core/src/airflow/ui/openapi-gen/requests/types.gen.ts
index 18528b9da12..6f3d2f0147b 100644
--- a/airflow-core/src/airflow/ui/openapi-gen/requests/types.gen.ts
+++ b/airflow-core/src/airflow/ui/openapi-gen/requests/types.gen.ts
@@ -909,6 +909,22 @@ export type PluginCollectionResponse = {
total_entries: number;
};
+/**
+ * Plugin Import Error Collection serializer.
+ */
+export type PluginImportErrorCollectionResponse = {
+ import_errors: Array<PluginImportErrorResponse>;
+ total_entries: number;
+};
+
+/**
+ * Plugin Import Error serializer for responses.
+ */
+export type PluginImportErrorResponse = {
+ source: string;
+ error: string;
+};
+
/**
* Plugin serializer.
*/
@@ -2413,6 +2429,8 @@ export type GetPluginsData = {
export type GetPluginsResponse = PluginCollectionResponse;
+export type ImportErrorsResponse = PluginImportErrorCollectionResponse;
+
export type DeletePoolData = {
poolName: string;
};
@@ -4730,6 +4748,24 @@ export type $OpenApiTs = {
};
};
};
+ "/api/v2/plugins/importErrors": {
+ get: {
+ res: {
+ /**
+ * Successful Response
+ */
+ 200: PluginImportErrorCollectionResponse;
+ /**
+ * Unauthorized
+ */
+ 401: HTTPExceptionResponse;
+ /**
+ * Forbidden
+ */
+ 403: HTTPExceptionResponse;
+ };
+ };
+ };
"/api/v2/pools/{pool_name}": {
delete: {
req: DeletePoolData;
diff --git
a/airflow-core/tests/unit/api_fastapi/core_api/routes/public/test_plugins.py
b/airflow-core/tests/unit/api_fastapi/core_api/routes/public/test_plugins.py
index 392b7dcb07d..c16e1f114a9 100644
--- a/airflow-core/tests/unit/api_fastapi/core_api/routes/public/test_plugins.py
+++ b/airflow-core/tests/unit/api_fastapi/core_api/routes/public/test_plugins.py
@@ -16,6 +16,8 @@
# under the License.
from __future__ import annotations
+from unittest.mock import patch
+
import pytest
from tests_common.test_utils.markers import
skip_if_force_lowest_dependencies_marker
@@ -73,3 +75,33 @@ class TestGetPlugins:
def test_should_response_403(self, unauthorized_test_client):
response = unauthorized_test_client.get("/plugins")
assert response.status_code == 403
+
+
+@skip_if_force_lowest_dependencies_marker
+class TestGetPluginImportErrors:
+ @patch(
+ "airflow.plugins_manager.import_errors",
+ new={"plugins/test_plugin.py": "something went wrong"},
+ )
+ def test_should_respond_200(self, test_client, session):
+ response = test_client.get("/plugins/importErrors")
+ assert response.status_code == 200
+
+ body = response.json()
+ assert body == {
+ "import_errors": [
+ {
+ "source": "plugins/test_plugin.py",
+ "error": "something went wrong",
+ }
+ ],
+ "total_entries": 1,
+ }
+
+ def test_should_response_401(self, unauthenticated_test_client):
+ response = unauthenticated_test_client.get("/plugins/importErrors")
+ assert response.status_code == 401
+
+ def test_should_response_403(self, unauthorized_test_client):
+ response = unauthorized_test_client.get("/plugins/importErrors")
+ assert response.status_code == 403