Skip to content

Commit

Permalink
Add plugins endpoint to the REST API (#14280)
Browse files Browse the repository at this point in the history
* add plugins endpoint

* include access menu permission to plugin endpoint

* apply suggestions from code review

* fixup! apply suggestions from code review

* refactor get plugins info

* fixup! refactor get plugins info

* fixup! fixup! refactor get plugins info

* fixup! fixup! fixup! refactor get plugins info

* added plugin in summary of changes

* Remove menu access permission

* remove menu_links and admin_views and add limit and offset to endpoint

* fixup! remove menu_links and admin_views and add limit and offset to endpoint
  • Loading branch information
ephraimbuddy authored Feb 26, 2021
1 parent e05ba51 commit 31acf95
Show file tree
Hide file tree
Showing 7 changed files with 490 additions and 26 deletions.
32 changes: 32 additions & 0 deletions airflow/api_connexion/endpoints/plugin_endpoint.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
# Licensed to the Apache Software Foundation (ASF) under one
# or more contributor license agreements. See the NOTICE file
# distributed with this work for additional information
# regarding copyright ownership. The ASF licenses this file
# to you under the Apache License, Version 2.0 (the
# "License"); you may not use this file except in compliance
# with the License. You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing,
# software distributed under the License is distributed on an
# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
# KIND, either express or implied. See the License for the
# specific language governing permissions and limitations
# under the License.
from airflow.api_connexion import security
from airflow.api_connexion.parameters import check_limit, format_parameters
from airflow.api_connexion.schemas.plugin_schema import PluginCollection, plugin_collection_schema
from airflow.plugins_manager import get_plugin_info
from airflow.security import permissions


@security.requires_access([(permissions.ACTION_CAN_READ, permissions.RESOURCE_PLUGIN)])
@format_parameters({'limit': check_limit})
def get_plugins(limit, offset=0):
"""Get plugins endpoint"""
plugins_info = get_plugin_info()
total_entries = len(plugins_info)
plugins_info = plugins_info[offset:]
plugins_info = plugins_info[:limit]
return plugin_collection_schema.dump(PluginCollection(plugins=plugins_info, total_entries=total_entries))
100 changes: 99 additions & 1 deletion airflow/api_connexion/openapi/v1.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -132,6 +132,7 @@ info:
| Airflow version | Description |
|-|-|
| v2.0 | Initial release |
| v2.0.2 | Added /plugins endpoint |
# Trying the API
Expand Down Expand Up @@ -1380,7 +1381,28 @@ paths:
application/json:
schema:
$ref: '#/components/schemas/VersionInfo'

/plugins:
get:
summary: Get a list of loaded plugins
x-openapi-router-controller: airflow.api_connexion.endpoints.plugin_endpoint
operationId: get_plugins
tags: [Plugin]
parameters:
- $ref: '#/components/parameters/PageLimit'
- $ref: '#/components/parameters/PageOffset'
responses:
'200':
description: Success
content:
application/json:
schema:
$ref: '#/components/schemas/PluginCollection'
'401':
$ref: '#/components/responses/Unauthenticated'
'403':
$ref: '#/components/responses/PermissionDenied'
'404':
$ref: '#/components/responses/NotFound'

components:
# Reusable schemas (data models)
Expand Down Expand Up @@ -2064,6 +2086,81 @@ components:
type: array
items:
$ref: '#/components/schemas/Task'
# Plugin
PluginCollectionItem:
type: object
description: Plugin Item
properties:
number:
type: string
description: The plugin number
name:
type: string
description: The name of the plugin
hooks:
type: array
items:
type: string
nullable: true
description: The plugin hooks
executors:
type: array
items:
type: string
nullable: true
description: The plugin executors
macros:
type: array
items:
type: object
nullable: true
description: The plugin macros
flask_blueprints:
type: array
items:
type: object
nullable: true
description: The flask blueprints
appbuilder_views:
type: array
items:
type: object
nullable: true
description: The appuilder views
appbuilder_menu_items:
type: array
items:
type: object
nullable: true
description: The Flask Appbuilder menu items
global_operator_extra_links:
type: array
items:
type: object
nullable: true
description: The global operator extra links
operator_extra_links:
type: array
items:
type: object
nullable: true
description: Operator extra links
source:
type: string
description: The plugin source
nullable: true

PluginCollection:
type: object
description: Plugin Collection
allOf:
- type: object
properties:
plugins:
type: array
items:
$ref: '#/components/schemas/PluginCollectionItem'
- $ref: '#/components/schemas/CollectionInfo'

# Configuration
ConfigOption:
Expand Down Expand Up @@ -2910,6 +3007,7 @@ tags:
- name: TaskInstance
- name: Variable
- name: XCom
- name: Plugin


externalDocs:
Expand Down
54 changes: 54 additions & 0 deletions airflow/api_connexion/schemas/plugin_schema.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
# Licensed to the Apache Software Foundation (ASF) under one
# or more contributor license agreements. See the NOTICE file
# distributed with this work for additional information
# regarding copyright ownership. The ASF licenses this file
# to you under the Apache License, Version 2.0 (the
# "License"); you may not use this file except in compliance
# with the License. You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing,
# software distributed under the License is distributed on an
# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
# KIND, either express or implied. See the License for the
# specific language governing permissions and limitations
# under the License.

from typing import List, NamedTuple

from marshmallow import Schema, fields


class PluginSchema(Schema):
"""Plugin schema"""

number = fields.Int()
name = fields.String()
hooks = fields.List(fields.String())
executors = fields.List(fields.String())
macros = fields.List(fields.String())
flask_blueprints = fields.List(fields.String())
appbuilder_views = fields.List(fields.String())
appbuilder_menu_items = fields.List(fields.Dict())
global_operator_extra_links = fields.List(fields.String())
operator_extra_links = fields.List(fields.String())
source = fields.String()


class PluginCollection(NamedTuple):
"""Plugin List"""

plugins: List
total_entries: int


class PluginCollectionSchema(Schema):
"""Plugin Collection List"""

plugins = fields.List(fields.Nested(PluginSchema))
total_entries = fields.Int()


plugin_schema = PluginSchema()
plugin_collection_schema = PluginCollectionSchema()
27 changes: 2 additions & 25 deletions airflow/cli/commands/plugins_command.py
Original file line number Diff line number Diff line change
Expand Up @@ -19,22 +19,9 @@

from airflow import plugins_manager
from airflow.cli.simple_table import AirflowConsole
from airflow.plugins_manager import PluginsDirectorySource
from airflow.plugins_manager import PluginsDirectorySource, get_plugin_info
from airflow.utils.cli import suppress_logs_and_warning

# list to maintain the order of items.
PLUGINS_ATTRIBUTES_TO_DUMP = [
"source",
"hooks",
"executors",
"macros",
"flask_blueprints",
"appbuilder_views",
"appbuilder_menu_items",
"global_operator_extra_links",
"operator_extra_links",
]


def _get_name(class_like_object) -> str:
if isinstance(class_like_object, (str, PluginsDirectorySource)):
Expand All @@ -52,21 +39,11 @@ def _join_plugins_names(value: Union[List[Any], Any]) -> str:
@suppress_logs_and_warning
def dump_plugins(args):
"""Dump plugins information"""
plugins_manager.ensure_plugins_loaded()
plugins_manager.integrate_macros_plugins()
plugins_manager.integrate_executor_plugins()
plugins_manager.initialize_extra_operators_links_plugins()
plugins_manager.initialize_web_ui_plugins()
plugins_info: List[Dict[str, str]] = get_plugin_info()
if not plugins_manager.plugins:
print("No plugins loaded")
return

plugins_info: List[Dict[str, str]] = []
for plugin in plugins_manager.plugins:
info = {"name": plugin.name}
info.update({n: getattr(plugin, n) for n in PLUGINS_ATTRIBUTES_TO_DUMP})
plugins_info.append(info)

# Remove empty info
if args.output == "table": # pylint: disable=too-many-nested-blocks
# We can do plugins_info[0] as the element it will exist as there's
Expand Down
34 changes: 34 additions & 0 deletions airflow/plugins_manager.py
Original file line number Diff line number Diff line change
Expand Up @@ -63,6 +63,17 @@
Used by the DAG serialization code to only allow specific classes to be created
during deserialization
"""
PLUGINS_ATTRIBUTES_TO_DUMP = {
"hooks",
"executors",
"macros",
"flask_blueprints",
"appbuilder_views",
"appbuilder_menu_items",
"global_operator_extra_links",
"operator_extra_links",
"source",
}


class AirflowPluginSource:
Expand Down Expand Up @@ -427,3 +438,26 @@ def integrate_macros_plugins() -> None:
# Register the newly created module on airflow.macros such that it
# can be accessed when rendering templates.
setattr(macros, plugin.name, macros_module)


def get_plugin_info(attrs_to_dump: Optional[List[str]] = None) -> List[Dict[str, Any]]:
"""
Dump plugins attributes
:param attrs_to_dump: A list of plugin attributes to dump
:type attrs_to_dump: List
"""
ensure_plugins_loaded()
integrate_executor_plugins()
integrate_macros_plugins()
initialize_web_ui_plugins()
initialize_extra_operators_links_plugins()
if not attrs_to_dump:
attrs_to_dump = PLUGINS_ATTRIBUTES_TO_DUMP
plugins_info = []
if plugins:
for plugin in plugins:
info = {"name": plugin.name}
info.update({n: getattr(plugin, n) for n in attrs_to_dump})
plugins_info.append(info)
return plugins_info
Loading

0 comments on commit 31acf95

Please sign in to comment.