Merge remote-tracking branch 'origin/master' into tagyourit

Conflicts:
	static/css/quay.css
	static/js/graphing.js
	static/partials/view-repo.html
	test/data/test.db
This commit is contained in:
jakedt 2014-04-15 15:58:30 -04:00
commit 3f42d15335
132 changed files with 4266 additions and 1924 deletions

View file

@ -85,11 +85,32 @@ def handle_api_error(error):
def resource(*urls, **kwargs):
def wrapper(api_resource):
if not api_resource:
return None
api.add_resource(api_resource, *urls, **kwargs)
return api_resource
return wrapper
def show_if(value):
def f(inner):
if not value:
return None
return inner
return f
def hide_if(value):
def f(inner):
if value:
return None
return inner
return f
def truthy_bool(param):
return param not in {False, 'false', 'False', '0', 'FALSE', '', 'null'}
@ -103,6 +124,9 @@ def format_date(date):
def add_method_metadata(name, value):
def modifier(func):
if func is None:
return None
if '__api_metadata' not in dir(func):
func.__api_metadata = {}
func.__api_metadata[name] = value
@ -111,11 +135,15 @@ def add_method_metadata(name, value):
def method_metadata(func, name):
if func is None:
return None
if '__api_metadata' in dir(func):
return func.__api_metadata.get(name, None)
return None
nickname = partial(add_method_metadata, 'nickname')
related_user_resource = partial(add_method_metadata, 'related_user_resource')
internal_only = add_method_metadata('internal', True)
@ -274,6 +302,7 @@ import endpoints.api.repository
import endpoints.api.repotoken
import endpoints.api.robot
import endpoints.api.search
import endpoints.api.superuser
import endpoints.api.tag
import endpoints.api.team
import endpoints.api.trigger

View file

