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:
Sam Chow 2018-05-14 15:45:26 -04:00
parent 15c15faf30
commit d080ca2cc6
49 changed files with 8996 additions and 153 deletions

View 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
}

View file

@ -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}

View file

@ -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