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

View file

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

View file

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

View file

@ -69,7 +69,7 @@
{% endblock %}
<script type="text/javascript">
window.__endpoints = {{ route_data|safe }}.endpoints;
window.__endpoints = {{ route_data|safe }}.apis;
window.__token = '{{ csrf_token() }}';
</script>