From e759066ae06ebee4a5af7b583ca7cd71f8dcda70 Mon Sep 17 00:00:00 2001 From: Joseph Schorr Date: Fri, 14 Mar 2014 23:40:41 -0400 Subject: [PATCH] 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) --- endpoints/api/discovery.py | 24 +++++--- endpoints/common.py | 29 ++-------- static/js/app.js | 111 ++++++++++++++++++++++--------------- templates/base.html | 2 +- 4 files changed, 87 insertions(+), 79 deletions(-) diff --git a/endpoints/api/discovery.py b/endpoints/api/discovery.py index 8e24b0c80..dc5ee9178 100644 --- a/endpoints/api/discovery.py +++ b/endpoints/api/discovery.py @@ -29,7 +29,7 @@ def fully_qualified_name(method_view_class): return '%s.%s' % (inst.__module__, inst.__class__.__name__) -def swagger_route_data(include_internal): +def swagger_route_data(include_internal=False, compact=False): apis = [] models = {} for rule in app.url_map.iter_rules(): @@ -86,14 +86,19 @@ def swagger_route_data(include_internal): new_operation = { 'method': method_name, - 'nickname': method_metadata(method, 'nickname'), - 'type': 'void', - 'summary': method.__doc__ if method.__doc__ else '', - 'parameters': parameters, + 'nickname': method_metadata(method, 'nickname') } + if not compact: + new_operation.update({ + 'type': 'void', + 'summary': method.__doc__ if method.__doc__ else '', + 'parameters': parameters, + }) + + scope = method_metadata(method, 'oauth2_scope') - if scope: + if scope and not compact: new_operation['authorizations'] = { 'oauth2': [scope], } @@ -123,7 +128,11 @@ def swagger_route_data(include_internal): if not internal or (internal and include_internal): apis.append(new_resource) - + + # If compact form was requested, simply return the APIs. + if compact: + return {'apis': apis} + swagger_data = { 'apiVersion': 'v1', 'swaggerVersion': '1.2', @@ -153,6 +162,7 @@ def swagger_route_data(include_internal): 'apis': apis, 'models': models, } + return swagger_data @resource('/v1/discovery') diff --git a/endpoints/common.py b/endpoints/common.py index c479a3566..a4747b21f 100644 --- a/endpoints/common.py +++ b/endpoints/common.py @@ -12,6 +12,7 @@ from data import model from data.queue import dockerfile_build_queue from app import app, login_manager from auth.permissions import QuayDeferredPermissionUser +from api.discovery import swagger_route_data logger = logging.getLogger(__name__) @@ -24,29 +25,7 @@ def get_route_data(): if route_data: return route_data - routes = [] - 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 - } + route_data = swagger_route_data(include_internal=True, compact=True) return route_data @@ -114,7 +93,7 @@ app.jinja_env.globals['csrf_token'] = generate_csrf_token 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)) resp.headers['X-FRAME-OPTIONS'] = 'DENY' return resp @@ -160,4 +139,4 @@ def start_build(repository, dockerfile_id, tags, build_name, subdir, manual, ip=request.remote_addr, metadata=metadata, repository=repository) - return build_request \ No newline at end of file + return build_request diff --git a/static/js/app.js b/static/js/app.js index 7065af6f1..ce9ce5e4b 100644 --- a/static/js/app.js +++ b/static/js/app.js @@ -214,43 +214,22 @@ quayApp = angular.module('quay', ['ngRoute', 'chieffancypants.loadingBar', 'angu 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) { - // We already have /api/ on the URLs, so remove them from the paths. - path = path.substr('/api/'.length, path.length); + // We already have /api/v1/ on the URLs, so remove them from the paths. + path = path.substr('/api/v1/'.length, path.length); var url = ''; for (var i = 0; i < path.length; ++i) { var c = path[i]; - if (c == '<') { - var end = path.indexOf('>', i); + if (c == '{') { + var end = path.indexOf('}', i); 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]) { throw new Error('Missing parameter: ' + varName); } - url += isPathVar ? parameters[varName] : encodeURI(parameters[varName]); + url += parameters[varName]; i = end; continue; } @@ -261,15 +240,41 @@ quayApp = angular.module('quay', ['ngRoute', 'chieffancypants.loadingBar', 'angu return url; }; - var getGenericMethodName = function(userMethodName) { - return formatMethodName(userMethodName.replace('_user', '')); + var getGenericOperationName = function(userOperationName) { + return userOperationName.replace('User', ''); }; - var buildMethodsForEndpoint = function(endpoint) { - var method = endpoint.methods[0].toLowerCase(); - var methodName = formatMethodName(endpoint['name']); - apiService[methodName] = function(opt_options, opt_parameters, opt_background) { - var one = Restangular.one(buildUrl(endpoint['path'], opt_parameters)); + var getMatchingUserOperationName = function(orgOperationName, method, userRelatedResource) { + if (userRelatedResource) { + var operations = userRelatedResource['operations']; + for (var i = 0; i < operations.length; ++i) { + 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) { one.withHttpConfig({ 'ignoreLoadingBar': true @@ -278,36 +283,50 @@ quayApp = angular.module('quay', ['ngRoute', 'chieffancypants.loadingBar', 'angu return one['custom' + method.toUpperCase()](opt_options); }; + // If the method for the operation is a GET, add an operationAsResource method. if (method == 'get') { - apiService[methodName + 'AsResource'] = function(opt_parameters, opt_background) { - return getResource(buildUrl(endpoint['path'], opt_parameters), opt_background); + apiService[operationName + 'AsResource'] = function(opt_parameters, opt_background) { + return getResource(buildUrl(path, opt_parameters), opt_background); }; } - if (endpoint['user_method']) { - apiService[getGenericMethodName(endpoint['user_method'])] = function(orgname, opt_options, opt_parameters, opt_background) { + // If the resource has a user-related resource, 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 (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.name) { orgname = orgname.name; } var params = jQuery.extend({'orgname' : orgname}, opt_parameters || {}, opt_background); - return apiService[methodName](opt_options, params); + return apiService[operationName](opt_options, params); } 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) { return apiService; } + var resourceMap = {}; + + // Build the map of resource names to their objects. for (var i = 0; i < window.__endpoints.length; ++i) { - var endpoint = window.__endpoints[i]; - buildMethodsForEndpoint(endpoint); + var endpointResource = window.__endpoints[i]; + 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; @@ -846,7 +865,7 @@ quayApp = angular.module('quay', ['ngRoute', 'chieffancypants.loadingBar', 'angu otherwise({redirectTo: '/'}); }]). config(function(RestangularProvider) { - RestangularProvider.setBaseUrl('/api/'); + RestangularProvider.setBaseUrl('/api/v1/'); }); @@ -1519,7 +1538,7 @@ quayApp.directive('logsView', function () { var loadLogs = Restangular.one(url); loadLogs.customGET().then(function(resp) { - $scope.logsPath = '/api/' + url; + $scope.logsPath = '/api/v1/' + url; if (!$scope.chart) { $scope.chart = new LogUsageChart(logKinds); @@ -2011,7 +2030,7 @@ quayApp.directive('repoSearch', function () { var repoHound = new Bloodhound({ name: 'repositories', remote: { - url: '/api/find/repository?query=%QUERY', + url: '/api/v1/find/repository?query=%QUERY', replace: function (url, uriEncodedQuery) { url = url.replace('%QUERY', uriEncodedQuery); url += '&cb=' + searchToken; @@ -2242,7 +2261,7 @@ quayApp.directive('entitySearch', function () { var entitySearchB = new Bloodhound({ name: 'entities' + number, remote: { - url: '/api/entities/%QUERY', + url: '/api/v1/entities/%QUERY', replace: function (url, uriEncodedQuery) { var namespace = $scope.namespace || ''; url = url.replace('%QUERY', uriEncodedQuery); diff --git a/templates/base.html b/templates/base.html index 852302f90..3a9b23c89 100644 --- a/templates/base.html +++ b/templates/base.html @@ -69,7 +69,7 @@ {% endblock %}