Merge pull request #28 from coreos-inc/swagger2

Switch to Swagger v2
This commit is contained in:
Jake Moshenko 2015-06-29 12:18:10 -04:00
commit 6e6b3c675f
23 changed files with 257 additions and 174 deletions

View file

@ -1,3 +1,5 @@
""" Billing information, subscriptions, and plan information. """
import stripe
from flask import request
@ -396,7 +398,7 @@ class UserInvoiceList(ApiResource):
@path_param('orgname', 'The name of the organization')
@related_user_resource(UserInvoiceList)
@show_if(features.BILLING)
class OrgnaizationInvoiceList(ApiResource):
class OrganizationInvoiceList(ApiResource):
""" Resource for listing an orgnaization's invoices. """
@require_scope(scopes.ORG_ADMIN)
@nickname('listOrgInvoices')

View file

@ -1,3 +1,5 @@
""" Create, list, cancel and get status/logs of repository builds. """
import logging
import json
import time
@ -165,7 +167,8 @@ class RepositoryBuildList(RepositoryParamResource):
},
'docker_tags': {
'type': 'array',
'description': 'The tags to which the built images will be pushed',
'description': 'The tags to which the built images will be pushed. ' +
'If none specified, "latest" is used.',
'items': {
'type': 'string'
},

View file

@ -1,5 +1,8 @@
""" API discovery information. """
import re
import logging
import sys
from flask.ext.restful import reqparse
@ -7,7 +10,7 @@ from endpoints.api import (ApiResource, resource, method_metadata, nickname, tru
parse_args, query_param)
from app import app
from auth import scopes
from collections import OrderedDict
logger = logging.getLogger(__name__)
@ -32,162 +35,197 @@ def fully_qualified_name(method_view_class):
def swagger_route_data(include_internal=False, compact=False):
apis = []
def swagger_parameter(name, description, kind='path', param_type='string', required=True,
enum=None, schema=None):
# https://github.com/swagger-api/swagger-spec/blob/master/versions/2.0.md#parameterObject
parameter_info = {
'name': name,
'in': kind,
'required': required
}
if not compact:
parameter_info['description'] = description or ''
if schema:
parameter_info['schema'] = {
'$ref': '#/definitions/%s' % schema
}
else:
parameter_info['type'] = param_type
if enum is not None and len(list(enum)) > 0:
parameter_info['enum'] = list(enum)
return parameter_info
paths = {}
models = {}
tags = []
tags_added = set()
for rule in app.url_map.iter_rules():
endpoint_method = app.view_functions[rule.endpoint]
if 'view_class' in dir(endpoint_method):
view_class = endpoint_method.view_class
# Verify that we have a view class for this API method.
if not 'view_class' in dir(endpoint_method):
continue
param_data_map = {}
if '__api_path_params' in dir(view_class):
param_data_map = view_class.__api_path_params
view_class = endpoint_method.view_class
operations = []
# Hide the class if it is internal.
internal = method_metadata(view_class, 'internal')
if not include_internal and internal:
continue
method_names = list(rule.methods.difference(['HEAD', 'OPTIONS']))
for method_name in method_names:
method = getattr(view_class, method_name.lower(), None)
# Build the tag.
parts = fully_qualified_name(view_class).split('.')
tag_name = parts[-2]
if not tag_name in tags_added:
tags_added.add(tag_name)
tags.append({
'name': tag_name,
'description': (sys.modules[view_class.__module__].__doc__ or '').strip()
})
parameters = []
for param in rule.arguments:
parameters.append({
'paramType': 'path',
'name': param,
'dataType': 'string',
'description': param_data_map.get(param, {'description': ''})['description'],
'required': True,
})
if method is None:
logger.debug('Unable to find method for %s in class %s', method_name, view_class)
else:
req_schema_name = method_metadata(method, 'request_schema')
if req_schema_name:
parameters.append({
'paramType': 'body',
'name': 'body',
'description': 'Request body contents.',
'dataType': req_schema_name,
'required': True,
})
schema = view_class.schemas[req_schema_name]
models[req_schema_name] = schema
if '__api_query_params' in dir(method):
for param_spec in method.__api_query_params:
new_param = {
'paramType': 'query',
'name': param_spec['name'],
'description': param_spec['help'],
'dataType': TYPE_CONVERTER[param_spec['type']],
'required': param_spec['required'],
}
if len(param_spec['choices']) > 0:
new_param['enum'] = list(param_spec['choices'])
parameters.append(new_param)
new_operation = {
'method': method_name,
'nickname': method_metadata(method, 'nickname') or '(unnamed)'
}
if not compact:
response_type = 'void'
res_schema_name = method_metadata(method, 'response_schema')
if res_schema_name:
models[res_schema_name] = view_class.schemas[res_schema_name]
response_type = res_schema_name
new_operation.update({
'type': response_type,
'summary': method.__doc__.strip() if method.__doc__ else '',
'parameters': parameters,
})
scope = method_metadata(method, 'oauth2_scope')
if scope and not compact:
new_operation['authorizations'] = {
'oauth2': [
{
'scope': scope.scope
}
],
}
internal = method_metadata(method, 'internal')
if internal is not None:
new_operation['internal'] = True
if include_internal:
requires_fresh_login = method_metadata(method, 'requires_fresh_login')
if requires_fresh_login is not None:
new_operation['requires_fresh_login'] = True
if not internal or (internal and include_internal):
# Swagger requires valid nicknames on all operations.
if new_operation.get('nickname'):
operations.append(new_operation)
else:
logger.debug('Operation missing nickname: %s' % method)
swagger_path = PARAM_REGEX.sub(r'{\2}', rule.rule)
new_resource = {
'path': swagger_path,
'description': view_class.__doc__.strip() if view_class.__doc__ else "",
'operations': operations,
'name': fully_qualified_name(view_class),
}
# Build the Swagger data for the path.
swagger_path = PARAM_REGEX.sub(r'{\2}', rule.rule)
path_swagger = {
'name': fully_qualified_name(view_class),
'path': swagger_path,
'tag': tag_name
}
if include_internal:
related_user_res = method_metadata(view_class, 'related_user_resource')
if related_user_res is not None:
new_resource['quayUserRelated'] = fully_qualified_name(related_user_res)
path_swagger['quay_user_related'] = fully_qualified_name(related_user_res)
internal = method_metadata(view_class, 'internal')
paths[swagger_path] = path_swagger
# Add any global path parameters.
param_data_map = view_class.__api_path_params if '__api_path_params' in dir(view_class) else {}
if param_data_map:
path_parameters_swagger = []
for path_parameter in param_data_map:
description = param_data_map[path_parameter].get('description')
path_parameters_swagger.append(swagger_parameter(path_parameter, description))
path_swagger['parameters'] = path_parameters_swagger
# Add the individual HTTP operations.
method_names = list(rule.methods.difference(['HEAD', 'OPTIONS']))
for method_name in method_names:
# https://github.com/swagger-api/swagger-spec/blob/master/versions/2.0.md#operation-object
method = getattr(view_class, method_name.lower(), None)
if method is None:
logger.debug('Unable to find method for %s in class %s', method_name, view_class)
continue
operation_swagger = {
'operationId': method_metadata(method, 'nickname') or 'unnamed',
'parameters': [],
}
if not compact:
operation_swagger.update({
'description': method.__doc__.strip() if method.__doc__ else '',
'tags': [tag_name]
})
# Mark the method as internal.
internal = method_metadata(method, 'internal')
if internal is not None:
new_resource['internal'] = True
operation_swagger['internal'] = True
if include_internal:
requires_fresh_login = method_metadata(method, 'requires_fresh_login')
if requires_fresh_login is not None:
operation_swagger['requires_fresh_login'] = True
# Add the path parameters.
if rule.arguments:
for path_parameter in rule.arguments:
description = param_data_map.get(path_parameter, {}).get('description')
operation_swagger['parameters'].append(swagger_parameter(path_parameter, description))
# Add the query parameters.
if '__api_query_params' in dir(method):
for query_parameter_info in method.__api_query_params:
name = query_parameter_info['name']
description = query_parameter_info['help']
param_type = TYPE_CONVERTER[query_parameter_info['type']]
required = query_parameter_info['required']
operation_swagger['parameters'].append(
swagger_parameter(name, description, kind='query',
param_type=param_type,
required=required,
enum=query_parameter_info['choices']))
# Add the OAuth security block.
# https://github.com/swagger-api/swagger-spec/blob/master/versions/2.0.md#securityRequirementObject
scope = method_metadata(method, 'oauth2_scope')
if scope and not compact:
operation_swagger['security'] = [{'oauth2_implicit': scope.scope}]
# TODO: Add the responses block.
# https://github.com/swagger-api/swagger-spec/blob/master/versions/2.0.md#responsesObject
response_schema_name = method_metadata(method, 'response_schema')
if response_schema_name and not compact:
models[response_schema_name] = view_class.schemas[response_schema_name]
response_type = response_schema_name
# Add the request block.
request_schema_name = method_metadata(method, 'request_schema')
if request_schema_name and not compact:
models[request_schema_name] = view_class.schemas[request_schema_name]
operation_swagger['parameters'].append(
swagger_parameter('body', 'Request body contents.', kind='body',
schema=request_schema_name))
# Add the operation to the parent path.
if not internal or (internal and include_internal):
apis.append(new_resource)
path_swagger[method_name.lower()] = operation_swagger
tags.sort(key=lambda t: t['name'])
paths = OrderedDict(sorted(paths.items(), key=lambda p: p[1]['tag']))
# If compact form was requested, simply return the APIs.
if compact:
return {'apis': apis}
return {'paths': paths}
swagger_data = {
'apiVersion': 'v1',
'swaggerVersion': '1.2',
'basePath': '%s://%s' % (PREFERRED_URL_SCHEME, SERVER_HOSTNAME),
'resourcePath': '/',
'swagger': '2.0',
'host': SERVER_HOSTNAME,
'basePath': '/',
'schemes': [
PREFERRED_URL_SCHEME
],
'info': {
'title': 'Quay.io API',
'version': 'v1',
'title': 'Quay.io Frontend',
'description': ('This API allows you to perform many of the operations required to work '
'with Quay.io repositories, users, and organizations. You can find out more '
'at <a href="https://quay.io">Quay.io</a>.'),
'termsOfServiceUrl': 'https://quay.io/tos',
'contact': 'support@quay.io',
'termsOfService': 'https://quay.io/tos',
'contact': {
'email': 'support@quay.io'
}
},
'authorizations': {
'oauth2': {
'scopes': [scope._asdict() for scope in scopes.ALL_SCOPES.values()],
'grantTypes': {
"implicit": {
"tokenName": "access_token",
"loginEndpoint": {
"url": "%s://%s/oauth/authorize" % (PREFERRED_URL_SCHEME, SERVER_HOSTNAME),
},
},
},
'securityDefinitions': {
'oauth2_implicit': {
"type": "oauth2",
"flow": "implicit",
"authorizationUrl": "%s://%s/oauth/authorize" % (PREFERRED_URL_SCHEME, SERVER_HOSTNAME),
'scopes': {scope.scope:scope.description for scope in scopes.ALL_SCOPES.values()},
},
},
'apis': apis,
'models': models,
'paths': paths,
'definitions': models,
'tags': tags
}
return swagger_data

View file

@ -1,3 +1,5 @@
""" List and lookup repository images, and download image diffs. """
import json
from collections import defaultdict

View file

@ -1,3 +1,5 @@
""" Access usage logs for organizations or repositories. """
import json
from datetime import datetime, timedelta

View file

@ -1,3 +1,5 @@
""" Manage organizations, members and OAuth applications. """
import logging
from flask import request
@ -333,7 +335,7 @@ def app_view(application):
@resource('/v1/organization/<orgname>/applications')
@path_param('orgname', 'The name of the organization')
class OrganizationApplications(ApiResource):
""" Resource for managing applications defined by an organizations. """
""" Resource for managing applications defined by an organization. """
schemas = {
'NewApp': {
'id': 'NewApp',

View file

@ -1,3 +1,5 @@
""" Manage repository permissions. """
import logging
from flask import request

View file

@ -1,3 +1,5 @@
""" Manage default permissions added to repositories. """
from flask import request
from endpoints.api import (resource, nickname, ApiResource, validate_json_request, request_error,

View file

@ -1,3 +1,5 @@
""" Authorize repository to send e-mail notifications. """
import logging
from flask import request, abort

View file

@ -1,3 +1,5 @@
""" List, create and manage repositories. """
import logging
import json
import datetime

View file

@ -1,3 +1,5 @@
""" List, create and manage repository events/notifications. """
import json
from flask import request, abort

View file

@ -1,3 +1,5 @@
""" Manage repository access tokens (DEPRECATED). """
import logging
from flask import request

View file

@ -1,3 +1,5 @@
""" Manage user and organization robot accounts. """
from endpoints.api import (resource, nickname, ApiResource, log_action, related_user_resource,
Unauthorized, require_user_admin, internal_only, require_scope,
path_param, parse_args, truthy_bool, query_param)
@ -84,7 +86,6 @@ class UserRobotList(ApiResource):
@resource('/v1/user/robots/<robot_shortname>')
@path_param('robot_shortname', 'The short name for the robot, without any user or organization prefix')
@internal_only
class UserRobot(ApiResource):
""" Resource for managing a user's robots. """
@require_user_admin
@ -180,7 +181,6 @@ class OrgRobot(ApiResource):
@resource('/v1/user/robots/<robot_shortname>/permissions')
@path_param('robot_shortname', 'The short name for the robot, without any user or organization prefix')
@internal_only
class UserRobotPermissions(ApiResource):
""" Resource for listing the permissions a user's robot has in the system. """
@require_user_admin
@ -221,7 +221,6 @@ class OrgRobotPermissions(ApiResource):
@resource('/v1/user/robots/<robot_shortname>/regenerate')
@path_param('robot_shortname', 'The short name for the robot, without any user or organization prefix')
@internal_only
class RegenerateUserRobot(ApiResource):
""" Resource for regenerate an organization's robot's token. """
@require_user_admin

View file

@ -1,3 +1,5 @@
""" Conduct searches against all registry context. """
from endpoints.api import (ApiResource, parse_args, query_param, truthy_bool, nickname, resource,
require_scope, path_param)
from data import model

View file

@ -1,3 +1,5 @@
""" Subscribe to plans. """
import logging
import stripe

View file

@ -1,3 +1,5 @@
""" Superuser Config API. """
import logging
import os
import json

View file

@ -1,3 +1,5 @@
""" Superuser API. """
import string
import logging
import json

View file

@ -1,3 +1,5 @@
""" Manage the tags of a repository. """
from flask import request, abort
from endpoints.api import (resource, nickname, require_repo_read, require_repo_write,

View file

@ -1,3 +1,5 @@
""" Create, list and manage an organization's teams. """
from flask import request
from endpoints.api import (resource, nickname, ApiResource, validate_json_request, request_error,

View file

@ -1,3 +1,5 @@
""" Create, list and manage build triggers. """
import json
import logging
@ -397,7 +399,7 @@ class ActivateBuildTrigger(RepositoryParamResource):
'properties': {
'branch_name': {
'type': 'string',
'description': '(GitHub Only) If specified, the name of the GitHub branch to build.'
'description': '(SCM only) If specified, the name of the branch to build.'
},
'commit_sha': {
'type': 'string',

View file

@ -1,3 +1,5 @@
""" Manage the current user. """
import logging
import json
@ -183,11 +185,17 @@ class User(ApiResource):
},
'organizations': {
'type': 'array',
'description': 'Information about the organizations in which the user is a member'
'description': 'Information about the organizations in which the user is a member',
'items': {
'type': 'object'
}
},
'logins': {
'type': 'array',
'description': 'The list of external login providers against which the user has authenticated'
'description': 'The list of external login providers against which the user has authenticated',
'items': {
'type': 'object'
}
},
'can_create_repo': {
'type': 'boolean',
@ -807,10 +815,10 @@ class StarredRepositoryList(ApiResource):
@path_param('repository', 'The full path of the repository. e.g. namespace/name')
class StarredRepository(RepositoryParamResource):
""" Operations for managing a specific starred repository. """
@nickname('deleteStar')
@require_user_admin
def delete(self, namespace, repository):
""" Removes a star from a repository. """
user = get_authenticated_user()
repo = model.get_repository(namespace, repository)

View file

@ -101,27 +101,14 @@ angular.module('quay').factory('ApiService', ['Restangular', '$q', 'UtilService'
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'];
}
if (userRelatedResource[method.toLowerCase()]) {
return userRelatedResource[method.toLowerCase()]['operationId'];
}
}
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 freshLoginInProgress = [];
var reject = function(msg) {
for (var i = 0; i < freshLoginInProgress.length; ++i) {
@ -224,14 +211,13 @@ angular.module('quay').factory('ApiService', ['Restangular', '$q', 'UtilService'
};
};
var buildMethodsForOperation = function(operation, resource, resourceMap) {
var method = operation['method'].toLowerCase();
var operationName = operation['nickname'];
var path = resource['path'];
var buildMethodsForOperation = function(operation, method, path, resourceMap) {
var operationName = operation['operationId'];
var urlPath = path['path'];
// Add the operation itself.
apiService[operationName] = function(opt_options, opt_parameters, opt_background, opt_forcessl) {
var one = Restangular.one(buildUrl(path, opt_parameters, opt_forcessl));
var one = Restangular.one(buildUrl(urlPath, opt_parameters, opt_forcessl));
if (opt_background) {
one.withHttpConfig({
'ignoreLoadingBar': true
@ -251,15 +237,15 @@ angular.module('quay').factory('ApiService', ['Restangular', '$q', 'UtilService'
// If the method for the operation is a GET, add an operationAsResource method.
if (method == 'get') {
apiService[operationName + 'AsResource'] = function(opt_parameters, opt_background) {
return getResource(buildUrl(path, opt_parameters), opt_background);
return getResource(buildUrl(urlPath, opt_parameters), opt_background);
};
}
// If the resource has a user-related resource, then make a generic operation for this operation
// If the operation has a user-related operation, 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']]);
if (path['quay_user_related']) {
var userOperationName = getMatchingUserOperationName(operationName, method, resourceMap[path['quay_user_related']]);
var genericOperationName = getGenericOperationName(userOperationName);
apiService[genericOperationName] = function(orgname, opt_options, opt_parameters, opt_background) {
if (orgname) {
@ -280,19 +266,34 @@ angular.module('quay').factory('ApiService', ['Restangular', '$q', 'UtilService'
return apiService;
}
var allowedMethods = ['get', 'post', 'put', 'delete'];
var resourceMap = {};
var forEachOperation = function(callback) {
for (var path in window.__endpoints) {
if (!window.__endpoints.hasOwnProperty(path)) {
continue;
}
for (var method in window.__endpoints[path]) {
if (!window.__endpoints[path].hasOwnProperty(method)) {
continue;
}
if (allowedMethods.indexOf(method.toLowerCase()) < 0) { continue; }
callback(window.__endpoints[path][method], method, window.__endpoints[path]);
}
}
};
// Build the map of resource names to their objects.
for (var i = 0; i < window.__endpoints.length; ++i) {
var endpointResource = window.__endpoints[i];
resourceMap[endpointResource['name']] = endpointResource;
}
forEachOperation(function(operation, method, path) {
resourceMap[path.name] = path;
});
// Construct the methods for each API endpoint.
for (var i = 0; i < window.__endpoints.length; ++i) {
var endpointResource = window.__endpoints[i];
buildMethodsForEndpointResource(endpointResource, resourceMap);
}
forEachOperation(function(operation, method, path) {
buildMethodsForOperation(operation, method, path, resourceMap);
});
apiService.getErrorMessage = function(resp, defaultMessage) {
var message = defaultMessage;

View file

@ -41,7 +41,7 @@
{% endblock %}
<script type="text/javascript">
window.__endpoints = {{ route_data|safe }}.apis;
window.__endpoints = {{ route_data|safe }}.paths;
window.__features = {{ feature_set|safe }};
window.__config = {{ config_set|safe }};
window.__oauth = {{ oauth_set|safe }};