@ -1,16 +1,17 @@
import stripe
from flask import request
from app import billing
from endpoints.api import (resource, nickname, ApiResource, validate_json_request, log_action,
related_user_resource, internal_only, Unauthorized, NotFound,
require_user_admin)
require_user_admin, show_if, hide_if)
from endpoints.api.subscribe import subscribe, subscription_view
from auth.permissions import AdministerOrganizationPermission
from auth.auth_context import get_authenticated_user
from data import model
from data.plans import PLANS
from data.billing import PLANS
import features
def carderror_response(e):
return {'carderror': e.message}, 402
@ -22,7 +23,7 @@ def get_card(user):
}
if user.stripe_id:
cus = stripe.Customer.retrieve(user.stripe_id)
cus = billing.Customer.retrieve(user.stripe_id)
if cus and cus.default_card:
# Find the default card.
default_card = None
@ -43,7 +44,7 @@ def get_card(user):
def set_card(user, token):
if user.stripe_id:
cus = stripe.Customer.retrieve(user.stripe_id)
cus = billing.Customer.retrieve(user.stripe_id)
if cus:
try:
cus.card = token
@ -72,13 +73,14 @@ def get_invoices(customer_id):
'plan': i.lines.data[0].plan.id if i.lines.data[0].plan else None
}
invoices = stripe.Invoice.all(customer=customer_id, count=12)
invoices = billing.Invoice.all(customer=customer_id, count=12)
return {
'invoices': [invoice_view(i) for i in invoices.data]
}
@resource('/v1/plans/')
@show_if(features.BILLING)
class ListPlans(ApiResource):
""" Resource for listing the available plans. """
@nickname('listPlans')
@ -91,6 +93,7 @@ class ListPlans(ApiResource):
@resource('/v1/user/card')
@internal_only
@show_if(features.BILLING)
class UserCard(ApiResource):
""" Resource for managing a user's credit card. """
schemas = {
@ -132,6 +135,7 @@ class UserCard(ApiResource):
@resource('/v1/organization/<orgname>/card')
@internal_only
@related_user_resource(UserCard)
@show_if(features.BILLING)
class OrganizationCard(ApiResource):
""" Resource for managing an organization's credit card. """
schemas = {
@ -178,6 +182,7 @@ class OrganizationCard(ApiResource):
@resource('/v1/user/plan')
@internal_only
@show_if(features.BILLING)
class UserPlan(ApiResource):
""" Resource for managing a user's subscription. """
schemas = {
@ -220,7 +225,7 @@ class UserPlan(ApiResource):
private_repos = model.get_private_repo_count(user.username)
if user.stripe_id:
cus = stripe.Customer.retrieve(user.stripe_id)
cus = billing.Customer.retrieve(user.stripe_id)
if cus.subscription:
return subscription_view(cus.subscription, private_repos)
@ -234,6 +239,7 @@ class UserPlan(ApiResource):
@resource('/v1/organization/<orgname>/plan')
@internal_only
@related_user_resource(UserPlan)
@show_if(features.BILLING)
class OrganizationPlan(ApiResource):
""" Resource for managing a org's subscription. """
schemas = {
@ -279,7 +285,7 @@ class OrganizationPlan(ApiResource):
private_repos = model.get_private_repo_count(orgname)
organization = model.get_organization(orgname)
if organization.stripe_id:
cus = stripe.Customer.retrieve(organization.stripe_id)
cus = billing.Customer.retrieve(organization.stripe_id)
if cus.subscription:
return subscription_view(cus.subscription, private_repos)
@ -294,6 +300,7 @@ class OrganizationPlan(ApiResource):
@resource('/v1/user/invoices')
@internal_only
@show_if(features.BILLING)
class UserInvoiceList(ApiResource):
""" Resource for listing a user's invoices. """
@require_user_admin
@ -310,6 +317,7 @@ class UserInvoiceList(ApiResource):
@resource('/v1/organization/<orgname>/invoices')
@internal_only
@related_user_resource(UserInvoiceList)
@show_if(features.BILLING)
class OrgnaizationInvoiceList(ApiResource):
""" Resource for listing an orgnaization's invoices. """
@nickname('listOrgInvoices')
@ -323,4 +331,4 @@ class OrgnaizationInvoiceList(ApiResource):
return get_invoices(organization.stripe_id)
raise Unauthorized()
raise Unauthorized()

View file

@ -3,19 +3,20 @@ import json
from flask import request
from app import app
from app import app, userfiles as user_files
from endpoints.api import (RepositoryParamResource, parse_args, query_param, nickname, resource,
require_repo_read, require_repo_write, validate_json_request,
ApiResource, internal_only, format_date, api, Unauthorized, NotFound)
from endpoints.common import start_build
from endpoints.trigger import BuildTrigger
from data import model
from auth.permissions import ModifyRepositoryPermission
from auth.auth_context import get_authenticated_user
from auth.permissions import ModifyRepositoryPermission, AdministerOrganizationPermission
from data.buildlogs import BuildStatusRetrievalError
from util.names import parse_robot_username
logger = logging.getLogger(__name__)
user_files = app.config['USERFILES']
build_logs = app.config['BUILDLOGS']
@ -33,7 +34,15 @@ def get_job_config(build_obj):
return None
def user_view(user):
return {
'name': user.username,
'kind': 'user',
'is_robot': user.robot,
}
def trigger_view(trigger):
if trigger and trigger.uuid:
config_dict = get_trigger_config(trigger)
build_trigger = BuildTrigger.get_trigger_for_service(trigger.service.name)
@ -42,7 +51,8 @@ def trigger_view(trigger):
'config': config_dict,
'id': trigger.uuid,
'connected_user': trigger.connected_user.username,
'is_active': build_trigger.is_active(config_dict)
'is_active': build_trigger.is_active(config_dict),
'pull_robot': user_view(trigger.pull_robot) if trigger.pull_robot else None
}
return None
@ -67,6 +77,7 @@ def build_status_view(build_obj, can_write=False):
'is_writer': can_write,
'trigger': trigger_view(build_obj.trigger),
'resource_key': build_obj.resource_key,
'pull_robot': user_view(build_obj.pull_robot) if build_obj.pull_robot else None,
}
if can_write:
@ -95,6 +106,10 @@ class RepositoryBuildList(RepositoryParamResource):
'type': 'string',
'description': 'Subdirectory in which the Dockerfile can be found',
},
'pull_robot': {
'type': 'string',
'description': 'Username of a Quay robot account to use as pull credentials',
}
},
},
}
@ -123,6 +138,22 @@ class RepositoryBuildList(RepositoryParamResource):
dockerfile_id = request_json['file_id']
subdir = request_json['subdirectory'] if 'subdirectory' in request_json else ''
pull_robot_name = request_json.get('pull_robot', None)
# Verify the security behind the pull robot.
if pull_robot_name:
result = parse_robot_username(pull_robot_name)
if result:
pull_robot = model.lookup_robot(pull_robot_name)
if not pull_robot:
raise NotFound()
# Make sure the user has administer permissions for the robot's namespace.
(robot_namespace, shortname) = result
if not AdministerOrganizationPermission(robot_namespace).can():
raise Unauthorized()
else:
raise Unauthorized()
# Check if the dockerfile resource has already been used. If so, then it
# can only be reused if the user has access to the repository for which it
@ -137,7 +168,8 @@ class RepositoryBuildList(RepositoryParamResource):
repo = model.get_repository(namespace, repository)
display_name = user_files.get_file_checksum(dockerfile_id)
build_request = start_build(repo, dockerfile_id, ['latest'], display_name, subdir, True)
build_request = start_build(repo, dockerfile_id, ['latest'], display_name, subdir, True,
pull_robot_name=pull_robot_name)
resp = build_status_view(build_request, True)
repo_string = '%s/%s' % (namespace, repository)

View file

