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__)
|
||||
|
||||
|
||||
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],
|
||||
}
|
||||
|
@ -124,6 +129,10 @@ 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')
|
||||
|
|
|
@ -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
|
||||
|
|
111
static/js/app.js
111
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);
|
||||
|
|
|
@ -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>
|
||||
|
||||
|
|
Reference in a new issue