Change ApiService to use the new swagger-backed discovery and the new /v1/ API endpoints. Also changes all other /api/ calls (the few that are still manually invoked)
This commit is contained in:
parent
767ab1085a
commit
e759066ae0
4 changed files with 87 additions and 79 deletions
|
@ -29,7 +29,7 @@ def fully_qualified_name(method_view_class):
|
||||||
return '%s.%s' % (inst.__module__, inst.__class__.__name__)
|
return '%s.%s' % (inst.__module__, inst.__class__.__name__)
|
||||||
|
|
||||||
|
|
||||||
def swagger_route_data(include_internal):
|
def swagger_route_data(include_internal=False, compact=False):
|
||||||
apis = []
|
apis = []
|
||||||
models = {}
|
models = {}
|
||||||
for rule in app.url_map.iter_rules():
|
for rule in app.url_map.iter_rules():
|
||||||
|
@ -86,14 +86,19 @@ def swagger_route_data(include_internal):
|
||||||
|
|
||||||
new_operation = {
|
new_operation = {
|
||||||
'method': method_name,
|
'method': method_name,
|
||||||
'nickname': method_metadata(method, 'nickname'),
|
'nickname': method_metadata(method, 'nickname')
|
||||||
'type': 'void',
|
|
||||||
'summary': method.__doc__ if method.__doc__ else '',
|
|
||||||
'parameters': parameters,
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if not compact:
|
||||||
|
new_operation.update({
|
||||||
|
'type': 'void',
|
||||||
|
'summary': method.__doc__ if method.__doc__ else '',
|
||||||
|
'parameters': parameters,
|
||||||
|
})
|
||||||
|
|
||||||
|
|
||||||
scope = method_metadata(method, 'oauth2_scope')
|
scope = method_metadata(method, 'oauth2_scope')
|
||||||
if scope:
|
if scope and not compact:
|
||||||
new_operation['authorizations'] = {
|
new_operation['authorizations'] = {
|
||||||
'oauth2': [scope],
|
'oauth2': [scope],
|
||||||
}
|
}
|
||||||
|
@ -123,7 +128,11 @@ def swagger_route_data(include_internal):
|
||||||
|
|
||||||
if not internal or (internal and include_internal):
|
if not internal or (internal and include_internal):
|
||||||
apis.append(new_resource)
|
apis.append(new_resource)
|
||||||
|
|
||||||
|
# If compact form was requested, simply return the APIs.
|
||||||
|
if compact:
|
||||||
|
return {'apis': apis}
|
||||||
|
|
||||||
swagger_data = {
|
swagger_data = {
|
||||||
'apiVersion': 'v1',
|
'apiVersion': 'v1',
|
||||||
'swaggerVersion': '1.2',
|
'swaggerVersion': '1.2',
|
||||||
|
@ -153,6 +162,7 @@ def swagger_route_data(include_internal):
|
||||||
'apis': apis,
|
'apis': apis,
|
||||||
'models': models,
|
'models': models,
|
||||||
}
|
}
|
||||||
|
|
||||||
return swagger_data
|
return swagger_data
|
||||||
|
|
||||||
@resource('/v1/discovery')
|
@resource('/v1/discovery')
|
||||||
|
|
|
@ -12,6 +12,7 @@ from data import model
|
||||||
from data.queue import dockerfile_build_queue
|
from data.queue import dockerfile_build_queue
|
||||||
from app import app, login_manager
|
from app import app, login_manager
|
||||||
from auth.permissions import QuayDeferredPermissionUser
|
from auth.permissions import QuayDeferredPermissionUser
|
||||||
|
from api.discovery import swagger_route_data
|
||||||
|
|
||||||
|
|
||||||
logger = logging.getLogger(__name__)
|
logger = logging.getLogger(__name__)
|
||||||
|
@ -24,29 +25,7 @@ def get_route_data():
|
||||||
if route_data:
|
if route_data:
|
||||||
return route_data
|
return route_data
|
||||||
|
|
||||||
routes = []
|
route_data = swagger_route_data(include_internal=True, compact=True)
|
||||||
for rule in app.url_map.iter_rules():
|
|
||||||
if rule.endpoint.startswith('api.'):
|
|
||||||
endpoint_method = app.view_functions[rule.endpoint]
|
|
||||||
is_internal = '__internal_call' in dir(endpoint_method)
|
|
||||||
is_org_api = '__user_call' in dir(endpoint_method)
|
|
||||||
methods = list(rule.methods.difference(['HEAD', 'OPTIONS']))
|
|
||||||
|
|
||||||
route = {
|
|
||||||
'name': rule.endpoint[4:],
|
|
||||||
'methods': methods,
|
|
||||||
'path': rule.rule,
|
|
||||||
'parameters': list(rule.arguments)
|
|
||||||
}
|
|
||||||
|
|
||||||
if is_org_api:
|
|
||||||
route['user_method'] = endpoint_method.__user_call
|
|
||||||
|
|
||||||
routes.append(route)
|
|
||||||
|
|
||||||
route_data = {
|
|
||||||
'endpoints': routes
|
|
||||||
}
|
|
||||||
return route_data
|
return route_data
|
||||||
|
|
||||||
|
|
||||||
|
@ -114,7 +93,7 @@ app.jinja_env.globals['csrf_token'] = generate_csrf_token
|
||||||
|
|
||||||
def render_page_template(name, **kwargs):
|
def render_page_template(name, **kwargs):
|
||||||
|
|
||||||
resp = make_response(render_template(name, route_data=get_route_data(),
|
resp = make_response(render_template(name, route_data=json.dumps(get_route_data()),
|
||||||
**kwargs))
|
**kwargs))
|
||||||
resp.headers['X-FRAME-OPTIONS'] = 'DENY'
|
resp.headers['X-FRAME-OPTIONS'] = 'DENY'
|
||||||
return resp
|
return resp
|
||||||
|
@ -160,4 +139,4 @@ def start_build(repository, dockerfile_id, tags, build_name, subdir, manual,
|
||||||
ip=request.remote_addr, metadata=metadata,
|
ip=request.remote_addr, metadata=metadata,
|
||||||
repository=repository)
|
repository=repository)
|
||||||
|
|
||||||
return build_request
|
return build_request
|
||||||
|
|
111
static/js/app.js
111
static/js/app.js
|
@ -214,43 +214,22 @@ quayApp = angular.module('quay', ['ngRoute', 'chieffancypants.loadingBar', 'angu
|
||||||
return resource;
|
return resource;
|
||||||
};
|
};
|
||||||
|
|
||||||
var formatMethodName = function(endpointName) {
|
|
||||||
var formatted = '';
|
|
||||||
for (var i = 0; i < endpointName.length; ++i) {
|
|
||||||
var c = endpointName[i];
|
|
||||||
if (c == '_') {
|
|
||||||
c = endpointName[i + 1].toUpperCase();
|
|
||||||
i++;
|
|
||||||
}
|
|
||||||
|
|
||||||
formatted += c;
|
|
||||||
}
|
|
||||||
|
|
||||||
return formatted;
|
|
||||||
};
|
|
||||||
|
|
||||||
var buildUrl = function(path, parameters) {
|
var buildUrl = function(path, parameters) {
|
||||||
// We already have /api/ on the URLs, so remove them from the paths.
|
// We already have /api/v1/ on the URLs, so remove them from the paths.
|
||||||
path = path.substr('/api/'.length, path.length);
|
path = path.substr('/api/v1/'.length, path.length);
|
||||||
|
|
||||||
var url = '';
|
var url = '';
|
||||||
for (var i = 0; i < path.length; ++i) {
|
for (var i = 0; i < path.length; ++i) {
|
||||||
var c = path[i];
|
var c = path[i];
|
||||||
if (c == '<') {
|
if (c == '{') {
|
||||||
var end = path.indexOf('>', i);
|
var end = path.indexOf('}', i);
|
||||||
var varName = path.substr(i + 1, end - i - 1);
|
var varName = path.substr(i + 1, end - i - 1);
|
||||||
var colon = varName.indexOf(':');
|
|
||||||
var isPathVar = false;
|
|
||||||
if (colon > 0) {
|
|
||||||
isPathVar = true;
|
|
||||||
varName = varName.substr(colon + 1);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!parameters[varName]) {
|
if (!parameters[varName]) {
|
||||||
throw new Error('Missing parameter: ' + varName);
|
throw new Error('Missing parameter: ' + varName);
|
||||||
}
|
}
|
||||||
|
|
||||||
url += isPathVar ? parameters[varName] : encodeURI(parameters[varName]);
|
url += parameters[varName];
|
||||||
i = end;
|
i = end;
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
@ -261,15 +240,41 @@ quayApp = angular.module('quay', ['ngRoute', 'chieffancypants.loadingBar', 'angu
|
||||||
return url;
|
return url;
|
||||||
};
|
};
|
||||||
|
|
||||||
var getGenericMethodName = function(userMethodName) {
|
var getGenericOperationName = function(userOperationName) {
|
||||||
return formatMethodName(userMethodName.replace('_user', ''));
|
return userOperationName.replace('User', '');
|
||||||
};
|
};
|
||||||
|
|
||||||
var buildMethodsForEndpoint = function(endpoint) {
|
var getMatchingUserOperationName = function(orgOperationName, method, userRelatedResource) {
|
||||||
var method = endpoint.methods[0].toLowerCase();
|
if (userRelatedResource) {
|
||||||
var methodName = formatMethodName(endpoint['name']);
|
var operations = userRelatedResource['operations'];
|
||||||
apiService[methodName] = function(opt_options, opt_parameters, opt_background) {
|
for (var i = 0; i < operations.length; ++i) {
|
||||||
var one = Restangular.one(buildUrl(endpoint['path'], opt_parameters));
|
var operation = operations[i];
|
||||||
|
if (operation['method'].toLowerCase() == method) {
|
||||||
|
return operation['nickname'];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
throw new Error('Could not find user operation matching org operation: ' + orgOperationName);
|
||||||
|
};
|
||||||
|
|
||||||
|
var buildMethodsForEndpointResource = function(endpointResource, resourceMap) {
|
||||||
|
var name = endpointResource['name'];
|
||||||
|
var operations = endpointResource['operations'];
|
||||||
|
for (var i = 0; i < operations.length; ++i) {
|
||||||
|
var operation = operations[i];
|
||||||
|
buildMethodsForOperation(operation, endpointResource, resourceMap);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
var buildMethodsForOperation = function(operation, resource, resourceMap) {
|
||||||
|
var method = operation['method'].toLowerCase();
|
||||||
|
var operationName = operation['nickname'];
|
||||||
|
var path = resource['path'];
|
||||||
|
|
||||||
|
// Add the operation itself.
|
||||||
|
apiService[operationName] = function(opt_options, opt_parameters, opt_background) {
|
||||||
|
var one = Restangular.one(buildUrl(path, opt_parameters));
|
||||||
if (opt_background) {
|
if (opt_background) {
|
||||||
one.withHttpConfig({
|
one.withHttpConfig({
|
||||||
'ignoreLoadingBar': true
|
'ignoreLoadingBar': true
|
||||||
|
@ -278,36 +283,50 @@ quayApp = angular.module('quay', ['ngRoute', 'chieffancypants.loadingBar', 'angu
|
||||||
return one['custom' + method.toUpperCase()](opt_options);
|
return one['custom' + method.toUpperCase()](opt_options);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// If the method for the operation is a GET, add an operationAsResource method.
|
||||||
if (method == 'get') {
|
if (method == 'get') {
|
||||||
apiService[methodName + 'AsResource'] = function(opt_parameters, opt_background) {
|
apiService[operationName + 'AsResource'] = function(opt_parameters, opt_background) {
|
||||||
return getResource(buildUrl(endpoint['path'], opt_parameters), opt_background);
|
return getResource(buildUrl(path, opt_parameters), opt_background);
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
if (endpoint['user_method']) {
|
// If the resource has a user-related resource, then make a generic operation for this operation
|
||||||
apiService[getGenericMethodName(endpoint['user_method'])] = function(orgname, opt_options, opt_parameters, opt_background) {
|
// that can call both the user and the organization versions of the operation, depending on the
|
||||||
|
// parameters given.
|
||||||
|
if (resource['quayUserRelated']) {
|
||||||
|
var userOperationName = getMatchingUserOperationName(operationName, method, resourceMap[resource['quayUserRelated']]);
|
||||||
|
var genericOperationName = getGenericOperationName(userOperationName);
|
||||||
|
apiService[genericOperationName] = function(orgname, opt_options, opt_parameters, opt_background) {
|
||||||
if (orgname) {
|
if (orgname) {
|
||||||
if (orgname.name) {
|
if (orgname.name) {
|
||||||
orgname = orgname.name;
|
orgname = orgname.name;
|
||||||
}
|
}
|
||||||
|
|
||||||
var params = jQuery.extend({'orgname' : orgname}, opt_parameters || {}, opt_background);
|
var params = jQuery.extend({'orgname' : orgname}, opt_parameters || {}, opt_background);
|
||||||
return apiService[methodName](opt_options, params);
|
return apiService[operationName](opt_options, params);
|
||||||
} else {
|
} else {
|
||||||
return apiService[formatMethodName(endpoint['user_method'])](opt_options, opt_parameters, opt_background);
|
return apiService[userOperationName](opt_options, opt_parameters, opt_background);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
// Construct the methods for each API endpoint.
|
|
||||||
if (!window.__endpoints) {
|
if (!window.__endpoints) {
|
||||||
return apiService;
|
return apiService;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
var resourceMap = {};
|
||||||
|
|
||||||
|
// Build the map of resource names to their objects.
|
||||||
for (var i = 0; i < window.__endpoints.length; ++i) {
|
for (var i = 0; i < window.__endpoints.length; ++i) {
|
||||||
var endpoint = window.__endpoints[i];
|
var endpointResource = window.__endpoints[i];
|
||||||
buildMethodsForEndpoint(endpoint);
|
resourceMap[endpointResource['name']] = endpointResource;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Construct the methods for each API endpoint.
|
||||||
|
for (var i = 0; i < window.__endpoints.length; ++i) {
|
||||||
|
var endpointResource = window.__endpoints[i];
|
||||||
|
buildMethodsForEndpointResource(endpointResource, resourceMap);
|
||||||
}
|
}
|
||||||
|
|
||||||
return apiService;
|
return apiService;
|
||||||
|
@ -846,7 +865,7 @@ quayApp = angular.module('quay', ['ngRoute', 'chieffancypants.loadingBar', 'angu
|
||||||
otherwise({redirectTo: '/'});
|
otherwise({redirectTo: '/'});
|
||||||
}]).
|
}]).
|
||||||
config(function(RestangularProvider) {
|
config(function(RestangularProvider) {
|
||||||
RestangularProvider.setBaseUrl('/api/');
|
RestangularProvider.setBaseUrl('/api/v1/');
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
||||||
|
@ -1519,7 +1538,7 @@ quayApp.directive('logsView', function () {
|
||||||
|
|
||||||
var loadLogs = Restangular.one(url);
|
var loadLogs = Restangular.one(url);
|
||||||
loadLogs.customGET().then(function(resp) {
|
loadLogs.customGET().then(function(resp) {
|
||||||
$scope.logsPath = '/api/' + url;
|
$scope.logsPath = '/api/v1/' + url;
|
||||||
|
|
||||||
if (!$scope.chart) {
|
if (!$scope.chart) {
|
||||||
$scope.chart = new LogUsageChart(logKinds);
|
$scope.chart = new LogUsageChart(logKinds);
|
||||||
|
@ -2011,7 +2030,7 @@ quayApp.directive('repoSearch', function () {
|
||||||
var repoHound = new Bloodhound({
|
var repoHound = new Bloodhound({
|
||||||
name: 'repositories',
|
name: 'repositories',
|
||||||
remote: {
|
remote: {
|
||||||
url: '/api/find/repository?query=%QUERY',
|
url: '/api/v1/find/repository?query=%QUERY',
|
||||||
replace: function (url, uriEncodedQuery) {
|
replace: function (url, uriEncodedQuery) {
|
||||||
url = url.replace('%QUERY', uriEncodedQuery);
|
url = url.replace('%QUERY', uriEncodedQuery);
|
||||||
url += '&cb=' + searchToken;
|
url += '&cb=' + searchToken;
|
||||||
|
@ -2242,7 +2261,7 @@ quayApp.directive('entitySearch', function () {
|
||||||
var entitySearchB = new Bloodhound({
|
var entitySearchB = new Bloodhound({
|
||||||
name: 'entities' + number,
|
name: 'entities' + number,
|
||||||
remote: {
|
remote: {
|
||||||
url: '/api/entities/%QUERY',
|
url: '/api/v1/entities/%QUERY',
|
||||||
replace: function (url, uriEncodedQuery) {
|
replace: function (url, uriEncodedQuery) {
|
||||||
var namespace = $scope.namespace || '';
|
var namespace = $scope.namespace || '';
|
||||||
url = url.replace('%QUERY', uriEncodedQuery);
|
url = url.replace('%QUERY', uriEncodedQuery);
|
||||||
|
|
|
@ -69,7 +69,7 @@
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
|
|
||||||
<script type="text/javascript">
|
<script type="text/javascript">
|
||||||
window.__endpoints = {{ route_data|safe }}.endpoints;
|
window.__endpoints = {{ route_data|safe }}.apis;
|
||||||
window.__token = '{{ csrf_token() }}';
|
window.__token = '{{ csrf_token() }}';
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
|
|
Reference in a new issue