@ -23,13 +23,12 @@ TYPE_CONVERTER = {
int: 'integer',
}
URL_SCHEME = app.config['URL_SCHEME']
URL_HOST = app.config['URL_HOST']
PREFERRED_URL_SCHEME = app.config['PREFERRED_URL_SCHEME']
SERVER_HOSTNAME = app.config['SERVER_HOSTNAME']
def fully_qualified_name(method_view_class):
inst = method_view_class()
return '%s.%s' % (inst.__module__, inst.__class__.__name__)
return '%s.%s' % (method_view_class.__module__, method_view_class.__name__)
def swagger_route_data(include_internal=False, compact=False):
@ -143,7 +142,7 @@ def swagger_route_data(include_internal=False, compact=False):
swagger_data = {
'apiVersion': 'v1',
'swaggerVersion': '1.2',
'basePath': '%s://%s' % (URL_SCHEME, URL_HOST),
'basePath': '%s://%s' % (PREFERRED_URL_SCHEME, SERVER_HOSTNAME),
'resourcePath': '/',
'info': {
'title': 'Quay.io API',
@ -160,7 +159,7 @@ def swagger_route_data(include_internal=False, compact=False):
"implicit": {
"tokenName": "access_token",
"loginEndpoint": {
"url": "%s://%s/oauth/authorize" % (URL_SCHEME, URL_HOST),
"url": "%s://%s/oauth/authorize" % (PREFERRED_URL_SCHEME, SERVER_HOSTNAME),
},
},
},

View file

@ -2,16 +2,13 @@ import json
from collections import defaultdict
from app import app
from app import storage as store
from endpoints.api import (resource, nickname, require_repo_read, RepositoryParamResource,
format_date, NotFound)
from data import model
from util.cache import cache_control_flask_restful
store = app.config['STORAGE']
def image_view(image):
extended_props = image
if image.storage and image.storage.id:

View file

@ -29,8 +29,7 @@ def log_view(log):
return view
def get_logs(namespace, start_time, end_time, performer_name=None,
repository=None):
def get_logs(start_time, end_time, performer_name=None, repository=None, namespace=None):
performer = None
if performer_name:
performer = model.get_user(performer_name)
@ -54,8 +53,8 @@ def get_logs(namespace, start_time, end_time, performer_name=None,
if not end_time:
end_time = datetime.today()
logs = model.list_logs(namespace, start_time, end_time, performer=performer,
repository=repository)
logs = model.list_logs(start_time, end_time, performer=performer, repository=repository,
namespace=namespace)
return {
'start_time': format_date(start_time),
'end_time': format_date(end_time),
@ -80,7 +79,7 @@ class RepositoryLogs(RepositoryParamResource):
start_time = args['starttime']
end_time = args['endtime']
return get_logs(namespace, start_time, end_time, repository=repo)
return get_logs(start_time, end_time, repository=repo, namespace=namespace)
@resource('/v1/user/logs')
@ -100,7 +99,7 @@ class UserLogs(ApiResource):
end_time = args['endtime']
user = get_authenticated_user()
return get_logs(user.username, start_time, end_time, performer_name=performer_name)
return get_logs(start_time, end_time, performer_name=performer_name, namespace=user.username)
@resource('/v1/organization/<orgname>/logs')
@ -121,6 +120,6 @@ class OrgLogs(ApiResource):
start_time = args['starttime']
end_time = args['endtime']
return get_logs(orgname, start_time, end_time, performer_name=performer_name)
return get_logs(start_time, end_time, namespace=orgname, performer_name=performer_name)
raise Unauthorized()
raise Unauthorized()

View file

@ -1,20 +1,22 @@
import logging
import stripe
from flask import request
from app import billing as stripe
from endpoints.api import (resource, nickname, ApiResource, validate_json_request, request_error,
related_user_resource, internal_only, Unauthorized, NotFound,
require_user_admin, log_action)
require_user_admin, log_action, show_if)
from endpoints.api.team import team_view
from endpoints.api.user import User, PrivateRepositories
from auth.permissions import (AdministerOrganizationPermission, OrganizationMemberPermission,
CreateRepositoryPermission)
from auth.auth_context import get_authenticated_user
from data import model
from data.plans import get_plan
from data.billing import get_plan
from util.gravatar import compute_hash
import features
logger = logging.getLogger(__name__)
@ -163,6 +165,7 @@ class Organization(ApiResource):
@resource('/v1/organization/<orgname>/private')
@internal_only
@related_user_resource(PrivateRepositories)
@show_if(features.BILLING)
class OrgPrivateRepositories(ApiResource):
""" Custom verb to compute whether additional private repositories are available. """
@nickname('getOrganizationPrivateAllowed')

View file

@ -1,11 +1,13 @@
import logging
import stripe
from app import billing
from endpoints.api import request_error, log_action, NotFound
from endpoints.common import check_repository_usage
from data import model
from data.plans import PLANS
from data.billing import PLANS
import features
logger = logging.getLogger(__name__)
@ -24,6 +26,9 @@ def subscription_view(stripe_subscription, used_repos):
def subscribe(user, plan, token, require_business_plan):
if not features.BILLING:
return
plan_found = None
for plan_obj in PLANS:
if plan_obj['stripeId'] == plan:
@ -56,7 +61,7 @@ def subscribe(user, plan, token, require_business_plan):
card = token
try:
cus = stripe.Customer.create(email=user.email, plan=plan, card=card)
cus = billing.Customer.create(email=user.email, plan=plan, card=card)
user.stripe_id = cus.id
user.save()
check_repository_usage(user, plan_found)
@ -69,7 +74,7 @@ def subscribe(user, plan, token, require_business_plan):
else:
# Change the plan
cus = stripe.Customer.retrieve(user.stripe_id)
cus = billing.Customer.retrieve(user.stripe_id)
if plan_found['price'] == 0:
if cus.subscription is not None:

160
endpoints/api/superuser.py Normal file
View file

@ -0,0 +1,160 @@
import logging
import json
from app import app
from flask import request
from endpoints.api import (ApiResource, nickname, resource, validate_json_request, request_error,
log_action, internal_only, NotFound, require_user_admin, format_date,
InvalidToken, require_scope, format_date, hide_if, show_if, parse_args,
query_param, abort)
from endpoints.api.logs import get_logs
from data import model
from auth.permissions import SuperUserPermission
from auth.auth_context import get_authenticated_user
import features
logger = logging.getLogger(__name__)
@resource('/v1/superuser/logs')
@internal_only
@show_if(features.SUPER_USERS)
class SuperUserLogs(ApiResource):
""" Resource for fetching all logs in the system. """
@nickname('listAllLogs')
@parse_args
@query_param('starttime', 'Earliest time from which to get logs. (%m/%d/%Y %Z)', type=str)
@query_param('endtime', 'Latest time to which to get logs. (%m/%d/%Y %Z)', type=str)
@query_param('performer', 'Username for which to filter logs.', type=str)
def get(self, args):
""" List the logs for the current system. """
if SuperUserPermission().can():
performer_name = args['performer']
start_time = args['starttime']
end_time = args['endtime']
return get_logs(start_time, end_time)
abort(403)
@resource('/v1/superuser/seats')
@internal_only
@show_if(features.SUPER_USERS)
@hide_if(features.BILLING)
class SeatUsage(ApiResource):
""" Resource for managing the seats granted in the license for the system. """
@nickname('getSeatCount')
def get(self):
""" Returns the current number of seats being used in the system. """
if SuperUserPermission().can():
return {
'count': model.get_active_user_count(),
'allowed': app.config.get('LICENSE_SEAT_COUNT', 0)
}
abort(403)
def user_view(user):
return {
'username': user.username,
'email': user.email,
'verified': user.verified,
'super_user': user.username in app.config['SUPER_USERS']
}
@resource('/v1/superuser/users/')
@internal_only
@show_if(features.SUPER_USERS)
class SuperUserList(ApiResource):
""" Resource for listing users in the system. """
@nickname('listAllUsers')
def get(self):
""" Returns a list of all users in the system. """
if SuperUserPermission().can():
users = model.get_active_users()
return {
'users': [user_view(user) for user in users]
}
abort(403)
@resource('/v1/superuser/users/<username>')
@internal_only
@show_if(features.SUPER_USERS)
class SuperUserManagement(ApiResource):
""" Resource for managing users in the system. """
schemas = {
'UpdateUser': {
'id': 'UpdateUser',
'type': 'object',
'description': 'Description of updates for a user',
'properties': {
'password': {
'type': 'string',
'description': 'The new password for the user',
},
'email': {
'type': 'string',
'description': 'The new e-mail address for the user',
}
},
},
}
@nickname('getInstallUser')
def get(self, username):
""" Returns information about the specified user. """
if SuperUserPermission().can():
user = model.get_user(username)
if not user or user.organization or user.robot:
abort(404)
return user_view(user)
abort(403)
@nickname('deleteInstallUser')
def delete(self, username):
""" Deletes the specified user. """
if SuperUserPermission().can():
user = model.get_user(username)
if not user or user.organization or user.robot:
abort(404)
if username in app.config['SUPER_USERS']:
abort(403)
model.delete_user(user)
return 'Deleted', 204
abort(403)
@nickname('changeInstallUser')
@validate_json_request('UpdateUser')
def put(self, username):
""" Updates information about the specified user. """
if SuperUserPermission().can():
user = model.get_user(username)
if not user or user.organization or user.robot:
abort(404)
if username in app.config['SUPER_USERS']:
abort(403)
user_data = request.get_json()
if 'password' in user_data:
model.change_password(user, user_data['password'])
if 'email' in user_data:
model.update_email(user, user_data['email'])
return user_view(user)
abort(403)

View file

@ -16,7 +16,9 @@ from endpoints.trigger import (BuildTrigger as BuildTriggerBase, TriggerDeactiva
TriggerActivationException, EmptyRepositoryException,
RepositoryReadException)
from data import model
from auth.permissions import UserAdminPermission
from auth.permissions import UserAdminPermission, AdministerOrganizationPermission, ReadRepositoryPermission
from util.names import parse_robot_username
from util.dockerfileparse import parse_dockerfile
logger = logging.getLogger(__name__)
@ -139,7 +141,19 @@ class BuildTriggerActivate(RepositoryParamResource):
'BuildTriggerActivateRequest': {
'id': 'BuildTriggerActivateRequest',
'type': 'object',
'description': 'Arbitrary json.',
'required': [
'config'
],
'properties': {
'config': {
'type': 'object',
'description': 'Arbitrary json.',
},
'pull_robot': {
'type': 'string',
'description': 'The name of the robot that will be used to pull images.'
}
}
},
}
@ -160,7 +174,27 @@ class BuildTriggerActivate(RepositoryParamResource):
user_permission = UserAdminPermission(trigger.connected_user.username)
if user_permission.can():
new_config_dict = request.get_json()
# Update the pull robot (if any).
pull_robot_name = request.get_json().get('pull_robot', None)
if pull_robot_name:
pull_robot = model.lookup_robot(pull_robot_name)
if not pull_robot:
raise NotFound()
# Make sure the user has administer permissions for the robot's namespace.
(robot_namespace, shortname) = parse_robot_username(pull_robot_name)
if not AdministerOrganizationPermission(robot_namespace).can():
raise Unauthorized()
# Make sure the namespace matches that of the trigger.
if robot_namespace != namespace:
raise Unauthorized()
# Set the pull robot.
trigger.pull_robot = pull_robot
# Update the config.
new_config_dict = request.get_json()['config']
token_name = 'Build Trigger: %s' % trigger.service.name
token = model.create_delegate_token(namespace, repository, token_name,
@ -171,9 +205,8 @@ class BuildTriggerActivate(RepositoryParamResource):
trigger.repository.name)
path = url_for('webhooks.build_trigger_webhook',
repository=repository_path, trigger_uuid=trigger.uuid)
authed_url = _prepare_webhook_url(app.config['URL_SCHEME'], '$token',
token.code, app.config['URL_HOST'],
path)
authed_url = _prepare_webhook_url(app.config['PREFERRED_URL_SCHEME'], '$token', token.code,
app.config['SERVER_HOSTNAME'], path)
final_config = handler.activate(trigger.uuid, authed_url,
trigger.auth_token, new_config_dict)
@ -191,6 +224,7 @@ class BuildTriggerActivate(RepositoryParamResource):
log_action('setup_repo_trigger', namespace,
{'repo': repository, 'namespace': namespace,
'trigger_id': trigger.uuid, 'service': trigger.service.name,
'pull_robot': trigger.pull_robot.username if trigger.pull_robot else None,
'config': final_config}, repo=repo)
return trigger_view(trigger)
@ -198,6 +232,141 @@ class BuildTriggerActivate(RepositoryParamResource):
raise Unauthorized()
@resource('/v1/repository/<repopath:repository>/trigger/<trigger_uuid>/analyze')
@internal_only
class BuildTriggerAnalyze(RepositoryParamResource):
""" Custom verb for analyzing the config for a build trigger and suggesting various changes
(such as a robot account to use for pulling)
"""
schemas = {
'BuildTriggerAnalyzeRequest': {
'id': 'BuildTriggerAnalyzeRequest',
'type': 'object',
'required': [
'config'
],
'properties': {
'config': {
'type': 'object',
'description': 'Arbitrary json.',
}
}
},
}
@require_repo_admin
@nickname('analyzeBuildTrigger')
@validate_json_request('BuildTriggerAnalyzeRequest')
def post(self, namespace, repository, trigger_uuid):
""" Analyze the specified build trigger configuration. """
try:
trigger = model.get_build_trigger(namespace, repository, trigger_uuid)
except model.InvalidBuildTriggerException:
raise NotFound()
handler = BuildTriggerBase.get_trigger_for_service(trigger.service.name)
new_config_dict = request.get_json()['config']
try:
# Load the contents of the Dockerfile.
contents = handler.load_dockerfile_contents(trigger.auth_token, new_config_dict)
if not contents:
return {
'status': 'error',
'message': 'Could not read the Dockerfile for the trigger'
}
# Parse the contents of the Dockerfile.
parsed = parse_dockerfile(contents)
if not parsed:
return {
'status': 'error',
'message': 'Could not parse the Dockerfile specified'
}
# Determine the base image (i.e. the FROM) for the Dockerfile.
base_image = parsed.get_base_image()
if not base_image:
return {
'status': 'warning',
'message': 'No FROM line found in the Dockerfile'
}
# Check to see if the base image lives in Quay.
quay_registry_prefix = '%s/' % (app.config['SERVER_HOSTNAME'])
if not base_image.startswith(quay_registry_prefix):
return {
'status': 'publicbase'
}
# Lookup the repository in Quay.
result = base_image[len(quay_registry_prefix):].split('/', 2)
if len(result) != 2:
return {
'status': 'warning',
'message': '"%s" is not a valid Quay repository path' % (base_image)
}
(base_namespace, base_repository) = result
found_repository = model.get_repository(base_namespace, base_repository)
if not found_repository:
return {
'status': 'error',
'message': 'Repository "%s" was not found' % (base_image)
}
# If the repository is private and the user cannot see that repo, then
# mark it as not found.
can_read = ReadRepositoryPermission(base_namespace, base_repository)
if found_repository.visibility.name != 'public' and not can_read:
return {
'status': 'error',
'message': 'Repository "%s" was not found' % (base_image)
}
# Check to see if the repository is public. If not, we suggest the
# usage of a robot account to conduct the pull.
read_robots = []
if AdministerOrganizationPermission(base_namespace).can():
def robot_view(robot):
return {
'name': robot.username,
'kind': 'user',
'is_robot': True
}
def is_valid_robot(user):
# Make sure the user is a robot.
if not user.robot:
return False
# Make sure the current user can see/administer the robot.
(robot_namespace, shortname) = parse_robot_username(user.username)
return AdministerOrganizationPermission(robot_namespace).can()
repo_perms = model.get_all_repo_users(base_namespace, base_repository)
read_robots = [robot_view(perm.user) for perm in repo_perms if is_valid_robot(perm.user)]
return {
'namespace': base_namespace,
'name': base_repository,
'is_public': found_repository.visibility.name == 'public',
'robots': read_robots,
'status': 'analyzed',
'dockerfile_url': handler.dockerfile_url(trigger.auth_token, new_config_dict)
}
except RepositoryReadException as rre:
return {
'status': 'error',
'message': rre.message
}
raise NotFound()
@resource('/v1/repository/<repopath:repository>/trigger/<trigger_uuid>/start')
class ActivateBuildTrigger(RepositoryParamResource):
""" Custom verb to manually activate a build trigger. """
@ -220,8 +389,10 @@ class ActivateBuildTrigger(RepositoryParamResource):
dockerfile_id, tags, name, subdir = specs
repo = model.get_repository(namespace, repository)
pull_robot_name = model.get_pull_robot_name(trigger)
build_request = start_build(repo, dockerfile_id, tags, name, subdir, True)
build_request = start_build(repo, dockerfile_id, tags, name, subdir, True,
pull_robot_name=pull_robot_name)
resp = build_status_view(build_request, True)
repo_string = '%s/%s' % (namespace, repository)

View file

@ -1,27 +1,27 @@
import logging
import stripe
import json
from flask import request
from flask.ext.login import logout_user
from flask.ext.principal import identity_changed, AnonymousIdentity
from app import app
from app import app, billing as stripe
from endpoints.api import (ApiResource, nickname, resource, validate_json_request, request_error,
log_action, internal_only, NotFound, require_user_admin,
InvalidToken, require_scope, format_date)
InvalidToken, require_scope, format_date, hide_if, show_if)
from endpoints.api.subscribe import subscribe
from endpoints.common import common_login
from data import model
from data.plans import get_plan
from data.billing import get_plan
from auth.permissions import (AdministerOrganizationPermission, CreateRepositoryPermission,
UserAdminPermission, UserReadPermission)
UserAdminPermission, UserReadPermission, SuperUserPermission)
from auth.auth_context import get_authenticated_user
from auth import scopes
from util.gravatar import compute_hash
from util.email import (send_confirmation_email, send_recovery_email,
send_change_email)
import features
logger = logging.getLogger(__name__)
@ -65,6 +65,11 @@ def user_view(user):
'preferred_namespace': not (user.stripe_id is None),
})
if features.SUPER_USERS:
user_response.update({
'super_user': user and user == get_authenticated_user() and SuperUserPermission().can()
})
return user_response
@ -193,6 +198,7 @@ class User(ApiResource):
@resource('/v1/user/private')
@internal_only
@show_if(features.BILLING)
class PrivateRepositories(ApiResource):
""" Operations dealing with the available count of private repositories. """
@require_user_admin
@ -248,8 +254,7 @@ class ConvertToOrganization(ApiResource):
'description': 'Information required to convert a user to an organization.',
'required': [
'adminUser',
'adminPassword',
'plan',
'adminPassword'
],
'properties': {
'adminUser': {
@ -262,7 +267,7 @@ class ConvertToOrganization(ApiResource):
},
'plan': {
'type': 'string',
'description': 'The plan to which the organizatino should be subscribed',
'description': 'The plan to which the organization should be subscribed',
},
},
},
@ -289,8 +294,9 @@ class ConvertToOrganization(ApiResource):
message='The admin user credentials are not valid')
# Subscribe the organization to the new plan.
plan = convert_data['plan']
subscribe(user, plan, None, True) # Require business plans
if features.BILLING:
plan = convert_data.get('plan', 'free')
subscribe(user, plan, None, True) # Require business plans
# Convert the user to an organization.
model.convert_user_to_organization(user, model.get_user(admin_username))

