blob: 83dbc50eaa106e010a795da1b1d16b34bbfc2ccc [file] [log] [blame]
# 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.
import logging
import warnings
from os import path
from connexion import App, ProblemException
from flask import Flask, request
from airflow.api_connexion.exceptions import common_error_handler
from airflow.configuration import conf
from airflow.security import permissions
from airflow.www.views import lazy_add_provider_discovered_options_to_connection_form
log = logging.getLogger(__name__)
# airflow/www/extensions/init_views.py => airflow/
ROOT_APP_DIR = path.abspath(path.join(path.dirname(__file__), path.pardir, path.pardir))
def init_flash_views(app):
"""Init main app view - redirect to FAB"""
from airflow.www.blueprints import routes
app.register_blueprint(routes)
def init_appbuilder_views(app):
"""Initialize Web UI views"""
appbuilder = app.appbuilder
from airflow.www import views
# Remove the session from scoped_session registry to avoid
# reusing a session with a disconnected connection
appbuilder.session.remove()
appbuilder.add_view_no_menu(views.AutocompleteView())
appbuilder.add_view_no_menu(views.Airflow())
appbuilder.add_view(
views.DagRunModelView,
permissions.RESOURCE_DAG_RUN,
category=permissions.RESOURCE_BROWSE_MENU,
category_icon="fa-globe",
)
appbuilder.add_view(
views.JobModelView, permissions.RESOURCE_JOB, category=permissions.RESOURCE_BROWSE_MENU
)
appbuilder.add_view(
views.LogModelView, permissions.RESOURCE_AUDIT_LOG, category=permissions.RESOURCE_BROWSE_MENU
)
appbuilder.add_view(
views.VariableModelView, permissions.RESOURCE_VARIABLE, category=permissions.RESOURCE_ADMIN_MENU
)
appbuilder.add_view(
views.TaskInstanceModelView,
permissions.RESOURCE_TASK_INSTANCE,
category=permissions.RESOURCE_BROWSE_MENU,
)
appbuilder.add_view(
views.TaskRescheduleModelView,
permissions.RESOURCE_TASK_RESCHEDULE,
category=permissions.RESOURCE_BROWSE_MENU,
)
appbuilder.add_view(
views.TriggerModelView,
permissions.RESOURCE_TRIGGER,
category=permissions.RESOURCE_BROWSE_MENU,
)
appbuilder.add_view(
views.ConfigurationView,
permissions.RESOURCE_CONFIG,
category=permissions.RESOURCE_ADMIN_MENU,
category_icon="fa-user",
)
appbuilder.add_view(
views.ConnectionModelView, permissions.RESOURCE_CONNECTION, category=permissions.RESOURCE_ADMIN_MENU
)
appbuilder.add_view(
views.SlaMissModelView, permissions.RESOURCE_SLA_MISS, category=permissions.RESOURCE_BROWSE_MENU
)
appbuilder.add_view(
views.PluginView, permissions.RESOURCE_PLUGIN, category=permissions.RESOURCE_ADMIN_MENU
)
appbuilder.add_view(
views.ProviderView, permissions.RESOURCE_PROVIDER, category=permissions.RESOURCE_ADMIN_MENU
)
appbuilder.add_view(
views.PoolModelView, permissions.RESOURCE_POOL, category=permissions.RESOURCE_ADMIN_MENU
)
appbuilder.add_view(
views.XComModelView, permissions.RESOURCE_XCOM, category=permissions.RESOURCE_ADMIN_MENU
)
appbuilder.add_view(
views.DagDependenciesView,
permissions.RESOURCE_DAG_DEPENDENCIES,
category=permissions.RESOURCE_BROWSE_MENU,
)
# add_view_no_menu to change item position.
# I added link in extensions.init_appbuilder_links.init_appbuilder_links
appbuilder.add_view_no_menu(views.RedocView)
def init_plugins(app):
"""Integrate Flask and FAB with plugins"""
from airflow import plugins_manager
plugins_manager.initialize_web_ui_plugins()
appbuilder = app.appbuilder
for view in plugins_manager.flask_appbuilder_views:
name = view.get('name')
if name:
log.debug("Adding view %s with menu", name)
appbuilder.add_view(view["view"], name, category=view["category"])
else:
# if 'name' key is missing, intent is to add view without menu
log.debug("Adding view %s without menu", str(type(view["view"])))
appbuilder.add_view_no_menu(view["view"])
for menu_link in sorted(plugins_manager.flask_appbuilder_menu_links, key=lambda x: x["name"]):
log.debug("Adding menu link %s to %s", menu_link["name"], menu_link["href"])
appbuilder.add_link(**menu_link)
for blue_print in plugins_manager.flask_blueprints:
log.debug("Adding blueprint %s:%s", blue_print["name"], blue_print["blueprint"].import_name)
app.register_blueprint(blue_print["blueprint"])
def init_connection_form():
"""Initializes connection form"""
lazy_add_provider_discovered_options_to_connection_form()
def init_error_handlers(app: Flask):
"""Add custom errors handlers"""
from airflow.www import views
app.register_error_handler(500, views.show_traceback)
app.register_error_handler(404, views.not_found)
def set_cors_headers_on_response(response):
"""Add response headers"""
allow_headers = conf.get('api', 'access_control_allow_headers')
allow_methods = conf.get('api', 'access_control_allow_methods')
allow_origins = conf.get('api', 'access_control_allow_origins')
if allow_headers is not None:
response.headers['Access-Control-Allow-Headers'] = allow_headers
if allow_methods is not None:
response.headers['Access-Control-Allow-Methods'] = allow_methods
if allow_origins is not None:
allowed_origins = allow_origins.split(' ')
origin = request.environ.get('HTTP_ORIGIN', allowed_origins[0])
if origin in allowed_origins:
response.headers['Access-Control-Allow-Origin'] = origin
return response
def init_api_connexion(app: Flask) -> None:
"""Initialize Stable API"""
base_path = '/api/v1'
from airflow.www import views
@app.errorhandler(404)
@app.errorhandler(405)
def _handle_api_error(ex):
if request.path.startswith(base_path):
# 404 errors are never handled on the blueprint level
# unless raised from a view func so actual 404 errors,
# i.e. "no route for it" defined, need to be handled
# here on the application level
return common_error_handler(ex)
else:
return views.not_found(ex)
spec_dir = path.join(ROOT_APP_DIR, 'api_connexion', 'openapi')
connexion_app = App(__name__, specification_dir=spec_dir, skip_error_handlers=True)
connexion_app.app = app
api_bp = connexion_app.add_api(
specification='v1.yaml', base_path=base_path, validate_responses=True, strict_validation=True
).blueprint
# Like "api_bp.after_request", but the BP is already registered, so we have
# to register it in the app directly.
app.after_request_funcs.setdefault(api_bp.name, []).append(set_cors_headers_on_response)
app.register_error_handler(ProblemException, common_error_handler)
app.extensions['csrf'].exempt(api_bp)
def init_api_experimental(app):
"""Initialize Experimental API"""
if not conf.getboolean('api', 'enable_experimental_api', fallback=False):
return
from airflow.www.api.experimental import endpoints
warnings.warn(
"The experimental REST API is deprecated. Please migrate to the stable REST API. "
"Please note that the experimental API do not have access control. "
"The authenticated user has full access.",
DeprecationWarning,
)
app.register_blueprint(endpoints.api_experimental, url_prefix='/api/experimental')
app.extensions['csrf'].exempt(endpoints.api_experimental)