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:
Joseph Schorr 2014-03-14 23:40:41 -04:00
parent 767ab1085a
commit e759066ae0
4 changed files with 87 additions and 79 deletions

View file

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

View file

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

View file

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

View file

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