View file

@ -3,14 +3,15 @@ import logging
from flask import request, redirect, url_for, Blueprint
from flask.ext.login import current_user
from endpoints.common import render_page_template, common_login
from app import app, mixpanel
from endpoints.common import render_page_template, common_login, route_show_if
from app import app, analytics
from data import model
from util.names import parse_repository_name
from util.http import abort
from auth.permissions import AdministerRepositoryPermission
from auth.auth import require_session_login
import features
logger = logging.getLogger(__name__)
@ -48,6 +49,7 @@ def get_github_user(token):
@callback.route('/github/callback', methods=['GET'])
@route_show_if(features.GITHUB_LOGIN)
def github_oauth_callback():
error = request.args.get('error', None)
if error:
@ -83,13 +85,13 @@ def github_oauth_callback():
to_login = model.create_federated_user(username, found_email, 'github',
github_id)
# Success, tell mixpanel
mixpanel.track(to_login.username, 'register', {'service': 'github'})
# Success, tell analytics
analytics.track(to_login.username, 'register', {'service': 'github'})
state = request.args.get('state', None)
if state:
logger.debug('Aliasing with state: %s' % state)
mixpanel.alias(to_login.username, state)
analytics.alias(to_login.username, state)
except model.DataModelException, ex:
return render_page_template('githuberror.html', error_message=ex.message)
@ -101,6 +103,7 @@ def github_oauth_callback():
@callback.route('/github/callback/attach', methods=['GET'])
@route_show_if(features.GITHUB_LOGIN)
@require_session_login
def github_oauth_attach():
token = exchange_github_code_for_token(request.args.get('code'))

