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
|
@ -1,3 +1,3 @@
|
|||
app: PYTHONPATH="../" gunicorn -c conf/gunicorn_local.py application:application
|
||||
# webpack: npm run watch
|
||||
# webpack: npm run watch-config-app
|
||||
|
||||
|
|
|
@ -1,3 +1,4 @@
|
|||
from flask import Flask, request, Request
|
||||
from flask import Flask
|
||||
|
||||
app = Flask(__name__)
|
||||
|
||||
|
|
|
@ -1,11 +1,5 @@
|
|||
# import os
|
||||
# import logging
|
||||
# import logging.config
|
||||
|
||||
# from util.log import logfile_path
|
||||
from app import app as application
|
||||
|
||||
|
||||
# Bind all of the blueprints
|
||||
import web
|
||||
|
||||
|
|
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
|
||||
|
|
172
config_app/js/components/file-upload-box.js
Normal file
172
config_app/js/components/file-upload-box.js
Normal file
|
@ -0,0 +1,172 @@
|
|||
/**
|
||||
* An element which adds a stylize box for uploading a file.
|
||||
*/
|
||||
angular.module('quay-config').directive('fileUploadBox', function () {
|
||||
var directiveDefinitionObject = {
|
||||
priority: 0,
|
||||
templateUrl: '/static/directives/file-upload-box.html',
|
||||
replace: false,
|
||||
transclude: true,
|
||||
restrict: 'C',
|
||||
scope: {
|
||||
'selectMessage': '@selectMessage',
|
||||
|
||||
'filesSelected': '&filesSelected',
|
||||
'filesCleared': '&filesCleared',
|
||||
'filesValidated': '&filesValidated',
|
||||
|
||||
'extensions': '<extensions',
|
||||
|
||||
'reset': '=?reset'
|
||||
},
|
||||
controller: function($rootScope, $scope, $element, ApiService) {
|
||||
var MEGABYTE = 1000000;
|
||||
var MAX_FILE_SIZE = MAX_FILE_SIZE_MB * MEGABYTE;
|
||||
var MAX_FILE_SIZE_MB = 100;
|
||||
|
||||
var number = $rootScope.__fileUploadBoxIdCounter || 0;
|
||||
$rootScope.__fileUploadBoxIdCounter = number + 1;
|
||||
|
||||
$scope.boxId = number;
|
||||
$scope.state = 'clear';
|
||||
$scope.selectedFiles = [];
|
||||
|
||||
var conductUpload = function(file, url, fileId, mimeType, progressCb, doneCb) {
|
||||
var request = new XMLHttpRequest();
|
||||
request.open('PUT', url, true);
|
||||
request.setRequestHeader('Content-Type', mimeType);
|
||||
request.onprogress = function(e) {
|
||||
$scope.$apply(function() {
|
||||
if (e.lengthComputable) { progressCb((e.loaded / e.total) * 100); }
|
||||
});
|
||||
};
|
||||
|
||||
request.onerror = function() {
|
||||
$scope.$apply(function() { doneCb(false, 'Error when uploading'); });
|
||||
};
|
||||
|
||||
request.onreadystatechange = function() {
|
||||
var state = request.readyState;
|
||||
var status = request.status;
|
||||
|
||||
if (state == 4) {
|
||||
if (Math.floor(status / 100) == 2) {
|
||||
$scope.$apply(function() { doneCb(true, fileId); });
|
||||
} else {
|
||||
var message = request.statusText;
|
||||
if (status == 413) {
|
||||
message = 'Selected file too large to upload';
|
||||
}
|
||||
|
||||
$scope.$apply(function() { doneCb(false, message); });
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
request.send(file);
|
||||
};
|
||||
|
||||
var uploadFiles = function(callback) {
|
||||
var currentIndex = 0;
|
||||
var fileIds = [];
|
||||
|
||||
var progressCb = function(progress) {
|
||||
$scope.uploadProgress = progress;
|
||||
};
|
||||
|
||||
var doneCb = function(status, messageOrId) {
|
||||
if (status) {
|
||||
fileIds.push(messageOrId);
|
||||
currentIndex++;
|
||||
performFileUpload();
|
||||
} else {
|
||||
callback(false, messageOrId);
|
||||
}
|
||||
};
|
||||
|
||||
var performFileUpload = function() {
|
||||
// If we have finished uploading all of the files, invoke the overall callback
|
||||
// with the list of file IDs.
|
||||
if (currentIndex >= $scope.selectedFiles.length) {
|
||||
callback(true, fileIds);
|
||||
return;
|
||||
}
|
||||
|
||||
// For the current file, retrieve a file-drop URL from the API for the file.
|
||||
var currentFile = $scope.selectedFiles[currentIndex];
|
||||
var mimeType = currentFile.type || 'application/octet-stream';
|
||||
var data = {
|
||||
'mimeType': mimeType
|
||||
};
|
||||
|
||||
$scope.currentlyUploadingFile = currentFile;
|
||||
$scope.uploadProgress = 0;
|
||||
|
||||
ApiService.getFiledropUrl(data).then(function(resp) {
|
||||
// Perform the upload.
|
||||
conductUpload(currentFile, resp.url, resp.file_id, mimeType, progressCb, doneCb);
|
||||
}, function() {
|
||||
callback(false, 'Could not retrieve upload URL');
|
||||
});
|
||||
};
|
||||
|
||||
// Start the uploading.
|
||||
$scope.state = 'uploading';
|
||||
performFileUpload();
|
||||
};
|
||||
|
||||
$scope.handleFilesChanged = function(files) {
|
||||
if ($scope.state == 'uploading') { return; }
|
||||
|
||||
$scope.message = null;
|
||||
$scope.selectedFiles = files;
|
||||
|
||||
if (files.length == 0) {
|
||||
$scope.state = 'clear';
|
||||
$scope.filesCleared();
|
||||
} else {
|
||||
for (var i = 0; i < files.length; ++i) {
|
||||
if (files[i].size > MAX_FILE_SIZE) {
|
||||
$scope.state = 'error';
|
||||
$scope.message = 'File ' + files[i].name + ' is larger than the maximum file ' +
|
||||
'size of ' + MAX_FILE_SIZE_MB + ' MB';
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
$scope.state = 'checking';
|
||||
$scope.filesSelected({
|
||||
'files': files,
|
||||
'callback': function(status, message) {
|
||||
$scope.state = status ? 'okay' : 'error';
|
||||
$scope.message = message;
|
||||
|
||||
if (status) {
|
||||
$scope.filesValidated({
|
||||
'files': files,
|
||||
'uploadFiles': uploadFiles
|
||||
});
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
$scope.getAccepts = function(extensions) {
|
||||
if (!extensions || !extensions.length) {
|
||||
return '*';
|
||||
}
|
||||
|
||||
return extensions.join(',');
|
||||
};
|
||||
|
||||
$scope.$watch('reset', function(reset) {
|
||||
if (reset) {
|
||||
$scope.state = 'clear';
|
||||
$element.find('#file-drop-' + $scope.boxId).parent().trigger('reset');
|
||||
}
|
||||
});
|
||||
}
|
||||
};
|
||||
return directiveDefinitionObject;
|
||||
});
|
45
config_app/js/config-app.module.ts
Normal file
45
config_app/js/config-app.module.ts
Normal file
|
@ -0,0 +1,45 @@
|
|||
import { NgModule } from 'ng-metadata/core';
|
||||
import * as restangular from 'restangular';
|
||||
|
||||
const quayDependencies: string[] = [
|
||||
'restangular',
|
||||
'ngCookies',
|
||||
'angularFileUpload',
|
||||
'ngSanitize'
|
||||
];
|
||||
|
||||
@NgModule(({
|
||||
imports: quayDependencies,
|
||||
declarations: [],
|
||||
providers: [
|
||||
provideConfig,
|
||||
]
|
||||
}))
|
||||
class DependencyConfig{}
|
||||
|
||||
|
||||
provideConfig.$inject = [
|
||||
'$provide',
|
||||
'$injector',
|
||||
'$compileProvider',
|
||||
'RestangularProvider',
|
||||
];
|
||||
|
||||
function provideConfig($provide: ng.auto.IProvideService,
|
||||
$injector: ng.auto.IInjectorService,
|
||||
$compileProvider: ng.ICompileProvider,
|
||||
RestangularProvider: any): void {
|
||||
|
||||
// Configure the API provider.
|
||||
RestangularProvider.setBaseUrl('/api/v1/');
|
||||
|
||||
console.log('i');
|
||||
}
|
||||
|
||||
|
||||
@NgModule({
|
||||
imports: [ DependencyConfig ],
|
||||
declarations: [],
|
||||
providers: []
|
||||
})
|
||||
export class ConfigAppModule {}
|
|
@ -0,0 +1,8 @@
|
|||
<div class="config-bool-field-element">
|
||||
<form name="fieldform" novalidate>
|
||||
<label>
|
||||
<input type="checkbox" ng-model="binding">
|
||||
<span ng-transclude/>
|
||||
</label>
|
||||
</form>
|
||||
</div>
|
|
@ -0,0 +1,76 @@
|
|||
<div class="config-certificates-field-element">
|
||||
<div class="resource-view" resource="certificatesResource" error-message="'Could not load certificates list'">
|
||||
<!-- File -->
|
||||
<div class="co-alert co-alert-warning" ng-if="certInfo.status == 'file'">
|
||||
<code>extra_ca_certs</code> is a single file and cannot be processed by this tool. If a valid and appended list of certificates, they will be installed on container startup.
|
||||
</div>
|
||||
|
||||
<div ng-if="certInfo.status != 'file'">
|
||||
<div class="description">
|
||||
<p>This section lists any custom or self-signed SSL certificates that are installed in the <span class="registry-name"></span> container on startup after being read from the <code>extra_ca_certs</code> directory in the configuration volume.
|
||||
</p>
|
||||
<p>
|
||||
Custom certificates are typically used in place of publicly signed certificates for corporate-internal services.
|
||||
</p>
|
||||
<p>Please <strong>make sure</strong> that all custom names used for downstream services (such as Clair) are listed in the certificates below.</p>
|
||||
</div>
|
||||
|
||||
<table class="config-table" style="margin-bottom: 20px;">
|
||||
<tr>
|
||||
<td>Upload certificates:</td>
|
||||
<td>
|
||||
<div class="file-upload-box"
|
||||
select-message="Select custom certificate to add to configuration. Must be in PEM format and end extension '.crt'"
|
||||
files-selected="handleCertsSelected(files, callback)"
|
||||
reset="resetUpload"
|
||||
extensions="['.crt']"></div>
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
|
||||
<table class="co-table">
|
||||
<thead>
|
||||
<td>Certificate Filename</td>
|
||||
<td>Status</td>
|
||||
<td>Names Handled</td>
|
||||
<td class="options-col"></td>
|
||||
</thead>
|
||||
<tr ng-repeat="certificate in certInfo.certs" ng-if="!certsUploading">
|
||||
<td>{{ certificate.path }}</td>
|
||||
<td class="cert-status">
|
||||
<div ng-if="certificate.error" class="red">
|
||||
<i class="fa fa-exclamation-circle"></i>
|
||||
Error: {{ certificate.error }}
|
||||
</div>
|
||||
<div ng-if="certificate.expired" class="orange">
|
||||
<i class="fa fa-exclamation-triangle"></i>
|
||||
Certificate is expired
|
||||
</div>
|
||||
<div ng-if="!certificate.error && !certificate.expired" class="green">
|
||||
<i class="fa fa-check-circle"></i>
|
||||
Certificate is valid
|
||||
</div>
|
||||
</td>
|
||||
<td>
|
||||
<div class="empty" ng-if="!certificate.names">(None)</div>
|
||||
<a class="dns-name" ng-href="http://{{ name }}" ng-repeat="name in certificate.names" ng-safenewtab>{{ name }}</a>
|
||||
</td>
|
||||
<td class="options-col">
|
||||
<span class="cor-options-menu">
|
||||
<span class="cor-option" option-click="deleteCert(certificate.path)">
|
||||
<i class="fa fa-times"></i> Delete Certificate
|
||||
</span>
|
||||
</span>
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
<div ng-if="certsUploading" style="margin-top: 20px; text-align: center;">
|
||||
<div class="cor-loader-inline"></div>
|
||||
Uploading, validating and updating certificate(s)
|
||||
</div>
|
||||
<div class="empty" ng-if="!certInfo.certs.length && !certsUploading" style="margin-top: 20px;">
|
||||
<div class="empty-primary-msg">No custom certificates found.</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
|
@ -0,0 +1,46 @@
|
|||
<div class="config-contact-field-element">
|
||||
<table>
|
||||
<tr>
|
||||
<td>
|
||||
<div class="dropdown">
|
||||
<button class="btn btn-default dropdown-toggle" type="button" data-toggle="dropdown">
|
||||
<span ng-switch="kind">
|
||||
<span ng-switch-when="mailto"><i class="fa fa-envelope"></i>E-mail</span>
|
||||
<span ng-switch-when="irc"><i class="fa fa-comment"></i>IRC</span>
|
||||
<span ng-switch-when="tel"><i class="fa fa-phone"></i>Phone</span>
|
||||
<span ng-switch-default><i class="fa fa-ticket"></i>URL</span>
|
||||
</span>
|
||||
<span class="caret"></span>
|
||||
</button>
|
||||
<ul class="dropdown-menu" role="menu">
|
||||
<li role="presentation">
|
||||
<a role="menuitem" tabindex="-1" ng-click="kind = 'mailto'">
|
||||
<i class="fa fa-envelope"></i> E-mail
|
||||
</a>
|
||||
</li>
|
||||
<li role="presentation">
|
||||
<a role="menuitem" tabindex="-1" ng-click="kind = 'irc'">
|
||||
<i class="fa fa-comment"></i> IRC
|
||||
</a>
|
||||
</li>
|
||||
<li role="presentation">
|
||||
<a role="menuitem" tabindex="-1" ng-click="kind = 'tel'">
|
||||
<i class="fa fa-phone"></i> Telephone
|
||||
</a>
|
||||
</li>
|
||||
<li role="presentation">
|
||||
<a role="menuitem" tabindex="-1" ng-click="kind = 'http'">
|
||||
<i class="fa fa-ticket"></i> URL
|
||||
</a>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
</td>
|
||||
<td>
|
||||
<form>
|
||||
<input class="form-control" placeholder="{{ getPlaceholder(kind) }}" ng-model="value">
|
||||
</form>
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
</div>
|
|
@ -0,0 +1,4 @@
|
|||
<div class="config-contacts-field-element">
|
||||
<div class="config-contact-field" binding="item.value" ng-repeat="item in items">
|
||||
</div>
|
||||
</div>
|
13
config_app/js/config-field-templates/config-file-field.html
Normal file
13
config_app/js/config-field-templates/config-file-field.html
Normal file
|
@ -0,0 +1,13 @@
|
|||
<div class="config-file-field-element">
|
||||
<span ng-show="uploadProgress == null">
|
||||
<span ng-if="hasFile">
|
||||
<code>/conf/stack/{{ filename }}</code>
|
||||
<span style="margin-left: 20px; display: inline-block;">Select a replacement file:</span>
|
||||
</span>
|
||||
<span class="nofile" ng-if="!hasFile && skipCheckFile != 'true'">Please select a file to upload as <b>{{ filename }}</b>: </span>
|
||||
<input type="file" ng-file-select="onFileSelect($files)">
|
||||
</span>
|
||||
<span ng-show="uploadProgress != null">
|
||||
Uploading file as <strong>{{ filename }}</strong>... {{ uploadProgress }}%
|
||||
</span>
|
||||
</div>
|
17
config_app/js/config-field-templates/config-list-field.html
Normal file
17
config_app/js/config-field-templates/config-list-field.html
Normal file
|
@ -0,0 +1,17 @@
|
|||
<div class="config-list-field-element">
|
||||
<ul ng-show="binding && binding.length">
|
||||
<li class="item" ng-repeat="item in binding">
|
||||
<span class="item-title">{{ item }}</span>
|
||||
<span class="item-delete">
|
||||
<a ng-click="removeItem(item)">Remove</a>
|
||||
</span>
|
||||
</li>
|
||||
</ul>
|
||||
<span class="empty" ng-if="!binding || binding.length == 0">No {{ itemTitle }}s defined</span>
|
||||
<form class="form-control-container" ng-submit="addItem()">
|
||||
<input type="text" class="form-control" placeholder="{{ placeholder }}"
|
||||
ng-pattern="getRegexp(itemPattern)"
|
||||
ng-model="newItemName" style="display: inline-block">
|
||||
<button class="btn btn-default" style="display: inline-block">Add</button>
|
||||
</form>
|
||||
</div>
|
20
config_app/js/config-field-templates/config-map-field.html
Normal file
20
config_app/js/config-field-templates/config-map-field.html
Normal file
|
@ -0,0 +1,20 @@
|
|||
<div class="config-map-field-element">
|
||||
<table class="table" ng-show="hasValues(binding)">
|
||||
<tr class="item" ng-repeat="(key, value) in binding">
|
||||
<td class="item-title">{{ key }}</td>
|
||||
<td class="item-value">{{ value }}</td>
|
||||
<td class="item-delete">
|
||||
<a ng-click="removeKey(key)">Remove</a>
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
<span class="empty" ng-if="!hasValues(binding)">No entries defined</span>
|
||||
<form class="form-control-container" ng-submit="addEntry()">
|
||||
Add Key-Value:
|
||||
<select ng-model="newKey">
|
||||
<option ng-repeat="key in keys" value="{{ key }}">{{ key }}</option>
|
||||
</select>
|
||||
<input type="text" class="form-control" placeholder="Value" ng-model="newValue">
|
||||
<button class="btn btn-default" style="display: inline-block">Add Entry</button>
|
||||
</form>
|
||||
</div>
|
|
@ -0,0 +1,6 @@
|
|||
<div class="config-numeric-field-element">
|
||||
<form name="fieldform" novalidate>
|
||||
<input type="number" class="form-control" placeholder="{{ placeholder || '' }}"
|
||||
ng-model="bindinginternal" ng-trim="false" ng-minlength="1" required>
|
||||
</form>
|
||||
</div>
|
|
@ -0,0 +1 @@
|
|||
<div class="config-parsed-field-element"></div>
|
|
@ -0,0 +1,29 @@
|
|||
<div class="config-service-key-field-element">
|
||||
<!-- Loading -->
|
||||
<div class="cor-loader" ng-if="loading"></div>
|
||||
|
||||
<!-- Loading error -->
|
||||
<div class="co-alert co-alert-warning" ng-if="loadError">
|
||||
Could not load service keys
|
||||
</div>
|
||||
|
||||
<!-- Key config -->
|
||||
<div ng-show="!loading && !loadError">
|
||||
<div ng-show="hasValidKey">
|
||||
<i class="fa fa-check"></i>
|
||||
Valid key for service <code>{{ serviceName }}</code> exists
|
||||
</div>
|
||||
<div ng-show="!hasValidKey">
|
||||
No valid key found for service <code>{{ serviceName }}</code>
|
||||
<a class="co-modify-link" ng-click="showRequestServiceKey()">Create Key</a>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Note: This field is a hidden text field that binds to a model that is set to non-empty
|
||||
when there is a valid key. This allows us to use the existing Angular form validation
|
||||
code.
|
||||
-->
|
||||
<input type="text" ng-model="hasValidKeyStr" ng-required="true" style="position: absolute; top: 0px; left: 0px; visibility: hidden; width: 0px; height: 0px;">
|
||||
|
||||
<div class="request-service-key-dialog" request-key-info="requestKeyInfo" key-created="handleKeyCreated(key)"></div>
|
||||
</div>
|
1656
config_app/js/config-field-templates/config-setup-tool.html
Normal file
1656
config_app/js/config-field-templates/config-setup-tool.html
Normal file
File diff suppressed because it is too large
Load diff
|
@ -0,0 +1,10 @@
|
|||
<div class="config-string-field-element">
|
||||
<form name="fieldform" novalidate>
|
||||
<input type="text" class="form-control" placeholder="{{ placeholder || '' }}"
|
||||
ng-model="binding" ng-trim="false" ng-minlength="1"
|
||||
ng-pattern="getRegexp(pattern)" ng-required="!isOptional">
|
||||
<div class="alert alert-danger" ng-show="errorMessage">
|
||||
{{ errorMessage }}
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
|
@ -0,0 +1,6 @@
|
|||
<div class="config-string-list-field-element">
|
||||
<form name="fieldform" novalidate>
|
||||
<input type="text" class="form-control" placeholder="{{ placeholder || '' }}"
|
||||
ng-model="internalBinding" ng-trim="true" ng-minlength="1" ng-required="!isOptional">
|
||||
</form>
|
||||
</div>
|
|
@ -0,0 +1,10 @@
|
|||
<div class="config-variable-field-element">
|
||||
<div class="btn-group">
|
||||
<button type="button" class="btn btn-default"
|
||||
ng-repeat="section in sections"
|
||||
ng-click="setSection(section)"
|
||||
ng-class="section == currentSection ? 'active' : ''">{{ section.title }}</button>
|
||||
</div>
|
||||
|
||||
<span ng-transclude></span>
|
||||
</div>
|
1657
config_app/js/core-config-setup/config-setup-tool.html
Normal file
1657
config_app/js/core-config-setup/config-setup-tool.html
Normal file
File diff suppressed because it is too large
Load diff
1454
config_app/js/core-config-setup/core-config-setup.js
Normal file
1454
config_app/js/core-config-setup/core-config-setup.js
Normal file
File diff suppressed because it is too large
Load diff
36
config_app/js/main.ts
Normal file
36
config_app/js/main.ts
Normal file
|
@ -0,0 +1,36 @@
|
|||
// imports shims, etc
|
||||
import 'core-js';
|
||||
|
||||
import '../static/css/core-ui.css';
|
||||
|
||||
import * as angular from 'angular';
|
||||
import { ConfigAppModule } from './config-app.module';
|
||||
import { bundle } from 'ng-metadata/core';
|
||||
|
||||
// load all app dependencies
|
||||
require('../static/lib/angular-file-upload.min.js');
|
||||
require('../../static/js/tar');
|
||||
|
||||
const ng1QuayModule: string = bundle(ConfigAppModule, []).name;
|
||||
angular.module('quay-config', [ng1QuayModule])
|
||||
.run(() => {
|
||||
console.log(' init run was called')
|
||||
});
|
||||
|
||||
console.log('Hello world! I\'m the config app');
|
||||
|
||||
declare var require: any;
|
||||
function requireAll(r) {
|
||||
r.keys().forEach(r);
|
||||
}
|
||||
|
||||
// load all services
|
||||
// require('./services/api-service');
|
||||
requireAll(require.context('./services', true, /\.js$/));
|
||||
|
||||
|
||||
// load all the components after services
|
||||
requireAll(require.context('./setup', true, /\.js$/));
|
||||
requireAll(require.context('./core-config-setup', true, /\.js$/));
|
||||
|
||||
|
332
config_app/js/services/api-service.js
Normal file
332
config_app/js/services/api-service.js
Normal file
|
@ -0,0 +1,332 @@
|
|||
/**
|
||||
* Service which exposes the server-defined API as a nice set of helper methods and automatic
|
||||
* callbacks. Any method defined on the server is exposed here as an equivalent method. Also
|
||||
* defines some helper functions for working with API responses.
|
||||
*/
|
||||
// console.log(angular.module('quay-config').requires);
|
||||
angular.module('quay-config').factory('ApiService', ['Restangular', '$q', 'UtilService', function(Restangular, $q, UtilService) {
|
||||
var apiService = {};
|
||||
|
||||
// if (!window.__endpoints) {
|
||||
// return apiService;
|
||||
// }
|
||||
|
||||
var getResource = function(getMethod, operation, opt_parameters, opt_background) {
|
||||
var resource = {};
|
||||
resource.withOptions = function(options) {
|
||||
this.options = options;
|
||||
return this;
|
||||
};
|
||||
|
||||
resource.get = function(processor, opt_errorHandler) {
|
||||
var options = this.options;
|
||||
var result = {
|
||||
'loading': true,
|
||||
'value': null,
|
||||
'hasError': false
|
||||
};
|
||||
|
||||
getMethod(options, opt_parameters, opt_background, true).then(function(resp) {
|
||||
result.value = processor(resp);
|
||||
result.loading = false;
|
||||
}, function(resp) {
|
||||
result.hasError = true;
|
||||
result.loading = false;
|
||||
if (opt_errorHandler) {
|
||||
opt_errorHandler(resp);
|
||||
}
|
||||
});
|
||||
|
||||
return result;
|
||||
};
|
||||
|
||||
return resource;
|
||||
};
|
||||
|
||||
var buildUrl = function(path, parameters) {
|
||||
// We already have /api/v1/ on the URLs, so remove them from the paths.
|
||||
path = path.substr('/api/v1/'.length, path.length);
|
||||
|
||||
// Build the path, adjusted with the inline parameters.
|
||||
var used = {};
|
||||
var url = '';
|
||||
for (var i = 0; i < path.length; ++i) {
|
||||
var c = path[i];
|
||||
if (c == '{') {
|
||||
var end = path.indexOf('}', i);
|
||||
var varName = path.substr(i + 1, end - i - 1);
|
||||
|
||||
if (!parameters[varName]) {
|
||||
throw new Error('Missing parameter: ' + varName);
|
||||
}
|
||||
|
||||
used[varName] = true;
|
||||
url += parameters[varName];
|
||||
i = end;
|
||||
continue;
|
||||
}
|
||||
|
||||
url += c;
|
||||
}
|
||||
|
||||
// Append any query parameters.
|
||||
var isFirst = true;
|
||||
for (var paramName in parameters) {
|
||||
if (!parameters.hasOwnProperty(paramName)) { continue; }
|
||||
if (used[paramName]) { continue; }
|
||||
|
||||
var value = parameters[paramName];
|
||||
if (value) {
|
||||
url += isFirst ? '?' : '&';
|
||||
url += paramName + '=' + encodeURIComponent(value)
|
||||
isFirst = false;
|
||||
}
|
||||
}
|
||||
|
||||
return url;
|
||||
};
|
||||
|
||||
var getGenericOperationName = function(userOperationName) {
|
||||
return userOperationName.replace('User', '');
|
||||
};
|
||||
|
||||
var getMatchingUserOperationName = function(orgOperationName, method, userRelatedResource) {
|
||||
if (userRelatedResource) {
|
||||
if (userRelatedResource[method.toLowerCase()]) {
|
||||
return userRelatedResource[method.toLowerCase()]['operationId'];
|
||||
}
|
||||
}
|
||||
|
||||
throw new Error('Could not find user operation matching org operation: ' + orgOperationName);
|
||||
};
|
||||
|
||||
var freshLoginInProgress = [];
|
||||
var reject = function(msg) {
|
||||
for (var i = 0; i < freshLoginInProgress.length; ++i) {
|
||||
freshLoginInProgress[i].deferred.reject({'data': {'message': msg}});
|
||||
}
|
||||
freshLoginInProgress = [];
|
||||
};
|
||||
|
||||
var retry = function() {
|
||||
for (var i = 0; i < freshLoginInProgress.length; ++i) {
|
||||
freshLoginInProgress[i].retry();
|
||||
}
|
||||
freshLoginInProgress = [];
|
||||
};
|
||||
|
||||
var freshLoginFailCheck = function(opName, opArgs) {
|
||||
return function(resp) {
|
||||
var deferred = $q.defer();
|
||||
|
||||
// If the error is a fresh login required, show the dialog.
|
||||
// TODO: remove error_type (old style error)
|
||||
var fresh_login_required = resp.data['title'] == 'fresh_login_required' || resp.data['error_type'] == 'fresh_login_required';
|
||||
if (resp.status == 401 && fresh_login_required) {
|
||||
var retryOperation = function() {
|
||||
apiService[opName].apply(apiService, opArgs).then(function(resp) {
|
||||
deferred.resolve(resp);
|
||||
}, function(resp) {
|
||||
deferred.reject(resp);
|
||||
});
|
||||
};
|
||||
|
||||
var verifyNow = function() {
|
||||
if (!$('#freshPassword').val()) {
|
||||
return;
|
||||
}
|
||||
|
||||
var info = {
|
||||
'password': $('#freshPassword').val()
|
||||
};
|
||||
|
||||
$('#freshPassword').val('');
|
||||
|
||||
// Conduct the sign in of the user.
|
||||
apiService.verifyUser(info).then(function() {
|
||||
// On success, retry the operations. if it succeeds, then resolve the
|
||||
// deferred promise with the result. Otherwise, reject the same.
|
||||
retry();
|
||||
}, function(resp) {
|
||||
// Reject with the sign in error.
|
||||
reject('Invalid verification credentials');
|
||||
});
|
||||
};
|
||||
|
||||
// Add the retry call to the in progress list. If there is more than a single
|
||||
// in progress call, we skip showing the dialog (since it has already been
|
||||
// shown).
|
||||
freshLoginInProgress.push({
|
||||
'deferred': deferred,
|
||||
'retry': retryOperation
|
||||
})
|
||||
|
||||
if (freshLoginInProgress.length > 1) {
|
||||
return deferred.promise;
|
||||
}
|
||||
|
||||
var box = bootbox.dialog({
|
||||
"message": 'It has been more than a few minutes since you last logged in, ' +
|
||||
'so please verify your password to perform this sensitive operation:' +
|
||||
'<form style="margin-top: 10px" action="javascript:$(\'.btn-continue\').click();void(0)">' +
|
||||
'<input id="freshPassword" class="form-control" type="password" placeholder="Current Password">' +
|
||||
'</form>',
|
||||
"title": 'Please Verify',
|
||||
"buttons": {
|
||||
"verify": {
|
||||
"label": "Verify",
|
||||
"className": "btn-success btn-continue",
|
||||
"callback": verifyNow
|
||||
},
|
||||
"close": {
|
||||
"label": "Cancel",
|
||||
"className": "btn-default",
|
||||
"callback": function() {
|
||||
reject('Verification canceled')
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
box.bind('shown.bs.modal', function(){
|
||||
box.find("input").focus();
|
||||
box.find("form").submit(function() {
|
||||
if (!$('#freshPassword').val()) { return; }
|
||||
|
||||
box.modal('hide');
|
||||
verifyNow();
|
||||
});
|
||||
});
|
||||
|
||||
// Return a new promise. We'll accept or reject it based on the result
|
||||
// of the login.
|
||||
return deferred.promise;
|
||||
}
|
||||
|
||||
// Otherwise, we just 'raise' the error via the reject method on the promise.
|
||||
return $q.reject(resp);
|
||||
};
|
||||
};
|
||||
|
||||
var buildMethodsForOperation = function(operation, method, path, resourceMap) {
|
||||
var operationName = operation['operationId'];
|
||||
var urlPath = path['x-path'];
|
||||
|
||||
// Add the operation itself.
|
||||
apiService[operationName] = function(opt_options, opt_parameters, opt_background, opt_forceget) {
|
||||
var one = Restangular.one(buildUrl(urlPath, opt_parameters));
|
||||
if (opt_background) {
|
||||
one.withHttpConfig({
|
||||
'ignoreLoadingBar': true
|
||||
});
|
||||
}
|
||||
|
||||
var opObj = one[opt_forceget ? 'get' : 'custom' + method.toUpperCase()](opt_options);
|
||||
|
||||
// If the operation requires_fresh_login, then add a specialized error handler that
|
||||
// will defer the operation's result if sudo is requested.
|
||||
if (operation['x-requires-fresh-login']) {
|
||||
opObj = opObj.catch(freshLoginFailCheck(operationName, arguments));
|
||||
}
|
||||
return opObj;
|
||||
};
|
||||
|
||||
// If the method for the operation is a GET, add an operationAsResource method.
|
||||
if (method == 'get') {
|
||||
apiService[operationName + 'AsResource'] = function(opt_parameters, opt_background) {
|
||||
var getMethod = apiService[operationName];
|
||||
return getResource(getMethod, operation, opt_parameters, opt_background);
|
||||
};
|
||||
}
|
||||
|
||||
// If the operation has a user-related operation, then make a generic operation for this operation
|
||||
// that can call both the user and the organization versions of the operation, depending on the
|
||||
// parameters given.
|
||||
if (path['x-user-related']) {
|
||||
var userOperationName = getMatchingUserOperationName(operationName, method, resourceMap[path['x-user-related']]);
|
||||
var genericOperationName = getGenericOperationName(userOperationName);
|
||||
apiService[genericOperationName] = function(orgname, opt_options, opt_parameters, opt_background) {
|
||||
if (orgname) {
|
||||
if (orgname.name) {
|
||||
orgname = orgname.name;
|
||||
}
|
||||
|
||||
var params = jQuery.extend({'orgname' : orgname}, opt_parameters || {}, opt_background);
|
||||
return apiService[operationName](opt_options, params);
|
||||
} else {
|
||||
return apiService[userOperationName](opt_options, opt_parameters, opt_background);
|
||||
}
|
||||
};
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
var allowedMethods = ['get', 'post', 'put', 'delete'];
|
||||
var resourceMap = {};
|
||||
var forEachOperation = function(callback) {
|
||||
for (var path in window.__endpoints) {
|
||||
if (!window.__endpoints.hasOwnProperty(path)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
for (var method in window.__endpoints[path]) {
|
||||
if (!window.__endpoints[path].hasOwnProperty(method)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
if (allowedMethods.indexOf(method.toLowerCase()) < 0) { continue; }
|
||||
callback(window.__endpoints[path][method], method, window.__endpoints[path]);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
// Build the map of resource names to their objects.
|
||||
forEachOperation(function(operation, method, path) {
|
||||
resourceMap[path['x-name']] = path;
|
||||
});
|
||||
|
||||
// Construct the methods for each API endpoint.
|
||||
forEachOperation(function(operation, method, path) {
|
||||
buildMethodsForOperation(operation, method, path, resourceMap);
|
||||
});
|
||||
|
||||
apiService.getErrorMessage = function(resp, defaultMessage) {
|
||||
var message = defaultMessage;
|
||||
if (resp && resp['data']) {
|
||||
//TODO: remove error_message and error_description (old style error)
|
||||
message = resp['data']['detail'] || resp['data']['error_message'] || resp['data']['message'] || resp['data']['error_description'] || message;
|
||||
}
|
||||
|
||||
return message;
|
||||
};
|
||||
|
||||
apiService.errorDisplay = function(defaultMessage, opt_handler) {
|
||||
return function(resp) {
|
||||
var message = apiService.getErrorMessage(resp, defaultMessage);
|
||||
if (opt_handler) {
|
||||
var handlerMessage = opt_handler(resp);
|
||||
if (handlerMessage) {
|
||||
message = handlerMessage;
|
||||
}
|
||||
}
|
||||
|
||||
message = UtilService.stringToHTML(message);
|
||||
bootbox.dialog({
|
||||
"message": message,
|
||||
"title": defaultMessage || 'Request Failure',
|
||||
"buttons": {
|
||||
"close": {
|
||||
"label": "Close",
|
||||
"className": "btn-primary"
|
||||
}
|
||||
}
|
||||
});
|
||||
};
|
||||
};
|
||||
|
||||
// todo: remove hacks
|
||||
apiService.scGetConfig = () => new Promise(() => { hello: true });
|
||||
apiService.scRegistryStatus = () => new Promise(() => { hello: true });
|
||||
|
||||
return apiService;
|
||||
}]);
|
45
config_app/js/services/container-service.js
Normal file
45
config_app/js/services/container-service.js
Normal file
|
@ -0,0 +1,45 @@
|
|||
/**
|
||||
* Helper service for working with the registry's container. Only works in enterprise.
|
||||
*/
|
||||
angular.module('quay-config')
|
||||
.factory('ContainerService', ['ApiService', '$timeout', 'Restangular',
|
||||
function(ApiService, $timeout, Restangular) {
|
||||
var containerService = {};
|
||||
containerService.restartContainer = function(callback) {
|
||||
ApiService.scShutdownContainer(null, null).then(function(resp) {
|
||||
$timeout(callback, 2000);
|
||||
}, ApiService.errorDisplay('Cannot restart container. Please report this to support.'))
|
||||
};
|
||||
|
||||
containerService.scheduleStatusCheck = function(callback, opt_config) {
|
||||
$timeout(function() {
|
||||
containerService.checkStatus(callback, opt_config);
|
||||
}, 2000);
|
||||
};
|
||||
|
||||
containerService.checkStatus = function(callback, opt_config) {
|
||||
var errorHandler = function(resp) {
|
||||
if (resp.status == 404 || resp.status == 502 || resp.status == -1) {
|
||||
// Container has not yet come back up, so we schedule another check.
|
||||
containerService.scheduleStatusCheck(callback, opt_config);
|
||||
return;
|
||||
}
|
||||
|
||||
return ApiService.errorDisplay('Cannot load status. Please report this to support')(resp);
|
||||
};
|
||||
|
||||
// If config is specified, override the API base URL from this point onward.
|
||||
// TODO(jschorr): Find a better way than this. This is safe, since this will only be called
|
||||
// for a restart, but it is still ugly.
|
||||
if (opt_config && opt_config['SERVER_HOSTNAME']) {
|
||||
var scheme = opt_config['PREFERRED_URL_SCHEME'] || 'http';
|
||||
var baseUrl = scheme + '://' + opt_config['SERVER_HOSTNAME'] + '/api/v1/';
|
||||
Restangular.setBaseUrl(baseUrl);
|
||||
}
|
||||
|
||||
ApiService.scRegistryStatus(null, null, /* background */true)
|
||||
.then(callback, errorHandler);
|
||||
};
|
||||
|
||||
return containerService;
|
||||
}]);
|
23
config_app/js/services/cookie-service.js
Normal file
23
config_app/js/services/cookie-service.js
Normal file
|
@ -0,0 +1,23 @@
|
|||
/**
|
||||
* Helper service for working with cookies.
|
||||
*/
|
||||
angular.module('quay-config').factory('CookieService', ['$cookies', function($cookies) {
|
||||
var cookieService = {};
|
||||
cookieService.putPermanent = function(name, value) {
|
||||
document.cookie = escape(name) + "=" + escape(value) + "; expires=Fri, 31 Dec 9999 23:59:59 GMT; path=/";
|
||||
};
|
||||
|
||||
cookieService.putSession = function(name, value) {
|
||||
$cookies.put(name, value);
|
||||
};
|
||||
|
||||
cookieService.clear = function(name) {
|
||||
$cookies.remove(name);
|
||||
};
|
||||
|
||||
cookieService.get = function(name) {
|
||||
return $cookies.get(name);
|
||||
};
|
||||
|
||||
return cookieService;
|
||||
}]);
|
91
config_app/js/services/features-config.js
Normal file
91
config_app/js/services/features-config.js
Normal file
|
@ -0,0 +1,91 @@
|
|||
/**
|
||||
* Feature flags.
|
||||
*/
|
||||
angular.module('quay-config').factory('Features', [function() {
|
||||
if (!window.__features) {
|
||||
return {};
|
||||
}
|
||||
|
||||
var features = window.__features;
|
||||
features.getFeature = function(name, opt_defaultValue) {
|
||||
var value = features[name];
|
||||
if (value == null) {
|
||||
return opt_defaultValue;
|
||||
}
|
||||
return value;
|
||||
};
|
||||
|
||||
features.hasFeature = function(name) {
|
||||
return !!features.getFeature(name);
|
||||
};
|
||||
|
||||
features.matchesFeatures = function(list) {
|
||||
for (var i = 0; i < list.length; ++i) {
|
||||
var value = features.getFeature(list[i]);
|
||||
if (!value) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
return true;
|
||||
};
|
||||
|
||||
return features;
|
||||
}]);
|
||||
|
||||
/**
|
||||
* Application configuration.
|
||||
*/
|
||||
angular.module('quay-config').factory('Config', ['Features', function(Features) {
|
||||
if (!window.__config) {
|
||||
return {};
|
||||
}
|
||||
|
||||
var config = window.__config;
|
||||
config.getDomain = function() {
|
||||
return config['SERVER_HOSTNAME'];
|
||||
};
|
||||
|
||||
config.getHost = function(opt_auth) {
|
||||
var auth = opt_auth;
|
||||
if (auth) {
|
||||
auth = auth + '@';
|
||||
}
|
||||
|
||||
return config['PREFERRED_URL_SCHEME'] + '://' + auth + config['SERVER_HOSTNAME'];
|
||||
};
|
||||
|
||||
config.getHttp = function() {
|
||||
return config['PREFERRED_URL_SCHEME'];
|
||||
};
|
||||
|
||||
config.getUrl = function(opt_path) {
|
||||
var path = opt_path || '';
|
||||
return config['PREFERRED_URL_SCHEME'] + '://' + config['SERVER_HOSTNAME'] + path;
|
||||
};
|
||||
|
||||
config.getValue = function(name, opt_defaultValue) {
|
||||
var value = config[name];
|
||||
if (value == null) {
|
||||
return opt_defaultValue;
|
||||
}
|
||||
return value;
|
||||
};
|
||||
|
||||
config.getEnterpriseLogo = function(opt_defaultValue) {
|
||||
if (!config.ENTERPRISE_LOGO_URL) {
|
||||
if (opt_defaultValue) {
|
||||
return opt_defaultValue;
|
||||
}
|
||||
|
||||
if (Features.BILLING) {
|
||||
return '/static/img/quay-horizontal-color.svg';
|
||||
} else {
|
||||
return '/static/img/QuayEnterprise_horizontal_color.svg';
|
||||
}
|
||||
}
|
||||
|
||||
return config.ENTERPRISE_LOGO_URL;
|
||||
};
|
||||
|
||||
return config;
|
||||
}]);
|
217
config_app/js/services/user-service.js
Normal file
217
config_app/js/services/user-service.js
Normal file
|
@ -0,0 +1,217 @@
|
|||
import * as Raven from 'raven-js';
|
||||
|
||||
|
||||
/**
|
||||
* Service which monitors the current user session and provides methods for returning information
|
||||
* about the user.
|
||||
*/
|
||||
angular.module('quay-config')
|
||||
.factory('UserService', ['ApiService', 'CookieService', '$rootScope', 'Config', '$location', '$timeout',
|
||||
|
||||
function(ApiService, CookieService, $rootScope, Config, $location, $timeout) {
|
||||
var userResponse = {
|
||||
verified: false,
|
||||
anonymous: true,
|
||||
username: null,
|
||||
email: null,
|
||||
organizations: [],
|
||||
logins: [],
|
||||
beforeload: true
|
||||
};
|
||||
|
||||
var userService = {};
|
||||
var _EXTERNAL_SERVICES = ['ldap', 'jwtauthn', 'keystone', 'dex'];
|
||||
|
||||
userService.hasEverLoggedIn = function() {
|
||||
return CookieService.get('quay.loggedin') == 'true';
|
||||
};
|
||||
|
||||
userService.updateUserIn = function(scope, opt_callback) {
|
||||
scope.$watch(function () { return userService.currentUser(); }, function (currentUser) {
|
||||
if (currentUser) {
|
||||
$timeout(function(){
|
||||
scope.user = currentUser;
|
||||
if (opt_callback) {
|
||||
opt_callback(currentUser);
|
||||
}
|
||||
}, 0, false);
|
||||
};
|
||||
}, true);
|
||||
};
|
||||
|
||||
userService.load = function(opt_callback) {
|
||||
var handleUserResponse = function(loadedUser) {
|
||||
userResponse = loadedUser;
|
||||
|
||||
if (!userResponse.anonymous) {
|
||||
if (Config.MIXPANEL_KEY) {
|
||||
try {
|
||||
mixpanel.identify(userResponse.username);
|
||||
mixpanel.people.set({
|
||||
'$email': userResponse.email,
|
||||
'$username': userResponse.username,
|
||||
'verified': userResponse.verified
|
||||
});
|
||||
mixpanel.people.set_once({
|
||||
'$created': new Date()
|
||||
})
|
||||
} catch (e) {
|
||||
window.console.log(e);
|
||||
}
|
||||
}
|
||||
|
||||
if (Config.MARKETO_MUNCHKIN_ID && userResponse['marketo_user_hash']) {
|
||||
var associateLeadBody = {'Email': userResponse.email};
|
||||
if (window.Munchkin !== undefined) {
|
||||
try {
|
||||
Munchkin.munchkinFunction(
|
||||
'associateLead',
|
||||
associateLeadBody,
|
||||
userResponse['marketo_user_hash']
|
||||
);
|
||||
} catch (e) {
|
||||
}
|
||||
} else {
|
||||
window.__quay_munchkin_queue.push([
|
||||
'associateLead',
|
||||
associateLeadBody,
|
||||
userResponse['marketo_user_hash']
|
||||
]);
|
||||
}
|
||||
}
|
||||
|
||||
if (window.Raven !== undefined) {
|
||||
try {
|
||||
Raven.setUser({
|
||||
email: userResponse.email,
|
||||
id: userResponse.username
|
||||
});
|
||||
} catch (e) {
|
||||
window.console.log(e);
|
||||
}
|
||||
}
|
||||
|
||||
CookieService.putPermanent('quay.loggedin', 'true');
|
||||
} else {
|
||||
if (window.Raven !== undefined) {
|
||||
Raven.setUser();
|
||||
}
|
||||
}
|
||||
|
||||
// If the loaded user has a prompt, redirect them to the update page.
|
||||
if (loadedUser.prompts && loadedUser.prompts.length) {
|
||||
$location.path('/updateuser');
|
||||
return;
|
||||
}
|
||||
|
||||
if (opt_callback) {
|
||||
opt_callback(loadedUser);
|
||||
}
|
||||
};
|
||||
|
||||
ApiService.getLoggedInUser().then(function(loadedUser) {
|
||||
handleUserResponse(loadedUser);
|
||||
}, function() {
|
||||
handleUserResponse({'anonymous': true});
|
||||
});
|
||||
};
|
||||
|
||||
userService.isOrganization = function(name) {
|
||||
return !!userService.getOrganization(name);
|
||||
};
|
||||
|
||||
userService.getOrganization = function(name) {
|
||||
if (!userResponse || !userResponse.organizations) { return null; }
|
||||
for (var i = 0; i < userResponse.organizations.length; ++i) {
|
||||
var org = userResponse.organizations[i];
|
||||
if (org.name == name) {
|
||||
return org;
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
};
|
||||
|
||||
userService.isNamespaceAdmin = function(namespace) {
|
||||
if (namespace == userResponse.username) {
|
||||
return true;
|
||||
}
|
||||
|
||||
var org = userService.getOrganization(namespace);
|
||||
if (!org) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return org.is_org_admin;
|
||||
};
|
||||
|
||||
userService.isKnownNamespace = function(namespace) {
|
||||
if (namespace == userResponse.username) {
|
||||
return true;
|
||||
}
|
||||
|
||||
var org = userService.getOrganization(namespace);
|
||||
return !!org;
|
||||
};
|
||||
|
||||
userService.getNamespace = function(namespace) {
|
||||
var org = userService.getOrganization(namespace);
|
||||
if (org) {
|
||||
return org;
|
||||
}
|
||||
|
||||
if (namespace == userResponse.username) {
|
||||
return userResponse;
|
||||
}
|
||||
|
||||
return null;
|
||||
};
|
||||
|
||||
userService.getCLIUsername = function() {
|
||||
if (!userResponse) {
|
||||
return null;
|
||||
}
|
||||
|
||||
var externalUsername = null;
|
||||
userResponse.logins.forEach(function(login) {
|
||||
if (_EXTERNAL_SERVICES.indexOf(login.service) >= 0) {
|
||||
externalUsername = login.service_identifier;
|
||||
}
|
||||
});
|
||||
|
||||
return externalUsername || userResponse.username;
|
||||
};
|
||||
|
||||
userService.deleteNamespace = function(info, callback) {
|
||||
var namespace = info.user ? info.user.username : info.organization.name;
|
||||
if (!namespace) {
|
||||
return;
|
||||
}
|
||||
|
||||
var errorDisplay = ApiService.errorDisplay('Could not delete namespace', callback);
|
||||
var cb = function(resp) {
|
||||
userService.load(function(currentUser) {
|
||||
callback(true);
|
||||
$location.path('/');
|
||||
});
|
||||
}
|
||||
|
||||
if (info.user) {
|
||||
ApiService.deleteCurrentUser().then(cb, errorDisplay)
|
||||
} else {
|
||||
var delParams = {
|
||||
'orgname': info.organization.name
|
||||
};
|
||||
ApiService.deleteAdminedOrganization(null, delParams).then(cb, errorDisplay);
|
||||
}
|
||||
};
|
||||
|
||||
userService.currentUser = function() {
|
||||
return userResponse;
|
||||
};
|
||||
|
||||
// Update the user in the root scope.
|
||||
userService.updateUserIn($rootScope);
|
||||
|
||||
return userService;
|
||||
}]);
|
83
config_app/js/services/util-service.js
Normal file
83
config_app/js/services/util-service.js
Normal file
|
@ -0,0 +1,83 @@
|
|||
/**
|
||||
* Service which exposes various utility methods.
|
||||
*/
|
||||
angular.module('quay-config').factory('UtilService', ['$sanitize',
|
||||
function($sanitize) {
|
||||
var utilService = {};
|
||||
|
||||
var adBlockEnabled = null;
|
||||
|
||||
utilService.isAdBlockEnabled = function(callback) {
|
||||
if (adBlockEnabled !== null) {
|
||||
callback(adBlockEnabled);
|
||||
return;
|
||||
}
|
||||
|
||||
if(typeof blockAdBlock === 'undefined') {
|
||||
callback(true);
|
||||
return;
|
||||
}
|
||||
|
||||
var bab = new BlockAdBlock({
|
||||
checkOnLoad: false,
|
||||
resetOnEnd: true
|
||||
});
|
||||
|
||||
bab.onDetected(function() { adBlockEnabled = true; callback(true); });
|
||||
bab.onNotDetected(function() { adBlockEnabled = false; callback(false); });
|
||||
bab.check();
|
||||
};
|
||||
|
||||
utilService.isEmailAddress = function(val) {
|
||||
var emailRegex = /^[a-zA-Z0-9.!#$%&’*+/=?^_`{|}~-]+@[a-zA-Z0-9-]+(?:\.[a-zA-Z0-9-]+)*$/;
|
||||
return emailRegex.test(val);
|
||||
};
|
||||
|
||||
utilService.escapeHtmlString = function(text) {
|
||||
var textStr = (text || '').toString();
|
||||
var adjusted = textStr.replace(/&/g, "&")
|
||||
.replace(/</g, "<")
|
||||
.replace(/>/g, ">")
|
||||
.replace(/"/g, """)
|
||||
.replace(/'/g, "'");
|
||||
|
||||
return adjusted;
|
||||
};
|
||||
|
||||
utilService.stringToHTML = function(text) {
|
||||
text = utilService.escapeHtmlString(text);
|
||||
text = text.replace(/\n/g, '<br>');
|
||||
return text;
|
||||
};
|
||||
|
||||
utilService.getRestUrl = function(args) {
|
||||
var url = '';
|
||||
for (var i = 0; i < arguments.length; ++i) {
|
||||
if (i > 0) {
|
||||
url += '/';
|
||||
}
|
||||
url += encodeURI(arguments[i])
|
||||
}
|
||||
return url;
|
||||
};
|
||||
|
||||
utilService.textToSafeHtml = function(text) {
|
||||
return $sanitize(utilService.escapeHtmlString(text));
|
||||
};
|
||||
|
||||
return utilService;
|
||||
}])
|
||||
.factory('CoreDialog', [() => {
|
||||
var service = {};
|
||||
service['fatal'] = function(title, message) {
|
||||
bootbox.dialog({
|
||||
"title": title,
|
||||
"message": "<div class='alert-icon-container-container'><div class='alert-icon-container'><div class='alert-icon'></div></div></div>" + message,
|
||||
"buttons": {},
|
||||
"className": "co-dialog fatal-error",
|
||||
"closeButton": false
|
||||
});
|
||||
};
|
||||
|
||||
return service;
|
||||
}]);
|
332
config_app/js/setup/setup.component.js
Normal file
332
config_app/js/setup/setup.component.js
Normal file
|
@ -0,0 +1,332 @@
|
|||
import * as URI from 'urijs';
|
||||
const templateUrl = require('./setup.html');
|
||||
|
||||
(function() {
|
||||
/**
|
||||
* The Setup page provides a nice GUI walkthrough experience for setting up Quay Enterprise.
|
||||
*/
|
||||
|
||||
angular.module('quay-config').directive('setup', () => {
|
||||
const directiveDefinitionObject = {
|
||||
priority: 1,
|
||||
templateUrl,
|
||||
replace: true,
|
||||
transclude: true,
|
||||
restrict: 'C',
|
||||
scope: {
|
||||
'isActive': '=isActive',
|
||||
'configurationSaved': '&configurationSaved'
|
||||
},
|
||||
controller: SetupCtrl,
|
||||
};
|
||||
|
||||
return directiveDefinitionObject;
|
||||
})
|
||||
|
||||
function SetupCtrl($scope, $timeout, ApiService, Features, UserService, ContainerService, CoreDialog) {
|
||||
// if (!Features.SUPER_USERS) {
|
||||
// return;
|
||||
// }
|
||||
|
||||
$scope.HOSTNAME_REGEX = '^[a-zA-Z-0-9_\.\-]+(:[0-9]+)?$';
|
||||
|
||||
$scope.validateHostname = function(hostname) {
|
||||
if (hostname.indexOf('127.0.0.1') == 0 || hostname.indexOf('localhost') == 0) {
|
||||
return 'Please specify a non-localhost hostname. "localhost" will refer to the container, not your machine.'
|
||||
}
|
||||
|
||||
return null;
|
||||
};
|
||||
|
||||
// Note: The values of the enumeration are important for isStepFamily. For example,
|
||||
// *all* states under the "configuring db" family must start with "config-db".
|
||||
$scope.States = {
|
||||
// Loading the state of the product.
|
||||
'LOADING': 'loading',
|
||||
|
||||
// The configuration directory is missing.
|
||||
'MISSING_CONFIG_DIR': 'missing-config-dir',
|
||||
|
||||
// The config.yaml exists but it is invalid.
|
||||
'INVALID_CONFIG': 'config-invalid',
|
||||
|
||||
// DB is being configured.
|
||||
'CONFIG_DB': 'config-db',
|
||||
|
||||
// DB information is being validated.
|
||||
'VALIDATING_DB': 'config-db-validating',
|
||||
|
||||
// DB information is being saved to the config.
|
||||
'SAVING_DB': 'config-db-saving',
|
||||
|
||||
// A validation error occurred with the database.
|
||||
'DB_ERROR': 'config-db-error',
|
||||
|
||||
// Database is being setup.
|
||||
'DB_SETUP': 'setup-db',
|
||||
|
||||
// Database setup has succeeded.
|
||||
'DB_SETUP_SUCCESS': 'setup-db-success',
|
||||
|
||||
// An error occurred when setting up the database.
|
||||
'DB_SETUP_ERROR': 'setup-db-error',
|
||||
|
||||
// The container is being restarted for the database changes.
|
||||
'DB_RESTARTING': 'setup-db-restarting',
|
||||
|
||||
// A superuser is being configured.
|
||||
'CREATE_SUPERUSER': 'create-superuser',
|
||||
|
||||
// The superuser is being created.
|
||||
'CREATING_SUPERUSER': 'create-superuser-creating',
|
||||
|
||||
// An error occurred when setting up the superuser.
|
||||
'SUPERUSER_ERROR': 'create-superuser-error',
|
||||
|
||||
// The superuser was created successfully.
|
||||
'SUPERUSER_CREATED': 'create-superuser-created',
|
||||
|
||||
// General configuration is being setup.
|
||||
'CONFIG': 'config',
|
||||
|
||||
// The configuration is fully valid.
|
||||
'VALID_CONFIG': 'valid-config',
|
||||
|
||||
// The container is being restarted for the configuration changes.
|
||||
'CONFIG_RESTARTING': 'config-restarting',
|
||||
|
||||
// The product is ready for use.
|
||||
'READY': 'ready'
|
||||
}
|
||||
|
||||
$scope.csrf_token = window.__token;
|
||||
$scope.currentStep = $scope.States.LOADING;
|
||||
$scope.errors = {};
|
||||
$scope.stepProgress = [];
|
||||
$scope.hasSSL = false;
|
||||
$scope.hostname = null;
|
||||
$scope.currentConfig = null;
|
||||
|
||||
$scope.currentState = {
|
||||
'hasDatabaseSSLCert': false
|
||||
};
|
||||
|
||||
$scope.$watch('currentStep', function(currentStep) {
|
||||
$scope.stepProgress = $scope.getProgress(currentStep);
|
||||
|
||||
switch (currentStep) {
|
||||
case $scope.States.CONFIG:
|
||||
$('#setupModal').modal('hide');
|
||||
break;
|
||||
|
||||
case $scope.States.MISSING_CONFIG_DIR:
|
||||
$scope.showMissingConfigDialog();
|
||||
break;
|
||||
|
||||
case $scope.States.INVALID_CONFIG:
|
||||
$scope.showInvalidConfigDialog();
|
||||
break;
|
||||
|
||||
case $scope.States.DB_SETUP:
|
||||
$scope.performDatabaseSetup();
|
||||
// Fall-through.
|
||||
|
||||
case $scope.States.CREATE_SUPERUSER:
|
||||
case $scope.States.DB_RESTARTING:
|
||||
case $scope.States.CONFIG_DB:
|
||||
case $scope.States.VALID_CONFIG:
|
||||
case $scope.States.READY:
|
||||
$('#setupModal').modal({
|
||||
keyboard: false,
|
||||
backdrop: 'static'
|
||||
});
|
||||
break;
|
||||
}
|
||||
});
|
||||
|
||||
$scope.restartContainer = function(state) {
|
||||
$scope.currentStep = state;
|
||||
ContainerService.restartContainer(function() {
|
||||
$scope.checkStatus()
|
||||
});
|
||||
};
|
||||
|
||||
$scope.showSuperuserPanel = function() {
|
||||
$('#setupModal').modal('hide');
|
||||
var prefix = $scope.hasSSL ? 'https' : 'http';
|
||||
var hostname = $scope.hostname;
|
||||
if (!hostname) {
|
||||
hostname = document.location.hostname;
|
||||
if (document.location.port) {
|
||||
hostname = hostname + ':' + document.location.port;
|
||||
}
|
||||
}
|
||||
|
||||
window.location = prefix + '://' + hostname + '/superuser';
|
||||
};
|
||||
|
||||
$scope.configurationSaved = function(config) {
|
||||
$scope.hasSSL = config['PREFERRED_URL_SCHEME'] == 'https';
|
||||
$scope.hostname = config['SERVER_HOSTNAME'];
|
||||
$scope.currentConfig = config;
|
||||
|
||||
$scope.currentStep = $scope.States.VALID_CONFIG;
|
||||
};
|
||||
|
||||
$scope.getProgress = function(step) {
|
||||
var isStep = $scope.isStep;
|
||||
var isStepFamily = $scope.isStepFamily;
|
||||
var States = $scope.States;
|
||||
|
||||
return [
|
||||
isStepFamily(step, States.CONFIG_DB),
|
||||
isStepFamily(step, States.DB_SETUP),
|
||||
isStep(step, States.DB_RESTARTING),
|
||||
isStepFamily(step, States.CREATE_SUPERUSER),
|
||||
isStep(step, States.CONFIG),
|
||||
isStep(step, States.VALID_CONFIG),
|
||||
isStep(step, States.CONFIG_RESTARTING),
|
||||
isStep(step, States.READY)
|
||||
];
|
||||
};
|
||||
|
||||
$scope.isStepFamily = function(step, family) {
|
||||
if (!step) { return false; }
|
||||
return step.indexOf(family) == 0;
|
||||
};
|
||||
|
||||
$scope.isStep = function(step) {
|
||||
for (var i = 1; i < arguments.length; ++i) {
|
||||
if (arguments[i] == step) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
};
|
||||
|
||||
$scope.beginSetup = function() {
|
||||
$scope.currentStep = $scope.States.CONFIG_DB;
|
||||
};
|
||||
|
||||
$scope.showInvalidConfigDialog = function() {
|
||||
var message = "The <code>config.yaml</code> file found in <code>conf/stack</code> could not be parsed."
|
||||
var title = "Invalid configuration file";
|
||||
CoreDialog.fatal(title, message);
|
||||
};
|
||||
|
||||
|
||||
$scope.showMissingConfigDialog = function() {
|
||||
var message = "A volume should be mounted into the container at <code>/conf/stack</code>: " +
|
||||
"<br><br><pre>docker run -v /path/to/config:/conf/stack</pre>" +
|
||||
"<br>Once fixed, restart the container. For more information, " +
|
||||
"<a href='https://coreos.com/docs/enterprise-registry/initial-setup/'>" +
|
||||
"Read the Setup Guide</a>"
|
||||
|
||||
var title = "Missing configuration volume";
|
||||
CoreDialog.fatal(title, message);
|
||||
};
|
||||
|
||||
$scope.parseDbUri = function(value) {
|
||||
if (!value) { return null; }
|
||||
|
||||
// Format: mysql+pymysql://<username>:<url escaped password>@<hostname>/<database_name>
|
||||
var uri = URI(value);
|
||||
return {
|
||||
'kind': uri.protocol(),
|
||||
'username': uri.username(),
|
||||
'password': uri.password(),
|
||||
'server': uri.host(),
|
||||
'database': uri.path() ? uri.path().substr(1) : ''
|
||||
};
|
||||
};
|
||||
|
||||
$scope.serializeDbUri = function(fields) {
|
||||
if (!fields['server']) { return ''; }
|
||||
if (!fields['database']) { return ''; }
|
||||
|
||||
var uri = URI();
|
||||
try {
|
||||
uri = uri && uri.host(fields['server']);
|
||||
uri = uri && uri.protocol(fields['kind']);
|
||||
uri = uri && uri.username(fields['username']);
|
||||
uri = uri && uri.password(fields['password']);
|
||||
uri = uri && uri.path('/' + (fields['database'] || ''));
|
||||
uri = uri && uri.toString();
|
||||
} catch (ex) {
|
||||
return '';
|
||||
}
|
||||
|
||||
return uri;
|
||||
};
|
||||
|
||||
$scope.createSuperUser = function() {
|
||||
$scope.currentStep = $scope.States.CREATING_SUPERUSER;
|
||||
ApiService.scCreateInitialSuperuser($scope.superUser, null).then(function(resp) {
|
||||
UserService.load();
|
||||
$scope.checkStatus();
|
||||
}, function(resp) {
|
||||
$scope.currentStep = $scope.States.SUPERUSER_ERROR;
|
||||
$scope.errors.SuperuserCreationError = ApiService.getErrorMessage(resp, 'Could not create superuser');
|
||||
});
|
||||
};
|
||||
|
||||
$scope.performDatabaseSetup = function() {
|
||||
$scope.currentStep = $scope.States.DB_SETUP;
|
||||
ApiService.scSetupDatabase(null, null).then(function(resp) {
|
||||
if (resp['error']) {
|
||||
$scope.currentStep = $scope.States.DB_SETUP_ERROR;
|
||||
$scope.errors.DatabaseSetupError = resp['error'];
|
||||
} else {
|
||||
$scope.currentStep = $scope.States.DB_SETUP_SUCCESS;
|
||||
}
|
||||
}, ApiService.errorDisplay('Could not setup database. Please report this to support.'))
|
||||
};
|
||||
|
||||
$scope.validateDatabase = function() {
|
||||
$scope.currentStep = $scope.States.VALIDATING_DB;
|
||||
$scope.databaseInvalid = null;
|
||||
|
||||
var data = {
|
||||
'config': {
|
||||
'DB_URI': $scope.databaseUri
|
||||
},
|
||||
'hostname': window.location.host
|
||||
};
|
||||
|
||||
if ($scope.currentState.hasDatabaseSSLCert) {
|
||||
data['config']['DB_CONNECTION_ARGS'] = {
|
||||
'ssl': {
|
||||
'ca': 'conf/stack/database.pem'
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
var params = {
|
||||
'service': 'database'
|
||||
};
|
||||
|
||||
ApiService.scValidateConfig(data, params).then(function(resp) {
|
||||
var status = resp.status;
|
||||
|
||||
if (status) {
|
||||
$scope.currentStep = $scope.States.SAVING_DB;
|
||||
ApiService.scUpdateConfig(data, null).then(function(resp) {
|
||||
$scope.checkStatus();
|
||||
}, ApiService.errorDisplay('Cannot update config. Please report this to support'));
|
||||
} else {
|
||||
$scope.currentStep = $scope.States.DB_ERROR;
|
||||
$scope.errors.DatabaseValidationError = resp.reason;
|
||||
}
|
||||
}, ApiService.errorDisplay('Cannot validate database. Please report this to support'));
|
||||
};
|
||||
|
||||
$scope.checkStatus = function() {
|
||||
ContainerService.checkStatus(function(resp) {
|
||||
$scope.currentStep = resp['status'];
|
||||
}, $scope.currentConfig);
|
||||
};
|
||||
|
||||
// Load the initial status.
|
||||
$scope.checkStatus();
|
||||
};
|
||||
})();
|
311
config_app/js/setup/setup.html
Normal file
311
config_app/js/setup/setup.html
Normal file
|
@ -0,0 +1,311 @@
|
|||
<div>
|
||||
<div>
|
||||
<div class="cor-loader" ng-show="currentStep == States.LOADING"></div>
|
||||
<div class="page-content" quay-show="Features.SUPER_USERS && currentStep == States.CONFIG">
|
||||
<div class="cor-title">
|
||||
<span class="cor-title-link"></span>
|
||||
<span class="cor-title-content">Quay Enterprise Setup</span>
|
||||
</div>
|
||||
|
||||
<div class="co-main-content-panel" style="padding: 20px;">
|
||||
<div class="co-alert alert alert-info">
|
||||
<span class="cor-step-bar" progress="stepProgress">
|
||||
<span class="cor-step" title="Configure Database" text="1"></span>
|
||||
<span class="cor-step" title="Setup Database" icon="database"></span>
|
||||
<span class="cor-step" title="Container Restart" icon="refresh"></span>
|
||||
<span class="cor-step" title="Create Superuser" text="2"></span>
|
||||
<span class="cor-step" title="Configure Registry" text="3"></span>
|
||||
<span class="cor-step" title="Validate Configuration" text="4"></span>
|
||||
<span class="cor-step" title="Container Restart" icon="refresh"></span>
|
||||
<span class="cor-step" title="Setup Complete" icon="check"></span>
|
||||
</span>
|
||||
|
||||
<div><strong>Almost done!</strong></div>
|
||||
<div>Configure your Redis database and other settings below</div>
|
||||
</div>
|
||||
|
||||
<!--<config-setup-tool is-active="isStep(currentStep, States.CONFIG)"-->
|
||||
<!--configuration-saved="configurationSaved(config)"></config-setup-tool>-->
|
||||
<div class="config-setup-tool" is-active="true"
|
||||
configuration-saved="configurationSaved(config)"></div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Modal message dialog -->
|
||||
<div class="co-dialog modal fade initial-setup-modal" id="setupModal">
|
||||
<div class="modal-dialog">
|
||||
<div class="modal-content">
|
||||
<!-- Header -->
|
||||
<div class="modal-header">
|
||||
<span class="cor-step-bar" progress="stepProgress">
|
||||
<span class="cor-step" title="Configure Database" text="1"></span>
|
||||
<span class="cor-step" title="Setup Database" icon="database"></span>
|
||||
<span class="cor-step" title="Container Restart" icon="refresh"></span>
|
||||
<span class="cor-step" title="Create Superuser" text="2"></span>
|
||||
<span class="cor-step" title="Configure Registry" text="3"></span>
|
||||
<span class="cor-step" title="Validate Configuration" text="4"></span>
|
||||
<span class="cor-step" title="Container Restart" icon="refresh"></span>
|
||||
<span class="cor-step" title="Setup Complete" icon="check"></span>
|
||||
</span>
|
||||
<h4 class="modal-title"><span><span class="registry-name" is-short="true"></span> Setup</h4>
|
||||
</div>
|
||||
|
||||
<form id="superuserForm" name="superuserForm" ng-submit="createSuperUser()">
|
||||
<!-- Content: CREATE_SUPERUSER or SUPERUSER_ERROR or CREATING_SUPERUSER -->
|
||||
<div class="modal-body config-setup-tool-element" style="padding: 20px"
|
||||
ng-show="isStep(currentStep, States.CREATE_SUPERUSER, States.SUPERUSER_ERROR, States.CREATING_SUPERUSER)">
|
||||
<p>A superuser is the main administrator of your <span class="registry-name" is-short="true"></span>. Only superusers can edit configuration settings.</p>
|
||||
|
||||
<div class="form-group">
|
||||
<label>Username</label>
|
||||
<input class="form-control" type="text" ng-model="superUser.username"
|
||||
ng-pattern="/^[a-z0-9_]{4,30}$/" required>
|
||||
<div class="help-text">Minimum 4 characters in length</div>
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<label>Email address</label>
|
||||
<input class="form-control" type="email" ng-model="superUser.email" required>
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<label>Password</label>
|
||||
<input class="form-control" type="password" ng-model="superUser.password"
|
||||
ng-pattern="/^[^\s]+$/"
|
||||
ng-minlength="8" required>
|
||||
<div class="help-text">Minimum 8 characters in length</div>
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<label>Repeat Password</label>
|
||||
<input class="form-control" type="password" ng-model="superUser.repeatPassword"
|
||||
match="superUser.password" required>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Footer: CREATE_SUPERUSER or SUPERUSER_ERROR -->
|
||||
<div class="modal-footer"
|
||||
ng-show="isStep(currentStep, States.CREATE_SUPERUSER, States.SUPERUSER_ERROR)">
|
||||
<button type="submit" class="btn btn-primary" ng-disabled="!superuserForm.$valid">
|
||||
Create Super User
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
||||
|
||||
<!-- Content: DB_RESTARTING or CONFIG_RESTARTING -->
|
||||
<div class="modal-body" style="padding: 20px;"
|
||||
ng-show="isStep(currentStep, States.DB_RESTARTING, States.CONFIG_RESTARTING)">
|
||||
<h4 style="margin-bottom: 20px;">
|
||||
<i class="fa fa-lg fa-refresh" style="margin-right: 10px;"></i>
|
||||
<span class="registry-name"></span> is currently being restarted
|
||||
</h4>
|
||||
This can take several minutes. If the container does not restart on its own,
|
||||
please re-execute the <code>docker run</code> command.
|
||||
</div>
|
||||
|
||||
<!-- Content: READY -->
|
||||
<div class="modal-body" style="padding: 20px;"
|
||||
ng-show="isStep(currentStep, States.READY)">
|
||||
<h4>Installation and setup of <span class="registry-name"></span> is complete</h4>
|
||||
You can now invite users to join, create organizations and start pushing and pulling
|
||||
repositories.
|
||||
|
||||
<strong ng-if="hasSSL" style="margin-top: 20px;">
|
||||
Note: SSL is enabled. Please make sure to visit with
|
||||
an <u>https</u> prefix
|
||||
</strong>
|
||||
</div>
|
||||
|
||||
<!-- Content: VALID_CONFIG -->
|
||||
<div class="modal-body" style="padding: 20px;"
|
||||
ng-show="isStep(currentStep, States.VALID_CONFIG)">
|
||||
<h4>All configuration has been validated and saved</h4>
|
||||
The container must be restarted to apply the configuration changes.
|
||||
</div>
|
||||
|
||||
<!-- Content: DB_SETUP_SUCCESS -->
|
||||
<div class="modal-body" style="padding: 20px;"
|
||||
ng-show="isStep(currentStep, States.DB_SETUP_SUCCESS)">
|
||||
<h4>The database has been setup and is ready</h4>
|
||||
The container must be restarted to apply the configuration changes.
|
||||
</div>
|
||||
|
||||
<!-- Content: DB_SETUP or DB_SETUP_ERROR -->
|
||||
<div class="modal-body" style="padding: 20px;"
|
||||
ng-show="isStep(currentStep, States.DB_SETUP, States.DB_SETUP_ERROR)">
|
||||
<h4>
|
||||
<i class="fa fa-lg fa-database" style="margin-right: 10px;"></i>
|
||||
<span class="registry-name"></span> is currently setting up its database
|
||||
schema
|
||||
</h4>
|
||||
This can take several minutes.
|
||||
</div>
|
||||
|
||||
<!-- Content: CONFIG_DB or DB_ERROR or VALIDATING_DB or SAVING_DB -->
|
||||
<div class="modal-body validate-database config-setup-tool-element"
|
||||
ng-show="isStep(currentStep, States.CONFIG_DB, States.DB_ERROR, States.VALIDATING_DB, States.SAVING_DB)">
|
||||
<p>
|
||||
Please enter the connection details for your <strong>empty</strong> database. The schema will be created in the following step.</p>
|
||||
</p>
|
||||
|
||||
<div class="config-parsed-field" binding="databaseUri"
|
||||
parser="parseDbUri(value)"
|
||||
serializer="serializeDbUri(fields)">
|
||||
<table class="config-table">
|
||||
<tr>
|
||||
<td class="non-input">Database Type:</td>
|
||||
<td>
|
||||
<select ng-model="fields.kind">
|
||||
<option value="mysql+pymysql">MySQL</option>
|
||||
<option value="postgresql">Postgres</option>
|
||||
</select>
|
||||
</td>
|
||||
</tr>
|
||||
<tr ng-show="fields.kind">
|
||||
<td>Database Server:</td>
|
||||
<td>
|
||||
<span class="config-string-field" binding="fields.server"
|
||||
placeholder="dbserverhost"
|
||||
pattern="{{ HOSTNAME_REGEX }}"
|
||||
validator="validateHostname(value)">></span>
|
||||
<div class="help-text">
|
||||
The server (and optionally, custom port) where the database lives
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
<tr ng-show="fields.kind">
|
||||
<td>Username:</td>
|
||||
<td>
|
||||
<span class="config-string-field" binding="fields.username"
|
||||
placeholder="someuser"></span>
|
||||
<div class="help-text">This user must have <strong>full access</strong> to the database</div>
|
||||
</td>
|
||||
</tr>
|
||||
<tr ng-show="fields.kind">
|
||||
<td>Password:</td>
|
||||
<td>
|
||||
<input class="form-control" type="password" ng-model="fields.password"></span>
|
||||
</td>
|
||||
</tr>
|
||||
<tr ng-show="fields.kind">
|
||||
<td>Database Name:</td>
|
||||
<td>
|
||||
<span class="config-string-field" binding="fields.database"
|
||||
placeholder="registry-database"></span>
|
||||
</td>
|
||||
</tr>
|
||||
<tr ng-show="fields.kind">
|
||||
<td>SSL Certificate:</td>
|
||||
<td>
|
||||
<span class="config-file-field" filename="database.pem"
|
||||
skip-check-file="true" has-file="currentState.hasDatabaseSSLCert"></span>
|
||||
<div class="help-text">Optional SSL certicate (in PEM format) to use to connect to the database</div>
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Footer: CREATING_SUPERUSER -->
|
||||
<div class="modal-footer working" ng-show="isStep(currentStep, States.CREATING_SUPERUSER)">
|
||||
<span class="cor-loader-inline"></span> Creating superuser...
|
||||
</div>
|
||||
|
||||
<!-- Footer: SUPERUSER_ERROR -->
|
||||
<div class="modal-footer alert alert-warning"
|
||||
ng-show="isStep(currentStep, States.SUPERUSER_ERROR)">
|
||||
{{ errors.SuperuserCreationError }}
|
||||
</div>
|
||||
|
||||
<!-- Footer: DB_SETUP_ERROR -->
|
||||
<div class="modal-footer alert alert-warning"
|
||||
ng-show="isStep(currentStep, States.DB_SETUP_ERROR)">
|
||||
Database Setup Failed. Please report this to support: {{ errors.DatabaseSetupError }}
|
||||
</div>
|
||||
|
||||
<!-- Footer: DB_ERROR -->
|
||||
<div class="modal-footer alert alert-warning" ng-show="isStep(currentStep, States.DB_ERROR)">
|
||||
Database Validation Issue: {{ errors.DatabaseValidationError }}
|
||||
</div>
|
||||
|
||||
<!-- Footer: CONFIG_DB or DB_ERROR -->
|
||||
<div class="modal-footer"
|
||||
ng-show="isStep(currentStep, States.CONFIG_DB, States.DB_ERROR)">
|
||||
<span class="left-align" ng-show="isStep(currentStep, States.DB_ERROR)">
|
||||
<i class="fa fa-warning"></i>
|
||||
Problem Detected
|
||||
</span>
|
||||
|
||||
<button type="submit" class="btn btn-primary"
|
||||
ng-disabled="!databaseUri"
|
||||
ng-click="validateDatabase()">
|
||||
Validate Database Settings
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<!-- Footer: READY -->
|
||||
<div class="modal-footer"
|
||||
ng-show="isStep(currentStep, States.READY)">
|
||||
<span class="left-align">
|
||||
<i class="fa fa-check"></i>
|
||||
Installation Complete!
|
||||
</span>
|
||||
|
||||
<a ng-click="showSuperuserPanel()" class="btn btn-primary">
|
||||
View Superuser Panel
|
||||
</a>
|
||||
</div>
|
||||
|
||||
<!-- Footer: VALID_CONFIG -->
|
||||
<div class="modal-footer"
|
||||
ng-show="isStep(currentStep, States.VALID_CONFIG)">
|
||||
<span class="left-align">
|
||||
<i class="fa fa-check"></i>
|
||||
Configuration Validated and Saved
|
||||
</span>
|
||||
|
||||
<button type="submit" class="btn btn-primary"
|
||||
ng-click="restartContainer(States.CONFIG_RESTARTING)">
|
||||
Restart Container
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<!-- Footer: DB_SETUP_SUCCESS -->
|
||||
<div class="modal-footer"
|
||||
ng-show="isStep(currentStep, States.DB_SETUP_SUCCESS)">
|
||||
<span class="left-align">
|
||||
<i class="fa fa-check"></i>
|
||||
Database Setup and Ready
|
||||
</span>
|
||||
|
||||
<button type="submit" class="btn btn-primary"
|
||||
ng-click="restartContainer(States.DB_RESTARTING)">
|
||||
Restart Container
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<!-- Footer: DB_SETUP -->
|
||||
<div class="modal-footer working" ng-show="isStep(currentStep, States.DB_SETUP)">
|
||||
<span class="cor-loader-inline"></span> Setting up database...
|
||||
</div>
|
||||
|
||||
<!-- Footer: SAVING_DB -->
|
||||
<div class="modal-footer working" ng-show="isStep(currentStep, States.SAVING_DB)">
|
||||
<span class="cor-loader-inline"></span> Saving database configuration...
|
||||
</div>
|
||||
|
||||
<!-- Footer: VALIDATING_DB -->
|
||||
<div class="modal-footer working" ng-show="isStep(currentStep, States.VALIDATING_DB)">
|
||||
<span class="cor-loader-inline"></span> Testing database settings...
|
||||
</div>
|
||||
|
||||
<!-- Footer: DB_RESTARTING or CONFIG_RESTARTING-->
|
||||
<div class="modal-footer working"
|
||||
ng-show="isStep(currentStep, States.DB_RESTARTING, States.CONFIG_RESTARTING)">
|
||||
<span class="cor-loader-inline"></span> Waiting for container to restart...
|
||||
</div>
|
||||
</div><!-- /.modal-content -->
|
||||
</div><!-- /.modal-dialog -->
|
||||
</div><!-- /.modal -->
|
||||
</div>
|
1500
config_app/static/css/core-ui.css
Normal file
1500
config_app/static/css/core-ui.css
Normal file
File diff suppressed because it is too large
Load diff
2
config_app/static/lib/angular-file-upload.min.js
vendored
Normal file
2
config_app/static/lib/angular-file-upload.min.js
vendored
Normal file
|
@ -0,0 +1,2 @@
|
|||
/*! 1.4.0 */
|
||||
!function(){var a=angular.module("angularFileUpload",[]);a.service("$upload",["$http","$timeout",function(a,b){function c(c){c.method=c.method||"POST",c.headers=c.headers||{},c.transformRequest=c.transformRequest||function(b,c){return window.ArrayBuffer&&b instanceof window.ArrayBuffer?b:a.defaults.transformRequest[0](b,c)},window.XMLHttpRequest.__isShim&&(c.headers.__setXHR_=function(){return function(a){a&&(c.__XHR=a,c.xhrFn&&c.xhrFn(a),a.upload.addEventListener("progress",function(a){c.progress&&b(function(){c.progress&&c.progress(a)})},!1),a.upload.addEventListener("load",function(a){a.lengthComputable&&c.progress&&c.progress(a)},!1))}});var d=a(c);return d.progress=function(a){return c.progress=a,d},d.abort=function(){return c.__XHR&&b(function(){c.__XHR.abort()}),d},d.xhr=function(a){return c.xhrFn=a,d},d.then=function(a,b){return function(d,e,f){c.progress=f||c.progress;var g=b.apply(a,[d,e,f]);return g.abort=a.abort,g.progress=a.progress,g.xhr=a.xhr,g.then=a.then,g}}(d,d.then),d}this.upload=function(b){b.headers=b.headers||{},b.headers["Content-Type"]=void 0,b.transformRequest=b.transformRequest||a.defaults.transformRequest;var d=new FormData,e=b.transformRequest,f=b.data;return b.transformRequest=function(a,c){if(f)if(b.formDataAppender)for(var d in f){var g=f[d];b.formDataAppender(a,d,g)}else for(var d in f){var g=f[d];if("function"==typeof e)g=e(g,c);else for(var h=0;h<e.length;h++){var i=e[h];"function"==typeof i&&(g=i(g,c))}a.append(d,g)}if(null!=b.file){var j=b.fileFormDataName||"file";if("[object Array]"===Object.prototype.toString.call(b.file))for(var k="[object String]"===Object.prototype.toString.call(j),h=0;h<b.file.length;h++)a.append(k?j+h:j[h],b.file[h],b.file[h].name);else a.append(j,b.file,b.file.name)}return a},b.data=d,c(b)},this.http=function(a){return c(a)}}]),a.directive("ngFileSelect",["$parse","$timeout",function(a,b){return function(c,d,e){var f=a(e.ngFileSelect);d.bind("change",function(a){var d,e,g=[];if(d=a.target.files,null!=d)for(e=0;e<d.length;e++)g.push(d.item(e));b(function(){f(c,{$files:g,$event:a})})}),("ontouchstart"in window||navigator.maxTouchPoints>0||navigator.msMaxTouchPoints>0)&&d.bind("touchend",function(a){a.preventDefault(),a.target.click()})}}]),a.directive("ngFileDropAvailable",["$parse","$timeout",function(a,b){return function(c,d,e){if("draggable"in document.createElement("span")){var f=a(e.ngFileDropAvailable);b(function(){f(c)})}}}]),a.directive("ngFileDrop",["$parse","$timeout",function(a,b){return function(c,d,e){function f(a,b){if(b.isDirectory){var c=b.createReader();i++,c.readEntries(function(b){for(var c=0;c<b.length;c++)f(a,b[c]);i--})}else i++,b.file(function(b){i--,a.push(b)})}if("draggable"in document.createElement("span")){var g=null,h=a(e.ngFileDrop);d[0].addEventListener("dragover",function(a){b.cancel(g),a.stopPropagation(),a.preventDefault(),d.addClass(e.ngFileDragOverClass||"dragover")},!1),d[0].addEventListener("dragleave",function(){g=b(function(){d.removeClass(e.ngFileDragOverClass||"dragover")})},!1);var i=0;d[0].addEventListener("drop",function(a){a.stopPropagation(),a.preventDefault(),d.removeClass(e.ngFileDragOverClass||"dragover");var g=[],j=a.dataTransfer.items;if(j&&j.length>0&&j[0].webkitGetAsEntry)for(var k=0;k<j.length;k++)f(g,j[k].webkitGetAsEntry());else{var l=a.dataTransfer.files;if(null!=l)for(var k=0;k<l.length;k++)g.push(l.item(k))}!function m(d){b(function(){i?m(10):h(c,{$files:g,$event:a})},d||0)}()},!1)}}}])}();
|
|
@ -1,12 +0,0 @@
|
|||
<!DOCTYPE html>
|
||||
<html ng-app="quay">
|
||||
<head>
|
||||
<title>Config app</title>
|
||||
</head>
|
||||
<body>
|
||||
<div>
|
||||
<p>What is my purpose</p>
|
||||
<p>You make tarballs</p>
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
51
config_app/templates/index.html
Normal file
51
config_app/templates/index.html
Normal file
|
@ -0,0 +1,51 @@
|
|||
<!DOCTYPE html>
|
||||
<html ng-app="quay-config">
|
||||
<head>
|
||||
<script type="text/javascript">
|
||||
window.__endpoints = {{ route_data|tojson|safe }}.paths;
|
||||
</script>
|
||||
|
||||
|
||||
<!--REMOTE CSS-->
|
||||
<link rel="stylesheet" href="//netdna.bootstrapcdn.com/font-awesome/4.7.0/css/font-awesome.css" type="text/css">
|
||||
<link rel="stylesheet" href="//netdna.bootstrapcdn.com/bootstrap/3.3.2/css/bootstrap.min.css" type="text/css">
|
||||
<link rel="stylesheet" href="//fonts.googleapis.com/css?family=Source+Sans+Pro:300,400,700" type="text/css">
|
||||
<link rel="stylesheet" href="//s3.amazonaws.com/cdn.core-os.net/icons/core-icons.css" type="text/css">
|
||||
<link rel="stylesheet" href="//cdnjs.cloudflare.com/ajax/libs/bootstrap-datetimepicker/4.17.37/css/bootstrap-datetimepicker.min.css" type="text/css">
|
||||
<link rel="stylesheet" href="//cdn.jsdelivr.net/cal-heatmap/3.3.10/cal-heatmap.css" type="text/css">
|
||||
<link rel="stylesheet" href="//cdnjs.cloudflare.com/ajax/libs/ng-tags-input/3.1.1/ng-tags-input.min.css" type="text/css">
|
||||
|
||||
<!--REMOTE SCRIPTS-->
|
||||
<script src="//code.jquery.com/jquery.js"></script>
|
||||
<script src="//netdna.bootstrapcdn.com/bootstrap/3.3.2/js/bootstrap.min.js"></script>
|
||||
<script src="//ajax.googleapis.com/ajax/libs/angularjs/1.5.3/angular.min.js"></script>
|
||||
<!--<script src="//ajax.googleapis.com/ajax/libs/angularjs/1.5.3/angular-route.min.js"></script>-->
|
||||
<script src="//ajax.googleapis.com/ajax/libs/angularjs/1.5.3/angular-sanitize.min.js"></script>
|
||||
<!--<script src="//ajax.googleapis.com/ajax/libs/angularjs/1.5.3/angular-animate.min.js"></script>-->
|
||||
<script src="//ajax.googleapis.com/ajax/libs/angularjs/1.5.3/angular-cookies.min.js"></script>
|
||||
<script src="//cdn.jsdelivr.net/g/bootbox@4.1.0,underscorejs@1.5.2,restangular@1.2.0,d3js@3.3.3"></script>
|
||||
<!--<script src="//cdn.jsdelivr.net/g/momentjs"></script>-->
|
||||
<!--<script src="//cdnjs.cloudflare.com/ajax/libs/bootstrap-datepicker/1.2.0/js/bootstrap-datepicker.min.js"></script>-->
|
||||
<!--<script src="//cdnjs.cloudflare.com/ajax/libs/bootstrap-datetimepicker/4.17.37/js/bootstrap-datetimepicker.min.js"></script>-->
|
||||
<!--<script src="//cdn.ravenjs.com/3.1.0/angular/raven.min.js"></script>-->
|
||||
<!--<script src="//cdn.jsdelivr.net/cal-heatmap/3.3.10/cal-heatmap.min.js"></script>-->
|
||||
<!--<script src="//cdnjs.cloudflare.com/ajax/libs/angular-recaptcha/4.1.3/angular-recaptcha.min.js"></script>-->
|
||||
<!--<script src="//cdnjs.cloudflare.com/ajax/libs/ng-tags-input/3.1.1/ng-tags-input.min.js"></script>-->
|
||||
<!--<script src="//cdnjs.cloudflare.com/ajax/libs/corejs-typeahead/1.1.1/typeahead.bundle.min.js"></script>-->
|
||||
|
||||
{% for script_path in main_scripts %}
|
||||
<script src="/static/{{ script_path }}"></script>
|
||||
{% endfor %}
|
||||
<title>Config app</title>
|
||||
</head>
|
||||
<body>
|
||||
<div>
|
||||
<p>What is my purpose</p>
|
||||
<p>You make tarballs</p>
|
||||
<div class="setup">
|
||||
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
0
config_app/util/__init__.py
Normal file
0
config_app/util/__init__.py
Normal file
128
config_app/util/baseprovider.py
Normal file
128
config_app/util/baseprovider.py
Normal file
|
@ -0,0 +1,128 @@
|
|||
import logging
|
||||
import yaml
|
||||
|
||||
from abc import ABCMeta, abstractmethod
|
||||
from six import add_metaclass
|
||||
|
||||
from jsonschema import validate, ValidationError
|
||||
|
||||
from util.config.schema import CONFIG_SCHEMA
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class CannotWriteConfigException(Exception):
|
||||
""" Exception raised when the config cannot be written. """
|
||||
pass
|
||||
|
||||
|
||||
class SetupIncompleteException(Exception):
|
||||
""" Exception raised when attempting to verify config that has not yet been setup. """
|
||||
pass
|
||||
|
||||
|
||||
def import_yaml(config_obj, config_file):
|
||||
with open(config_file) as f:
|
||||
c = yaml.safe_load(f)
|
||||
if not c:
|
||||
logger.debug('Empty YAML config file')
|
||||
return
|
||||
|
||||
if isinstance(c, str):
|
||||
raise Exception('Invalid YAML config file: ' + str(c))
|
||||
|
||||
for key in c.iterkeys():
|
||||
if key.isupper():
|
||||
config_obj[key] = c[key]
|
||||
|
||||
if config_obj.get('SETUP_COMPLETE', True):
|
||||
try:
|
||||
validate(config_obj, CONFIG_SCHEMA)
|
||||
except ValidationError:
|
||||
# TODO: Change this into a real error
|
||||
logger.exception('Could not validate config schema')
|
||||
else:
|
||||
logger.debug('Skipping config schema validation because setup is not complete')
|
||||
|
||||
return config_obj
|
||||
|
||||
|
||||
def get_yaml(config_obj):
|
||||
return yaml.safe_dump(config_obj, encoding='utf-8', allow_unicode=True)
|
||||
|
||||
|
||||
def export_yaml(config_obj, config_file):
|
||||
try:
|
||||
with open(config_file, 'w') as f:
|
||||
f.write(get_yaml(config_obj))
|
||||
except IOError as ioe:
|
||||
raise CannotWriteConfigException(str(ioe))
|
||||
|
||||
|
||||
@add_metaclass(ABCMeta)
|
||||
class BaseProvider(object):
|
||||
""" A configuration provider helps to load, save, and handle config override in the application.
|
||||
"""
|
||||
|
||||
@property
|
||||
def provider_id(self):
|
||||
raise NotImplementedError
|
||||
|
||||
@abstractmethod
|
||||
def update_app_config(self, app_config):
|
||||
""" Updates the given application config object with the loaded override config. """
|
||||
|
||||
@abstractmethod
|
||||
def get_config(self):
|
||||
""" Returns the contents of the config override file, or None if none. """
|
||||
|
||||
@abstractmethod
|
||||
def save_config(self, config_object):
|
||||
""" Updates the contents of the config override file to those given. """
|
||||
|
||||
@abstractmethod
|
||||
def config_exists(self):
|
||||
""" Returns true if a config override file exists in the config volume. """
|
||||
|
||||
@abstractmethod
|
||||
def volume_exists(self):
|
||||
""" Returns whether the config override volume exists. """
|
||||
|
||||
@abstractmethod
|
||||
def volume_file_exists(self, filename):
|
||||
""" Returns whether the file with the given name exists under the config override volume. """
|
||||
|
||||
@abstractmethod
|
||||
def get_volume_file(self, filename, mode='r'):
|
||||
""" Returns a Python file referring to the given name under the config override volume. """
|
||||
|
||||
@abstractmethod
|
||||
def write_volume_file(self, filename, contents):
|
||||
""" Writes the given contents to the config override volumne, with the given filename. """
|
||||
|
||||
@abstractmethod
|
||||
def remove_volume_file(self, filename):
|
||||
""" Removes the config override volume file with the given filename. """
|
||||
|
||||
@abstractmethod
|
||||
def list_volume_directory(self, path):
|
||||
""" Returns a list of strings representing the names of the files found in the config override
|
||||
directory under the given path. If the path doesn't exist, returns None.
|
||||
"""
|
||||
|
||||
@abstractmethod
|
||||
def save_volume_file(self, filename, flask_file):
|
||||
""" Saves the given flask file to the config override volume, with the given
|
||||
filename.
|
||||
"""
|
||||
|
||||
@abstractmethod
|
||||
def requires_restart(self, app_config):
|
||||
""" If true, the configuration loaded into memory for the app does not match that on disk,
|
||||
indicating that this container requires a restart.
|
||||
"""
|
||||
|
||||
@abstractmethod
|
||||
def get_volume_path(self, directory, filename):
|
||||
""" Helper for constructing file paths, which may differ between providers. For example,
|
||||
kubernetes can't have subfolders in configmaps """
|
21
config_app/util/config.py
Normal file
21
config_app/util/config.py
Normal file
|
@ -0,0 +1,21 @@
|
|||
import os
|
||||
from util.config.provider import TestConfigProvider, KubernetesConfigProvider, FileConfigProvider
|
||||
|
||||
ROOT_DIR = os.path.dirname(os.path.abspath(__file__))
|
||||
CONF_DIR = os.getenv("QUAYCONF", os.path.join(ROOT_DIR, "conf/"))
|
||||
OVERRIDE_CONFIG_DIRECTORY = os.path.join(CONF_DIR, 'stack/')
|
||||
|
||||
|
||||
def get_config_provider(config_volume, yaml_filename, py_filename, testing=False, kubernetes=False):
|
||||
""" Loads and returns the config provider for the current environment. """
|
||||
if testing:
|
||||
return TestConfigProvider()
|
||||
|
||||
if kubernetes:
|
||||
return KubernetesConfigProvider(config_volume, yaml_filename, py_filename)
|
||||
|
||||
return FileConfigProvider(config_volume, yaml_filename, py_filename)
|
||||
|
||||
|
||||
config_provider = get_config_provider(OVERRIDE_CONFIG_DIRECTORY, 'config.yaml', 'config.py',
|
||||
testing=False, kubernetes=False)
|
60
config_app/util/fileprovider.py
Normal file
60
config_app/util/fileprovider.py
Normal file
|
@ -0,0 +1,60 @@
|
|||
import os
|
||||
import logging
|
||||
|
||||
from util.config.provider.baseprovider import export_yaml, CannotWriteConfigException
|
||||
from util.config.provider.basefileprovider import BaseFileProvider
|
||||
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
def _ensure_parent_dir(filepath):
|
||||
""" Ensures that the parent directory of the given file path exists. """
|
||||
try:
|
||||
parentpath = os.path.abspath(os.path.join(filepath, os.pardir))
|
||||
if not os.path.isdir(parentpath):
|
||||
os.makedirs(parentpath)
|
||||
except IOError as ioe:
|
||||
raise CannotWriteConfigException(str(ioe))
|
||||
|
||||
|
||||
class FileConfigProvider(BaseFileProvider):
|
||||
""" Implementation of the config provider that reads and writes the data
|
||||
from/to the file system. """
|
||||
def __init__(self, config_volume, yaml_filename, py_filename):
|
||||
super(FileConfigProvider, self).__init__(config_volume, yaml_filename, py_filename)
|
||||
|
||||
@property
|
||||
def provider_id(self):
|
||||
return 'file'
|
||||
|
||||
def save_config(self, config_obj):
|
||||
export_yaml(config_obj, self.yaml_path)
|
||||
|
||||
def write_volume_file(self, filename, contents):
|
||||
filepath = os.path.join(self.config_volume, filename)
|
||||
_ensure_parent_dir(filepath)
|
||||
|
||||
try:
|
||||
with open(filepath, mode='w') as f:
|
||||
f.write(contents)
|
||||
except IOError as ioe:
|
||||
raise CannotWriteConfigException(str(ioe))
|
||||
|
||||
return filepath
|
||||
|
||||
def remove_volume_file(self, filename):
|
||||
filepath = os.path.join(self.config_volume, filename)
|
||||
os.remove(filepath)
|
||||
|
||||
def save_volume_file(self, filename, flask_file):
|
||||
filepath = os.path.join(self.config_volume, filename)
|
||||
_ensure_parent_dir(filepath)
|
||||
|
||||
# Write the file.
|
||||
try:
|
||||
flask_file.save(filepath)
|
||||
except IOError as ioe:
|
||||
raise CannotWriteConfigException(str(ioe))
|
||||
|
||||
return filepath
|
|
@ -1,6 +1,8 @@
|
|||
from app import app as application
|
||||
from config_endpoints.setup_web import setup_web
|
||||
from config_endpoints.api import api_bp
|
||||
|
||||
|
||||
application.register_blueprint(setup_web)
|
||||
# application.register_blueprint(setup_web)
|
||||
application.register_blueprint(api_bp, url_prefix='/api')
|
||||
|
||||
|
|
60
config_app/webpack.config.js
Normal file
60
config_app/webpack.config.js
Normal file
|
@ -0,0 +1,60 @@
|
|||
const webpack = require('webpack');
|
||||
const path = require('path');
|
||||
|
||||
let config = {
|
||||
entry: {
|
||||
configapp: "./js/main.ts"
|
||||
},
|
||||
output: {
|
||||
path: path.resolve(__dirname, "static/build"),
|
||||
filename: '[name]-quay-frontend.bundle.js',
|
||||
},
|
||||
resolve: {
|
||||
extensions: [".ts", ".js"],
|
||||
modules: [
|
||||
// Allows us to use the top-level node modules
|
||||
path.resolve(__dirname, '../node_modules'),
|
||||
]
|
||||
},
|
||||
externals: {
|
||||
angular: "angular",
|
||||
jquery: "$",
|
||||
// moment: "moment",
|
||||
// "raven-js": "Raven",
|
||||
},
|
||||
module: {
|
||||
rules: [
|
||||
{
|
||||
test: /\.ts$/,
|
||||
use: ["ts-loader"],
|
||||
exclude: /node_modules/
|
||||
},
|
||||
{
|
||||
test: /\.css$/,
|
||||
use: [
|
||||
"style-loader",
|
||||
"css-loader?minimize=true",
|
||||
],
|
||||
},
|
||||
{
|
||||
test: /\.html$/,
|
||||
use: [
|
||||
'ngtemplate-loader?relativeTo=' + (path.resolve(__dirname)),
|
||||
'html-loader',
|
||||
]
|
||||
},
|
||||
]
|
||||
},
|
||||
plugins: [
|
||||
// Replace references to global variables with associated modules
|
||||
new webpack.ProvidePlugin({
|
||||
FileSaver: 'file-saver',
|
||||
angular: "angular",
|
||||
$: "jquery",
|
||||
// moment: "moment",
|
||||
}),
|
||||
],
|
||||
devtool: "cheap-module-source-map",
|
||||
};
|
||||
|
||||
module.exports = config;
|
|
@ -24,7 +24,7 @@ from _init import __version__
|
|||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
JS_BUNDLE_NAME = 'main'
|
||||
JS_BUNDLE_NAME = 'bundle'
|
||||
|
||||
|
||||
def common_login(user_uuid, permanent_session=True):
|
||||
|
@ -73,9 +73,9 @@ def _list_files(path, extension, contains=""):
|
|||
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=JS_BUNDLE_NAME, **kwargs):
|
||||
def render_page_template(name, route_data=None, **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)
|
||||
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:
|
||||
|
|
|
@ -1,11 +1,11 @@
|
|||
#!/usr/bin/env bash
|
||||
cat << "EOF"
|
||||
__ __
|
||||
/ \ / \ ______ _ _ __ __ __
|
||||
/ /\ / /\ \ / __ \ | | | | / \ \ \ / /
|
||||
/ / / / \ \ | | | | | | | | / /\ \ \ /
|
||||
\ \ \ \ / / | |__| | | |__| | / ____ \ | |
|
||||
\ \/ \ \/ / \_ ___/ \____/ /_/ \_\ |_|
|
||||
/ \ / \ ______ _ _ __ __ __ _____ ____ _ _ _____ _____ _____
|
||||
/ /\ / /\ \ / __ \ | | | | / \ \ \ / / / ____| / __ \ | \ | | | ___| |_ _| / ____|
|
||||
/ / / / \ \ | | | | | | | | / /\ \ \ / | | | | | | | \| | | |__ | | | | _
|
||||
\ \ \ \ / / | |__| | | |__| | / ____ \ | | | |____ | |__| | | . ` | | __| _| |_ | |__| |
|
||||
\ \/ \ \/ / \_ ___/ \____/ /_/ \_\ |_| \_____| \____/ |_| \_| |_| |_____| \_____|
|
||||
\__/ \__/ \ \__
|
||||
\___\ by CoreOS
|
||||
|
||||
|
|
|
@ -12,7 +12,10 @@
|
|||
"watch": "npm run clean && webpack --watch",
|
||||
"lint": "tslint --type-check -p tsconfig.json -e **/*.spec.ts",
|
||||
"analyze": "NODE_ENV=production webpack --profile --json | awk '{if(NR>1)print}' > static/build/stats.json && webpack-bundle-analyzer --mode static -r static/build/report.html static/build/stats.json",
|
||||
"clean": "rm -f static/build/*"
|
||||
"clean": "rm -f static/build/*",
|
||||
|
||||
"clean-config-app": "rm -f config_app/static/build/*",
|
||||
"watch-config-app": "npm run clean-config-app && cd config_app && webpack --watch"
|
||||
},
|
||||
"repository": {
|
||||
"type": "git",
|
||||
|
|
|
@ -1,5 +0,0 @@
|
|||
|
||||
|
||||
const setupPage = '';
|
||||
angular.module('quay', [setupPage]);
|
||||
console.log('Hello world! I\'m the config app');
|
8
web.py
8
web.py
|
@ -11,15 +11,7 @@ from endpoints.webhooks import webhooks
|
|||
from endpoints.wellknown import wellknown
|
||||
|
||||
|
||||
import os
|
||||
is_config_mode = 'FLAGGED_CONFIG_MODE' in os.environ
|
||||
print('\n\n\nAre we in config mode?')
|
||||
print(is_config_mode)
|
||||
|
||||
|
||||
application.register_blueprint(web)
|
||||
|
||||
|
||||
application.register_blueprint(githubtrigger, url_prefix='/oauth2')
|
||||
application.register_blueprint(gitlabtrigger, url_prefix='/oauth2')
|
||||
application.register_blueprint(oauthlogin, url_prefix='/oauth2')
|
||||
|
|
|
@ -3,10 +3,7 @@ const path = require('path');
|
|||
|
||||
|
||||
let config = {
|
||||
entry: {
|
||||
main: "./static/js/main.ts",
|
||||
configapp: "./static/configappjs/index.js"
|
||||
},
|
||||
entry: "./static/js/main.ts",
|
||||
output: {
|
||||
path: path.resolve(__dirname, "static/build"),
|
||||
publicPath: "/static/build/",
|
||||
|
|
Reference in a new issue