Create webpack config for config app
further improve developer morale get initial angular loading Add remote css to config index Starts work to port endpoints into config app Add the api blueprint
This commit is contained in:
parent
15c15faf30
commit
d080ca2cc6
49 changed files with 8996 additions and 153 deletions
151
config_app/config_endpoints/api/__init__.py
Normal file
151
config_app/config_endpoints/api/__init__.py
Normal file
|
@ -0,0 +1,151 @@
|
|||
import logging
|
||||
|
||||
from config_app import app
|
||||
from config_app.util.config import config_provider
|
||||
|
||||
from flask import Blueprint, request, session
|
||||
from flask_restful import Resource, abort, Api, reqparse
|
||||
from flask_restful.utils.cors import crossdomain
|
||||
|
||||
from functools import partial, wraps
|
||||
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
api_bp = Blueprint('api', __name__)
|
||||
|
||||
CROSS_DOMAIN_HEADERS = ['Authorization', 'Content-Type', 'X-Requested-With']
|
||||
|
||||
|
||||
class ApiExceptionHandlingApi(Api):
|
||||
@crossdomain(origin='*', headers=CROSS_DOMAIN_HEADERS)
|
||||
def handle_error(self, error):
|
||||
print('HANDLING ERROR IN API')
|
||||
return super(ApiExceptionHandlingApi, self).handle_error(error)
|
||||
|
||||
|
||||
api = ApiExceptionHandlingApi()
|
||||
|
||||
|
||||
class HelloWorld(Resource):
|
||||
def get(self):
|
||||
print("hit the dummy endpoint")
|
||||
return {'hello': 'world'}
|
||||
|
||||
|
||||
api.add_resource(HelloWorld, '/')
|
||||
|
||||
|
||||
|
||||
def verify_not_prod(func):
|
||||
@add_method_metadata('enterprise_only', True)
|
||||
def wrapped(*args, **kwargs):
|
||||
# Verify that we are not running on a production (i.e. hosted) stack. If so, we fail.
|
||||
# This should never happen (because of the feature-flag on SUPER_USERS), but we want to be
|
||||
# absolutely sure.
|
||||
# if app.config['SERVER_HOSTNAME'].find('quay.io') >= 0:
|
||||
# TODO(config_port) fixme
|
||||
if False:
|
||||
logger.error('!!! Super user method called IN PRODUCTION !!!')
|
||||
raise StandardError()
|
||||
|
||||
return func(*args, **kwargs)
|
||||
|
||||
return wrapped
|
||||
|
||||
|
||||
def resource(*urls, **kwargs):
|
||||
def wrapper(api_resource):
|
||||
if not api_resource:
|
||||
return None
|
||||
|
||||
api_resource.registered = True
|
||||
api.add_resource(api_resource, *urls, **kwargs)
|
||||
return api_resource
|
||||
|
||||
return wrapper
|
||||
|
||||
|
||||
class ApiResource(Resource):
|
||||
registered = False
|
||||
method_decorators = []
|
||||
|
||||
def options(self):
|
||||
return None, 200
|
||||
|
||||
|
||||
def add_method_metadata(name, value):
|
||||
def modifier(func):
|
||||
if func is None:
|
||||
return None
|
||||
|
||||
if '__api_metadata' not in dir(func):
|
||||
func.__api_metadata = {}
|
||||
func.__api_metadata[name] = value
|
||||
return func
|
||||
|
||||
return modifier
|
||||
|
||||
|
||||
def method_metadata(func, name):
|
||||
if func is None:
|
||||
return None
|
||||
|
||||
if '__api_metadata' in dir(func):
|
||||
return func.__api_metadata.get(name, None)
|
||||
return None
|
||||
|
||||
|
||||
def no_cache(f):
|
||||
@wraps(f)
|
||||
def add_no_cache(*args, **kwargs):
|
||||
response = f(*args, **kwargs)
|
||||
if response is not None:
|
||||
response.headers['Cache-Control'] = 'no-cache, no-store, must-revalidate'
|
||||
return response
|
||||
return add_no_cache
|
||||
|
||||
|
||||
nickname = partial(add_method_metadata, 'nickname')
|
||||
|
||||
api.init_app(api_bp)
|
||||
# api.decorators = [csrf_protect(),
|
||||
# crossdomain(origin='*', headers=CROSS_DOMAIN_HEADERS),
|
||||
# process_oauth, time_decorator(api_bp.name, metric_queue),
|
||||
# require_xhr_from_browser]
|
||||
|
||||
|
||||
|
||||
|
||||
@resource('/v1/superuser/config')
|
||||
class SuperUserConfig(ApiResource):
|
||||
""" Resource for fetching and updating the current configuration, if any. """
|
||||
schemas = {
|
||||
'UpdateConfig': {
|
||||
'type': 'object',
|
||||
'description': 'Updates the YAML config file',
|
||||
'required': [
|
||||
'config',
|
||||
'hostname'
|
||||
],
|
||||
'properties': {
|
||||
'config': {
|
||||
'type': 'object'
|
||||
},
|
||||
'hostname': {
|
||||
'type': 'string'
|
||||
},
|
||||
'password': {
|
||||
'type': 'string'
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
@verify_not_prod
|
||||
@nickname('scGetConfig')
|
||||
def get(self):
|
||||
""" Returns the currently defined configuration, if any. """
|
||||
config_object = config_provider.get_config()
|
||||
return {
|
||||
'config': config_object
|
||||
}
|
|
@ -1,78 +1,305 @@
|
|||
from flask import make_response, render_template, request, session
|
||||
import logging
|
||||
import os
|
||||
import re
|
||||
import sys
|
||||
from collections import OrderedDict
|
||||
from cachetools import lru_cache
|
||||
|
||||
def render_page_template(name, route_data=None, **kwargs):
|
||||
from flask import make_response, render_template
|
||||
from flask_restful import reqparse
|
||||
|
||||
from config_app.config_endpoints.api import method_metadata
|
||||
from config_app.app import app
|
||||
|
||||
|
||||
def truthy_bool(param):
|
||||
return param not in {False, 'false', 'False', '0', 'FALSE', '', 'null'}
|
||||
|
||||
|
||||
DEFAULT_JS_BUNDLE_NAME = 'configapp'
|
||||
PARAM_REGEX = re.compile(r'<([^:>]+:)*([\w]+)>')
|
||||
logger = logging.getLogger(__name__)
|
||||
TYPE_CONVERTER = {
|
||||
truthy_bool: 'boolean',
|
||||
str: 'string',
|
||||
basestring: 'string',
|
||||
reqparse.text_type: 'string',
|
||||
int: 'integer',
|
||||
}
|
||||
|
||||
|
||||
def _list_files(path, extension, contains=""):
|
||||
""" Returns a list of all the files with the given extension found under the given path. """
|
||||
|
||||
def matches(f):
|
||||
return os.path.splitext(f)[1] == '.' + extension and contains in os.path.splitext(f)[0]
|
||||
|
||||
def join_path(dp, f):
|
||||
# Remove the static/ prefix. It is added in the template.
|
||||
return os.path.join(dp, f)[len('static/'):]
|
||||
|
||||
filepath = os.path.join('static/', path)
|
||||
return [join_path(dp, f) for dp, _, files in os.walk(filepath) for f in files if matches(f)]
|
||||
|
||||
|
||||
def render_page_template(name, route_data=None, js_bundle_name=DEFAULT_JS_BUNDLE_NAME, **kwargs):
|
||||
""" Renders the page template with the given name as the response and returns its contents. """
|
||||
# main_scripts = _list_files('build', 'js', js_bundle_name)
|
||||
#
|
||||
# use_cdn = app.config.get('USE_CDN', True)
|
||||
# if request.args.get('use_cdn') is not None:
|
||||
# use_cdn = request.args.get('use_cdn') == 'true'
|
||||
#
|
||||
# external_styles = get_external_css(local=not use_cdn)
|
||||
# external_scripts = get_external_javascript(local=not use_cdn)
|
||||
#
|
||||
# # Add Stripe checkout if billing is enabled.
|
||||
# if features.BILLING:
|
||||
# external_scripts.append('//checkout.stripe.com/checkout.js')
|
||||
#
|
||||
# def get_external_login_config():
|
||||
# login_config = []
|
||||
# for login_service in oauth_login.services:
|
||||
# login_config.append({
|
||||
# 'id': login_service.service_id(),
|
||||
# 'title': login_service.service_name(),
|
||||
# 'config': login_service.get_public_config(),
|
||||
# 'icon': login_service.get_icon(),
|
||||
# })
|
||||
#
|
||||
# return login_config
|
||||
#
|
||||
# def get_oauth_config():
|
||||
# oauth_config = {}
|
||||
# for oauth_app in oauth_apps:
|
||||
# oauth_config[oauth_app.key_name] = oauth_app.get_public_config()
|
||||
#
|
||||
# return oauth_config
|
||||
#
|
||||
# contact_href = None
|
||||
# if len(app.config.get('CONTACT_INFO', [])) == 1:
|
||||
# contact_href = app.config['CONTACT_INFO'][0]
|
||||
#
|
||||
# version_number = ''
|
||||
# if not features.BILLING:
|
||||
# version_number = 'Quay %s' % __version__
|
||||
#
|
||||
# scopes_set = {scope.scope: scope._asdict() for scope in scopes.app_scopes(app.config).values()}
|
||||
main_scripts = _list_files('build', 'js', js_bundle_name)
|
||||
|
||||
contents = render_template(name,
|
||||
route_data=route_data,
|
||||
# external_styles=external_styles,
|
||||
# external_scripts=external_scripts,
|
||||
# main_scripts=main_scripts,
|
||||
# feature_set=features.get_features(),
|
||||
# config_set=frontend_visible_config(app.config),
|
||||
# oauth_set=get_oauth_config(),
|
||||
# external_login_set=get_external_login_config(),
|
||||
# scope_set=scopes_set,
|
||||
# vuln_priority_set=PRIORITY_LEVELS,
|
||||
# enterprise_logo=app.config.get('ENTERPRISE_LOGO_URL', ''),
|
||||
# mixpanel_key=app.config.get('MIXPANEL_KEY', ''),
|
||||
# munchkin_key=app.config.get('MARKETO_MUNCHKIN_ID', ''),
|
||||
# recaptcha_key=app.config.get('RECAPTCHA_SITE_KEY', ''),
|
||||
# google_tagmanager_key=app.config.get('GOOGLE_TAGMANAGER_KEY', ''),
|
||||
# google_anaytics_key=app.config.get('GOOGLE_ANALYTICS_KEY', ''),
|
||||
# sentry_public_dsn=app.config.get('SENTRY_PUBLIC_DSN', ''),
|
||||
# is_debug=str(app.config.get('DEBUGGING', False)).lower(),
|
||||
# show_chat=features.SUPPORT_CHAT,
|
||||
# aci_conversion=features.ACI_CONVERSION,
|
||||
# has_billing=features.BILLING,
|
||||
# contact_href=contact_href,
|
||||
# hostname=app.config['SERVER_HOSTNAME'],
|
||||
# preferred_scheme=app.config['PREFERRED_URL_SCHEME'],
|
||||
# version_number=version_number,
|
||||
# current_year=datetime.datetime.now().year,
|
||||
main_scripts=main_scripts,
|
||||
**kwargs)
|
||||
|
||||
resp = make_response(contents)
|
||||
resp.headers['X-FRAME-OPTIONS'] = 'DENY'
|
||||
return resp
|
||||
|
||||
|
||||
def fully_qualified_name(method_view_class):
|
||||
return '%s.%s' % (method_view_class.__module__, method_view_class.__name__)
|
||||
|
||||
|
||||
# @lru_cache(maxsize=1)
|
||||
def generate_route_data():
|
||||
include_internal = True
|
||||
compact = True
|
||||
|
||||
def swagger_parameter(name, description, kind='path', param_type='string', required=True,
|
||||
enum=None, schema=None):
|
||||
# https://github.com/swagger-api/swagger-spec/blob/master/versions/2.0.md#parameterObject
|
||||
parameter_info = {
|
||||
'name': name,
|
||||
'in': kind,
|
||||
'required': required
|
||||
}
|
||||
|
||||
if schema:
|
||||
parameter_info['schema'] = {
|
||||
'$ref': '#/definitions/%s' % schema
|
||||
}
|
||||
else:
|
||||
parameter_info['type'] = param_type
|
||||
|
||||
if enum is not None and len(list(enum)) > 0:
|
||||
parameter_info['enum'] = list(enum)
|
||||
|
||||
return parameter_info
|
||||
|
||||
paths = {}
|
||||
models = {}
|
||||
tags = []
|
||||
tags_added = set()
|
||||
operation_ids = set()
|
||||
|
||||
print('APP URL MAp:')
|
||||
print(app.url_map)
|
||||
for rule in app.url_map.iter_rules():
|
||||
endpoint_method = app.view_functions[rule.endpoint]
|
||||
|
||||
# Verify that we have a view class for this API method.
|
||||
if not 'view_class' in dir(endpoint_method):
|
||||
continue
|
||||
|
||||
view_class = endpoint_method.view_class
|
||||
|
||||
# Hide the class if it is internal.
|
||||
internal = method_metadata(view_class, 'internal')
|
||||
if not include_internal and internal:
|
||||
continue
|
||||
|
||||
# Build the tag.
|
||||
parts = fully_qualified_name(view_class).split('.')
|
||||
tag_name = parts[-2]
|
||||
if not tag_name in tags_added:
|
||||
tags_added.add(tag_name)
|
||||
tags.append({
|
||||
'name': tag_name,
|
||||
'description': (sys.modules[view_class.__module__].__doc__ or '').strip()
|
||||
})
|
||||
|
||||
# Build the Swagger data for the path.
|
||||
swagger_path = PARAM_REGEX.sub(r'{\2}', rule.rule)
|
||||
full_name = fully_qualified_name(view_class)
|
||||
path_swagger = {
|
||||
'x-name': full_name,
|
||||
'x-path': swagger_path,
|
||||
'x-tag': tag_name
|
||||
}
|
||||
|
||||
related_user_res = method_metadata(view_class, 'related_user_resource')
|
||||
if related_user_res is not None:
|
||||
path_swagger['x-user-related'] = fully_qualified_name(related_user_res)
|
||||
|
||||
paths[swagger_path] = path_swagger
|
||||
|
||||
# Add any global path parameters.
|
||||
param_data_map = view_class.__api_path_params if '__api_path_params' in dir(view_class) else {}
|
||||
if param_data_map:
|
||||
path_parameters_swagger = []
|
||||
for path_parameter in param_data_map:
|
||||
description = param_data_map[path_parameter].get('description')
|
||||
path_parameters_swagger.append(swagger_parameter(path_parameter, description))
|
||||
|
||||
path_swagger['parameters'] = path_parameters_swagger
|
||||
|
||||
# Add the individual HTTP operations.
|
||||
method_names = list(rule.methods.difference(['HEAD', 'OPTIONS']))
|
||||
for method_name in method_names:
|
||||
# https://github.com/swagger-api/swagger-spec/blob/master/versions/2.0.md#operation-object
|
||||
method = getattr(view_class, method_name.lower(), None)
|
||||
if method is None:
|
||||
logger.debug('Unable to find method for %s in class %s', method_name, view_class)
|
||||
continue
|
||||
|
||||
operationId = method_metadata(method, 'nickname')
|
||||
operation_swagger = {
|
||||
'operationId': operationId,
|
||||
'parameters': [],
|
||||
}
|
||||
|
||||
if operationId is None:
|
||||
continue
|
||||
|
||||
if operationId in operation_ids:
|
||||
raise Exception('Duplicate operation Id: %s' % operationId)
|
||||
|
||||
operation_ids.add(operationId)
|
||||
|
||||
# Mark the method as internal.
|
||||
internal = method_metadata(method, 'internal')
|
||||
if internal is not None:
|
||||
operation_swagger['x-internal'] = True
|
||||
|
||||
if include_internal:
|
||||
requires_fresh_login = method_metadata(method, 'requires_fresh_login')
|
||||
if requires_fresh_login is not None:
|
||||
operation_swagger['x-requires-fresh-login'] = True
|
||||
|
||||
# Add the path parameters.
|
||||
if rule.arguments:
|
||||
for path_parameter in rule.arguments:
|
||||
description = param_data_map.get(path_parameter, {}).get('description')
|
||||
operation_swagger['parameters'].append(swagger_parameter(path_parameter, description))
|
||||
|
||||
# Add the query parameters.
|
||||
if '__api_query_params' in dir(method):
|
||||
for query_parameter_info in method.__api_query_params:
|
||||
name = query_parameter_info['name']
|
||||
description = query_parameter_info['help']
|
||||
param_type = TYPE_CONVERTER[query_parameter_info['type']]
|
||||
required = query_parameter_info['required']
|
||||
|
||||
operation_swagger['parameters'].append(
|
||||
swagger_parameter(name, description, kind='query',
|
||||
param_type=param_type,
|
||||
required=required,
|
||||
enum=query_parameter_info['choices']))
|
||||
|
||||
# Add the OAuth security block.
|
||||
# https://github.com/swagger-api/swagger-spec/blob/master/versions/2.0.md#securityRequirementObject
|
||||
scope = method_metadata(method, 'oauth2_scope')
|
||||
if scope and not compact:
|
||||
operation_swagger['security'] = [{'oauth2_implicit': [scope.scope]}]
|
||||
|
||||
# Add the responses block.
|
||||
# https://github.com/swagger-api/swagger-spec/blob/master/versions/2.0.md#responsesObject
|
||||
response_schema_name = method_metadata(method, 'response_schema')
|
||||
if not compact:
|
||||
if response_schema_name:
|
||||
models[response_schema_name] = view_class.schemas[response_schema_name]
|
||||
|
||||
models['ApiError'] = {
|
||||
'type': 'object',
|
||||
'properties': {
|
||||
'status': {
|
||||
'type': 'integer',
|
||||
'description': 'Status code of the response.'
|
||||
},
|
||||
'type': {
|
||||
'type': 'string',
|
||||
'description': 'Reference to the type of the error.'
|
||||
},
|
||||
'detail': {
|
||||
'type': 'string',
|
||||
'description': 'Details about the specific instance of the error.'
|
||||
},
|
||||
'title': {
|
||||
'type': 'string',
|
||||
'description': 'Unique error code to identify the type of error.'
|
||||
},
|
||||
'error_message': {
|
||||
'type': 'string',
|
||||
'description': 'Deprecated; alias for detail'
|
||||
},
|
||||
'error_type': {
|
||||
'type': 'string',
|
||||
'description': 'Deprecated; alias for detail'
|
||||
}
|
||||
},
|
||||
'required': [
|
||||
'status',
|
||||
'type',
|
||||
'title',
|
||||
]
|
||||
}
|
||||
|
||||
responses = {
|
||||
'400': {
|
||||
'description': 'Bad Request',
|
||||
},
|
||||
|
||||
'401': {
|
||||
'description': 'Session required',
|
||||
},
|
||||
|
||||
'403': {
|
||||
'description': 'Unauthorized access',
|
||||
},
|
||||
|
||||
'404': {
|
||||
'description': 'Not found',
|
||||
},
|
||||
}
|
||||
|
||||
for _, body in responses.items():
|
||||
body['schema'] = {'$ref': '#/definitions/ApiError'}
|
||||
|
||||
if method_name == 'DELETE':
|
||||
responses['204'] = {
|
||||
'description': 'Deleted'
|
||||
}
|
||||
elif method_name == 'POST':
|
||||
responses['201'] = {
|
||||
'description': 'Successful creation'
|
||||
}
|
||||
else:
|
||||
responses['200'] = {
|
||||
'description': 'Successful invocation'
|
||||
}
|
||||
|
||||
if response_schema_name:
|
||||
responses['200']['schema'] = {
|
||||
'$ref': '#/definitions/%s' % response_schema_name
|
||||
}
|
||||
|
||||
operation_swagger['responses'] = responses
|
||||
|
||||
# Add the request block.
|
||||
request_schema_name = method_metadata(method, 'request_schema')
|
||||
if request_schema_name and not compact:
|
||||
models[request_schema_name] = view_class.schemas[request_schema_name]
|
||||
|
||||
operation_swagger['parameters'].append(
|
||||
swagger_parameter('body', 'Request body contents.', kind='body',
|
||||
schema=request_schema_name))
|
||||
|
||||
# Add the operation to the parent path.
|
||||
if not internal or (internal and include_internal):
|
||||
path_swagger[method_name.lower()] = operation_swagger
|
||||
|
||||
tags.sort(key=lambda t: t['name'])
|
||||
paths = OrderedDict(sorted(paths.items(), key=lambda p: p[1]['x-tag']))
|
||||
|
||||
if compact:
|
||||
return {'paths': paths}
|
||||
|
|
|
@ -1,47 +1,18 @@
|
|||
import os
|
||||
import json
|
||||
import logging
|
||||
|
||||
from datetime import timedelta, datetime
|
||||
|
||||
from cachetools import lru_cache
|
||||
# from flask import (abort, redirect, request, url_for, make_response, Response, render_template,
|
||||
# Blueprint, jsonify, send_file, session)
|
||||
from flask import Blueprint
|
||||
# from flask_login import current_user
|
||||
|
||||
|
||||
from app import (app)
|
||||
# from endpoints.api.discovery import swagger_route_data
|
||||
from common import render_page_template
|
||||
from config_app.config_endpoints.common import generate_route_data
|
||||
from util.cache import no_cache
|
||||
|
||||
|
||||
|
||||
# @lru_cache(maxsize=1)
|
||||
# def _get_route_data():
|
||||
# return swagger_route_data(include_internal=True, compact=True)
|
||||
|
||||
|
||||
def render_page_template_with_routedata(name, *args, **kwargs):
|
||||
return render_page_template(name, *args, **kwargs)
|
||||
|
||||
# Capture the unverified SSL errors.
|
||||
logger = logging.getLogger(__name__)
|
||||
logging.captureWarnings(True)
|
||||
|
||||
setup_web = Blueprint('setup_web', __name__, template_folder='templates')
|
||||
|
||||
# STATUS_TAGS = app.config['STATUS_TAGS']
|
||||
|
||||
def render_page_template_with_routedata(name, *args, **kwargs):
|
||||
return render_page_template(name, generate_route_data(), *args, **kwargs)
|
||||
|
||||
|
||||
@setup_web.route('/', methods=['GET'], defaults={'path': ''})
|
||||
@no_cache
|
||||
def index(path, **kwargs):
|
||||
return render_page_template_with_routedata('config_index.html', js_bundle_name='configapp', **kwargs)
|
||||
return render_page_template_with_routedata('index.html', js_bundle_name='configapp', **kwargs)
|
||||
|
||||
|
||||
@setup_web.errorhandler(404)
|
||||
@setup_web.route('/404', methods=['GET'])
|
||||
def not_found_error_display(e = None):
|
||||
resp = index('', error_code=404, error_info=dict(reason='notfound'))
|
||||
resp.status_code = 404
|
||||
return resp
|
||||
|
|
Reference in a new issue