View file

@ -3,7 +3,7 @@ import urlparse
import json
import string
from flask import make_response, render_template, request
from flask import make_response, render_template, request, abort
from flask.ext.login import login_user, UserMixin
from flask.ext.principal import identity_changed
from random import SystemRandom
@ -15,7 +15,10 @@ from auth.permissions import QuayDeferredPermissionUser
from auth import scopes
from endpoints.api.discovery import swagger_route_data
from werkzeug.routing import BaseConverter
from functools import wraps
from config import getFrontendVisibleConfig
import features
logger = logging.getLogger(__name__)
@ -27,6 +30,29 @@ class RepoPathConverter(BaseConverter):
app.url_map.converters['repopath'] = RepoPathConverter
def route_show_if(value):
def decorator(f):
@wraps(f)
def decorated_function(*args, **kwargs):
if not value:
abort(404)
return f(*args, **kwargs)
return decorated_function
return decorator
def route_hide_if(value):
def decorator(f):
@wraps(f)
def decorated_function(*args, **kwargs):
if value:
abort(404)
return f(*args, **kwargs)
return decorated_function
return decorator
def get_route_data():
global route_data
@ -91,7 +117,14 @@ def random_string():
def render_page_template(name, **kwargs):
resp = make_response(render_template(name, route_data=json.dumps(get_route_data()),
cache_buster='foobarbaz', **kwargs))
feature_set=json.dumps(features.get_features()),
config_set=json.dumps(getFrontendVisibleConfig(app.config)),
mixpanel_key=app.config.get('MIXPANEL_KEY', ''),
is_debug=str(app.config.get('DEBUGGING', False)).lower(),
show_chat=features.OLARK_CHAT,
cache_buster=random_string(),
**kwargs))
resp.headers['X-FRAME-OPTIONS'] = 'DENY'
return resp
@ -107,7 +140,7 @@ def check_repository_usage(user_or_org, plan_found):
def start_build(repository, dockerfile_id, tags, build_name, subdir, manual,
trigger=None):
trigger=None, pull_robot_name=None):
host = urlparse.urlparse(request.url).netloc
repo_path = '%s/%s/%s' % (host, repository.namespace, repository.name)
@ -118,16 +151,18 @@ def start_build(repository, dockerfile_id, tags, build_name, subdir, manual,
job_config = {
'docker_tags': tags,
'repository': repo_path,
'build_subdir': subdir,
'build_subdir': subdir
}
build_request = model.create_repository_build(repository, token, job_config,
dockerfile_id, build_name,
trigger)
trigger, pull_robot_name = pull_robot_name)
dockerfile_build_queue.put(json.dumps({
dockerfile_build_queue.put([repository.namespace, repository.name], json.dumps({
'build_uuid': build_request.uuid,
'namespace': repository.namespace,
'repository': repository.name,
'pull_credentials': model.get_pull_credentials(pull_robot_name) if pull_robot_name else None
}), retries_remaining=1)
metadata = {

View file

@ -9,7 +9,7 @@ from collections import OrderedDict
from data import model
from data.model import oauth
from data.queue import webhook_queue
from app import mixpanel, app
from app import analytics, app
from auth.auth import process_auth
from auth.auth_context import get_authenticated_user, get_validated_token, get_validated_oauth_token
from util.names import parse_repository_name
@ -227,7 +227,7 @@ def create_repository(namespace, repository):
}
if get_validated_oauth_token():
mixpanel.track(username, 'push_repo', extra_params)
analytics.track(username, 'push_repo', extra_params)
oauth_token = get_validated_oauth_token()
metadata['oauth_token_id'] = oauth_token.id
@ -236,7 +236,7 @@ def create_repository(namespace, repository):
elif get_authenticated_user():
username = get_authenticated_user().username
mixpanel.track(username, 'push_repo', extra_params)
analytics.track(username, 'push_repo', extra_params)
metadata['username'] = username
# Mark that the user has started pushing the repo.
@ -250,7 +250,7 @@ def create_repository(namespace, repository):
event.publish_event_data('docker-cli', user_data)
elif get_validated_token():
mixpanel.track(get_validated_token().code, 'push_repo', extra_params)
analytics.track(get_validated_token().code, 'push_repo', extra_params)
metadata['token'] = get_validated_token().friendly_name
metadata['token_code'] = get_validated_token().code
@ -315,7 +315,7 @@ def update_images(namespace, repository):
'pushed_image_count': len(image_with_checksums),
'pruned_image_count': num_removed,
}
webhook_queue.put(json.dumps(webhook_data))
webhook_queue.put([namespace, repository], json.dumps(webhook_data))
return make_response('Updated', 204)
@ -374,7 +374,7 @@ def get_repository_images(namespace, repository):
'repository': '%s/%s' % (namespace, repository),
}
mixpanel.track(pull_username, 'pull_repo', extra_params)
analytics.track(pull_username, 'pull_repo', extra_params)
model.log_action('pull_repo', namespace,
performer=get_authenticated_user(),
ip=request.remote_addr, metadata=metadata,

View file

@ -9,7 +9,7 @@ from time import time
from data.queue import image_diff_queue
from app import app
from app import storage as store
from auth.auth import process_auth, extract_namespace_repo_from_session
from util import checksums, changes
from util.http import abort
@ -17,9 +17,9 @@ from auth.permissions import (ReadRepositoryPermission,
ModifyRepositoryPermission)
from data import model
registry = Blueprint('registry', __name__)
store = app.config['STORAGE']
logger = logging.getLogger(__name__)
@ -179,7 +179,7 @@ def put_image_layer(namespace, repository, image_id):
# The layer is ready for download, send a job to the work queue to
# process it.
logger.debug('Queing diffs job for image: %s' % image_id)
image_diff_queue.put(json.dumps({
image_diff_queue.put([namespace, repository, image_id], json.dumps({
'namespace': namespace,
'repository': repository,
'image_id': image_id,
@ -232,7 +232,7 @@ def put_image_checksum(namespace, repository, image_id):
# The layer is ready for download, send a job to the work queue to
# process it.
logger.debug('Queing diffs job for image: %s' % image_id)
image_diff_queue.put(json.dumps({
image_diff_queue.put([namespace, repository, image_id], json.dumps({
'namespace': namespace,
'repository': repository,
'image_id': image_id,

View file

@ -2,14 +2,14 @@ import logging
import io
import os.path
import tarfile
import base64
from github import Github, UnknownObjectException, GithubException
from tempfile import SpooledTemporaryFile
from app import app
from app import app, userfiles as user_files
user_files = app.config['USERFILES']
client = app.config['HTTPCLIENT']
@ -46,6 +46,19 @@ class BuildTrigger(object):
def __init__(self):
pass
def dockerfile_url(self, auth_token, config):
"""
Returns the URL at which the Dockerfile for the trigger can be found or None if none/not applicable.
"""
return None
def load_dockerfile_contents(self, auth_token, config):
"""
Loads the Dockerfile found for the trigger's config and returns them or None if none could
be found/loaded.
"""
return None
def list_build_sources(self, auth_token):
"""
Take the auth information for the specific trigger type and load the
@ -168,7 +181,6 @@ class GithubBuildTrigger(BuildTrigger):
return config
def list_build_sources(self, auth_token):
gh_client = self._get_client(auth_token)
usr = gh_client.get_user()
@ -219,6 +231,41 @@ class GithubBuildTrigger(BuildTrigger):
raise RepositoryReadException(message)
def dockerfile_url(self, auth_token, config):
source = config['build_source']
subdirectory = config.get('subdir', '')
path = subdirectory + '/Dockerfile' if subdirectory else 'Dockerfile'
gh_client = self._get_client(auth_token)
try:
repo = gh_client.get_repo(source)
master_branch = repo.master_branch or 'master'
return 'https://github.com/%s/blob/%s/%s' % (source, master_branch, path)
except GithubException as ge:
return None
def load_dockerfile_contents(self, auth_token, config):
gh_client = self._get_client(auth_token)
source = config['build_source']
subdirectory = config.get('subdir', '')
path = subdirectory + '/Dockerfile' if subdirectory else 'Dockerfile'
try:
repo = gh_client.get_repo(source)
file_info = repo.get_file_contents(path)
if file_info is None:
return None
content = file_info.content
if file_info.encoding == 'base64':
content = base64.b64decode(content)
return content
except GithubException as ge:
message = ge.data.get('message', 'Unable to read Dockerfile: %s' % source)
raise RepositoryReadException(message)
@staticmethod
def _prepare_build(config, repo, commit_sha, build_name, ref):
# Prepare the download and upload URLs

View file

@ -1,5 +1,4 @@
import logging
import stripe
import os
from flask import (abort, redirect, request, url_for, make_response, Response,
@ -9,17 +8,20 @@ from urlparse import urlparse
from data import model
from data.model.oauth import DatabaseAuthorizationProvider
from app import app
from app import app, billing as stripe
from auth.auth import require_session_login
from auth.permissions import AdministerOrganizationPermission
from util.invoice import renderInvoiceToPdf
from util.seo import render_snapshot
from util.cache import no_cache
from endpoints.common import common_login, render_page_template
from endpoints.common import common_login, render_page_template, route_show_if, route_hide_if
from endpoints.csrf import csrf_protect, generate_csrf_token
from util.names import parse_repository_name
from util.gravatar import compute_hash
from auth import scopes
import features
logger = logging.getLogger(__name__)
web = Blueprint('web', __name__)
@ -54,6 +56,7 @@ def snapshot(path = ''):
@web.route('/plans/')
@no_cache
@route_show_if(features.BILLING)
def plans():
return index('')
@ -82,6 +85,12 @@ def organizations():
def user():
return index('')
@web.route('/superuser/')
@no_cache
@route_show_if(features.SUPER_USERS)
def superuser():
return index('')
@web.route('/signin/')
@no_cache
@ -152,6 +161,8 @@ def privacy():
@web.route('/receipt', methods=['GET'])
@route_show_if(features.BILLING)
@require_session_login
def receipt():
if not current_user.is_authenticated():
abort(401)

View file

@ -1,11 +1,10 @@
import logging
import stripe
import json
from flask import request, make_response, Blueprint
from app import billing as stripe
from data import model
from data.queue import dockerfile_build_queue
from auth.auth import process_auth
from auth.permissions import ModifyRepositoryPermission
from util.invoice import renderInvoiceToHtml
@ -73,8 +72,10 @@ def build_trigger_webhook(namespace, repository, trigger_uuid):
# This was just a validation request, we don't need to build anything
return make_response('Okay')
pull_robot_name = model.get_pull_robot_name(trigger)
repo = model.get_repository(namespace, repository)
start_build(repo, dockerfile_id, tags, name, subdir, False, trigger)
start_build(repo, dockerfile_id, tags, name, subdir, False, trigger,
pull_robot_name=pull_robot_name)
return make_response('Okay')