Add configurable limits for number of builds allowed under a namespace

We also support that limit being increased automatically once a successful billing charge has gone through
This commit is contained in:
Joseph Schorr 2018-02-20 13:58:14 -05:00
parent 62971b7f20
commit 9a452ace11
10 changed files with 125 additions and 33 deletions

View file

@ -16,9 +16,6 @@ from util.morecollections import AttrDict
logger = logging.getLogger(__name__)
MAX_BUILD_QUEUE_RATE_ITEMS = app.config.get('MAX_BUILD_QUEUE_RATE_ITEMS', -1)
MAX_BUILD_QUEUE_RATE_SECS = app.config.get('MAX_BUILD_QUEUE_RATE_SECS', -1)
class MaximumBuildsQueuedException(Exception):
"""
@ -32,15 +29,14 @@ def start_build(repository, prepared_build, pull_robot_name=None):
if repository.kind.name != 'image':
raise Exception('Attempt to start a build for application repository %s' % repository.id)
if MAX_BUILD_QUEUE_RATE_ITEMS > 0 and MAX_BUILD_QUEUE_RATE_SECS > 0:
queue_item_canonical_name = [repository.namespace_user.username, repository.name]
now = datetime.utcnow()
available_min = now - timedelta(seconds=MAX_BUILD_QUEUE_RATE_SECS)
available_builds = dockerfile_build_queue.num_available_jobs_between(available_min,
now,
queue_item_canonical_name)
if available_builds >= MAX_BUILD_QUEUE_RATE_ITEMS:
raise MaximumBuildsQueuedException()
if repository.namespace_user.maximum_queued_builds_count is not None:
queue_item_canonical_name = [repository.namespace_user.username]
available_builds = dockerfile_build_queue.num_available_jobs(queue_item_canonical_name)
if available_builds >= repository.namespace_user.maximum_queued_builds_count:
logger.debug('Prevented queueing of build under namespace %s due to reaching max: %s',
repository.namespace_user.username,
repository.namespace_user.maximum_queued_builds_count)
raise MaximumBuildsQueuedException()
host = app.config['SERVER_HOSTNAME']
repo_path = '%s/%s/%s' % (host, repository.namespace_user.username, repository.name)

View file

@ -0,0 +1,31 @@
import pytest
from data import model
from endpoints.building import start_build, PreparedBuild, MaximumBuildsQueuedException
from test.fixtures import *
def test_maximum_builds(app):
# Change the maximum number of builds to 1.
user = model.user.create_user('foobar', 'password', 'foo@example.com')
user.maximum_queued_builds_count = 1
user.save()
repo = model.repository.create_repository('foobar', 'somerepo', user)
# Try to queue a build; should succeed.
prepared_build = PreparedBuild()
prepared_build.build_name = 'foo'
prepared_build.is_manual = True
prepared_build.dockerfile_id = 'foobar'
prepared_build.archive_url = 'someurl'
prepared_build.tags = ['latest']
prepared_build.subdirectory = '/'
prepared_build.context = '/'
prepared_build.metadata = {}
start_build(repo, prepared_build)
# Try to queue a second build; should fail.
with pytest.raises(MaximumBuildsQueuedException):
start_build(repo, prepared_build)

View file

@ -2,7 +2,7 @@ import logging
from flask import request, make_response, Blueprint
from app import billing as stripe
from app import billing as stripe, app
from data import model
from auth.decorators import process_auth
from auth.permissions import ModifyRepositoryPermission
@ -26,22 +26,29 @@ def stripe_webhook():
logger.debug('Stripe webhook call: %s', request_data)
customer_id = request_data.get('data', {}).get('object', {}).get('customer', None)
user = model.user.get_user_or_org_by_customer_id(customer_id) if customer_id else None
namespace = model.user.get_user_or_org_by_customer_id(customer_id) if customer_id else None
event_type = request_data['type'] if 'type' in request_data else None
if event_type == 'charge.succeeded':
invoice_id = request_data['data']['object']['invoice']
if user and user.invoice_email:
# Lookup the invoice.
invoice = stripe.Invoice.retrieve(invoice_id)
if invoice:
invoice_html = renderInvoiceToHtml(invoice, user)
send_invoice_email(user.invoice_email_address or user.email, invoice_html)
namespace = model.user.get_user_or_org_by_customer_id(customer_id) if customer_id else None
if namespace:
# Increase the namespace's build allowance, since we had a successful charge.
build_maximum = app.config.get('BILLED_NAMESPACE_MAXIMUM_BUILD_COUNT')
if build_maximum is not None:
model.user.increase_maximum_build_count(namespace, build_maximum)
if namespace.invoice_email:
# Lookup the invoice.
invoice = stripe.Invoice.retrieve(invoice_id)
if invoice:
invoice_html = renderInvoiceToHtml(invoice, namespace)
send_invoice_email(namespace.invoice_email_address or namespace.email, invoice_html)
elif event_type.startswith('customer.subscription.'):
cust_email = user.email if user is not None else 'unknown@domain.com'
quay_username = user.username if user is not None else 'unknown'
cust_email = namespace.email if namespace is not None else 'unknown@domain.com'
quay_username = namespace.username if namespace is not None else 'unknown'
change_type = ''
if event_type.endswith('.deleted'):
@ -63,8 +70,8 @@ def stripe_webhook():
send_subscription_change(change_type, customer_id, cust_email, quay_username)
elif event_type == 'invoice.payment_failed':
if user:
send_payment_failed(user.email, user.username)
if namespace:
send_payment_failed(namespace.email, namespace.username)
return make_response('Okay')