Merge branch 'master' into comewithmeifyouwanttowork
This commit is contained in:
commit
3b72b26836
62 changed files with 923 additions and 714 deletions
|
@ -1,11 +1,11 @@
|
|||
conf/stack
|
||||
screenshots
|
||||
tools
|
||||
test/data/registry
|
||||
venv
|
||||
.git
|
||||
.gitignore
|
||||
Bobfile
|
||||
README.md
|
||||
license.py
|
||||
requirements-nover.txt
|
||||
run-local.sh
|
||||
run-local.sh
|
|
@ -4,10 +4,10 @@ ENV DEBIAN_FRONTEND noninteractive
|
|||
ENV HOME /root
|
||||
|
||||
# Install the dependencies.
|
||||
RUN apt-get update # 06AUG2014
|
||||
RUN apt-get update # 21AUG2014
|
||||
|
||||
# New ubuntu packages should be added as their own apt-get install lines below the existing install commands
|
||||
RUN apt-get install -y git python-virtualenv python-dev libjpeg8 libjpeg62-dev libevent-dev gdebi-core g++ libmagic1 phantomjs nodejs npm libldap2-dev libsasl2-dev
|
||||
RUN apt-get install -y git python-virtualenv python-dev libjpeg8 libjpeg62-dev libevent-dev gdebi-core g++ libmagic1 phantomjs nodejs npm libldap2-dev libsasl2-dev libpq-dev
|
||||
|
||||
# Build the python dependencies
|
||||
ADD requirements.txt requirements.txt
|
||||
|
|
|
@ -4,10 +4,10 @@ ENV DEBIAN_FRONTEND noninteractive
|
|||
ENV HOME /root
|
||||
|
||||
# Install the dependencies.
|
||||
RUN apt-get update # 06AUG2014
|
||||
RUN apt-get update # 21AUG2014
|
||||
|
||||
# New ubuntu packages should be added as their own apt-get install lines below the existing install commands
|
||||
RUN apt-get install -y git python-virtualenv python-dev libjpeg8 libjpeg62-dev libevent-dev gdebi-core g++ libmagic1 phantomjs nodejs npm libldap2-dev libsasl2-dev
|
||||
RUN apt-get install -y git python-virtualenv python-dev libjpeg8 libjpeg62-dev libevent-dev gdebi-core g++ libmagic1 phantomjs nodejs npm libldap2-dev libsasl2-dev libpq-dev
|
||||
|
||||
# Build the python dependencies
|
||||
ADD requirements.txt requirements.txt
|
||||
|
@ -30,6 +30,7 @@ RUN cd grunt && npm install
|
|||
RUN cd grunt && grunt
|
||||
|
||||
ADD conf/init/svlogd_config /svlogd_config
|
||||
ADD conf/init/doupdatelimits.sh /etc/my_init.d/
|
||||
ADD conf/init/preplogsdir.sh /etc/my_init.d/
|
||||
ADD conf/init/runmigration.sh /etc/my_init.d/
|
||||
|
||||
|
@ -38,9 +39,6 @@ ADD conf/init/nginx /etc/service/nginx
|
|||
ADD conf/init/diffsworker /etc/service/diffsworker
|
||||
ADD conf/init/notificationworker /etc/service/notificationworker
|
||||
|
||||
# TODO: Remove this after the prod CL push
|
||||
ADD conf/init/webhookworker /etc/service/webhookworker
|
||||
|
||||
# Download any external libs.
|
||||
RUN mkdir static/fonts static/ldn
|
||||
RUN venv/bin/python -m external_libraries
|
||||
|
|
52
app.py
52
app.py
|
@ -1,8 +1,9 @@
|
|||
import logging
|
||||
import os
|
||||
import json
|
||||
import yaml
|
||||
|
||||
from flask import Flask
|
||||
from flask import Flask as BaseFlask, Config as BaseConfig
|
||||
from flask.ext.principal import Principal
|
||||
from flask.ext.login import LoginManager
|
||||
from flask.ext.mail import Mail
|
||||
|
@ -21,11 +22,37 @@ from data.billing import Billing
|
|||
from data.buildlogs import BuildLogs
|
||||
from data.queue import WorkQueue
|
||||
from data.userevent import UserEventsBuilderModule
|
||||
from license import load_license
|
||||
from datetime import datetime
|
||||
|
||||
|
||||
OVERRIDE_CONFIG_FILENAME = 'conf/stack/config.py'
|
||||
class Config(BaseConfig):
|
||||
""" Flask config enhanced with a `from_yamlfile` method """
|
||||
|
||||
def from_yamlfile(self, config_file):
|
||||
with open(config_file) as f:
|
||||
c = yaml.load(f)
|
||||
if not c:
|
||||
logger.debug('Empty YAML config file')
|
||||
return
|
||||
|
||||
if isinstance(c, str):
|
||||
raise Exception('Invalid YAML config file: ' + str(c))
|
||||
|
||||
for key in c.iterkeys():
|
||||
if key.isupper():
|
||||
self[key] = c[key]
|
||||
|
||||
class Flask(BaseFlask):
|
||||
""" Extends the Flask class to implement our custom Config class. """
|
||||
|
||||
def make_config(self, instance_relative=False):
|
||||
root_path = self.instance_path if instance_relative else self.root_path
|
||||
return Config(root_path, self.default_config)
|
||||
|
||||
|
||||
OVERRIDE_CONFIG_YAML_FILENAME = 'conf/stack/config.yaml'
|
||||
OVERRIDE_CONFIG_PY_FILENAME = 'conf/stack/config.py'
|
||||
|
||||
OVERRIDE_CONFIG_KEY = 'QUAY_OVERRIDE_CONFIG'
|
||||
LICENSE_FILENAME = 'conf/stack/license.enc'
|
||||
|
||||
|
@ -43,22 +70,17 @@ else:
|
|||
logger.debug('Loading default config.')
|
||||
app.config.from_object(DefaultConfig())
|
||||
|
||||
if os.path.exists(OVERRIDE_CONFIG_FILENAME):
|
||||
logger.debug('Applying config file: %s', OVERRIDE_CONFIG_FILENAME)
|
||||
app.config.from_pyfile(OVERRIDE_CONFIG_FILENAME)
|
||||
if os.path.exists(OVERRIDE_CONFIG_PY_FILENAME):
|
||||
logger.debug('Applying config file: %s', OVERRIDE_CONFIG_PY_FILENAME)
|
||||
app.config.from_pyfile(OVERRIDE_CONFIG_PY_FILENAME)
|
||||
|
||||
if os.path.exists(OVERRIDE_CONFIG_YAML_FILENAME):
|
||||
logger.debug('Applying config file: %s', OVERRIDE_CONFIG_YAML_FILENAME)
|
||||
app.config.from_yamlfile(OVERRIDE_CONFIG_YAML_FILENAME)
|
||||
|
||||
environ_config = json.loads(os.environ.get(OVERRIDE_CONFIG_KEY, '{}'))
|
||||
app.config.update(environ_config)
|
||||
|
||||
logger.debug('Applying license config from: %s', LICENSE_FILENAME)
|
||||
try:
|
||||
app.config.update(load_license(LICENSE_FILENAME))
|
||||
except IOError:
|
||||
raise RuntimeError('License file %s not found; please check your configuration' % LICENSE_FILENAME)
|
||||
|
||||
if app.config.get('LICENSE_EXPIRATION', datetime.min) < datetime.utcnow():
|
||||
raise RuntimeError('License has expired, please contact support@quay.io')
|
||||
|
||||
features.import_features(app.config)
|
||||
|
||||
Principal(app, use_sessions=False)
|
||||
|
|
5
conf/init/doupdatelimits.sh
Executable file
5
conf/init/doupdatelimits.sh
Executable file
|
@ -0,0 +1,5 @@
|
|||
#! /bin/bash
|
||||
set -e
|
||||
|
||||
# Update the connection limit
|
||||
sysctl -w net.core.somaxconn=1024
|
|
@ -1,2 +0,0 @@
|
|||
#!/bin/sh
|
||||
exec svlogd -t /var/log/webhookworker/
|
|
@ -1,8 +0,0 @@
|
|||
#! /bin/bash
|
||||
|
||||
echo 'Starting webhook worker'
|
||||
|
||||
cd /
|
||||
venv/bin/python -m workers.webhookworker
|
||||
|
||||
echo 'Webhook worker exited'
|
|
@ -1,4 +1,4 @@
|
|||
client_max_body_size 8G;
|
||||
client_max_body_size 20G;
|
||||
client_body_temp_path /var/log/nginx/client_body 1 2;
|
||||
server_name _;
|
||||
|
||||
|
|
|
@ -153,6 +153,9 @@ class DefaultConfig(object):
|
|||
# Feature Flag: Whether to support GitHub build triggers.
|
||||
FEATURE_GITHUB_BUILD = False
|
||||
|
||||
# Feature Flag: Dockerfile build support.
|
||||
FEATURE_BUILD_SUPPORT = True
|
||||
|
||||
DISTRIBUTED_STORAGE_CONFIG = {
|
||||
'local_eu': ['LocalStorage', {'storage_path': 'test/data/registry/eu'}],
|
||||
'local_us': ['LocalStorage', {'storage_path': 'test/data/registry/us'}],
|
||||
|
|
|
@ -17,6 +17,8 @@ SCHEME_DRIVERS = {
|
|||
'mysql': MySQLDatabase,
|
||||
'mysql+pymysql': MySQLDatabase,
|
||||
'sqlite': SqliteDatabase,
|
||||
'postgresql': PostgresqlDatabase,
|
||||
'postgresql+psycopg2': PostgresqlDatabase,
|
||||
}
|
||||
|
||||
db = Proxy()
|
||||
|
@ -32,7 +34,7 @@ def _db_from_url(url, db_kwargs):
|
|||
if parsed_url.username:
|
||||
db_kwargs['user'] = parsed_url.username
|
||||
if parsed_url.password:
|
||||
db_kwargs['passwd'] = parsed_url.password
|
||||
db_kwargs['password'] = parsed_url.password
|
||||
|
||||
return SCHEME_DRIVERS[parsed_url.drivername](parsed_url.database, **db_kwargs)
|
||||
|
||||
|
|
|
@ -0,0 +1,37 @@
|
|||
"""add log kind for regenerating robot tokens
|
||||
|
||||
Revision ID: 43e943c0639f
|
||||
Revises: 82297d834ad
|
||||
Create Date: 2014-08-25 17:14:42.784518
|
||||
|
||||
"""
|
||||
|
||||
# revision identifiers, used by Alembic.
|
||||
revision = '43e943c0639f'
|
||||
down_revision = '82297d834ad'
|
||||
|
||||
from alembic import op
|
||||
import sqlalchemy as sa
|
||||
from sqlalchemy.dialects import mysql
|
||||
from data.model.sqlalchemybridge import gen_sqlalchemy_metadata
|
||||
from data.database import all_models
|
||||
|
||||
|
||||
def upgrade():
|
||||
schema = gen_sqlalchemy_metadata(all_models)
|
||||
|
||||
op.bulk_insert(schema.tables['logentrykind'],
|
||||
[
|
||||
{'id': 41, 'name':'regenerate_robot_token'},
|
||||
])
|
||||
|
||||
|
||||
def downgrade():
|
||||
schema = gen_sqlalchemy_metadata(all_models)
|
||||
|
||||
logentrykind = schema.tables['logentrykind']
|
||||
op.execute(
|
||||
(logentrykind.delete()
|
||||
.where(logentrykind.c.name == op.inline_literal('regenerate_robot_token')))
|
||||
|
||||
)
|
|
@ -20,12 +20,12 @@ def get_id(query):
|
|||
|
||||
def upgrade():
|
||||
conn = op.get_bind()
|
||||
event_id = get_id('Select id From externalnotificationevent Where name="repo_push" Limit 1')
|
||||
method_id = get_id('Select id From externalnotificationmethod Where name="webhook" Limit 1')
|
||||
event_id = get_id('Select id From externalnotificationevent Where name=\'repo_push\' Limit 1')
|
||||
method_id = get_id('Select id From externalnotificationmethod Where name=\'webhook\' Limit 1')
|
||||
conn.execute('Insert Into repositorynotification (uuid, repository_id, event_id, method_id, config_json) Select public_id, repository_id, %s, %s, parameters FROM webhook' % (event_id, method_id))
|
||||
|
||||
def downgrade():
|
||||
conn = op.get_bind()
|
||||
event_id = get_id('Select id From externalnotificationevent Where name="repo_push" Limit 1')
|
||||
method_id = get_id('Select id From externalnotificationmethod Where name="webhook" Limit 1')
|
||||
event_id = get_id('Select id From externalnotificationevent Where name=\'repo_push\' Limit 1')
|
||||
method_id = get_id('Select id From externalnotificationmethod Where name=\'webhook\' Limit 1')
|
||||
conn.execute('Insert Into webhook (public_id, repository_id, parameters) Select uuid, repository_id, config_json FROM repositorynotification Where event_id=%s And method_id=%s' % (event_id, method_id))
|
||||
|
|
|
@ -203,7 +203,7 @@ def upgrade():
|
|||
sa.Column('id', sa.Integer(), nullable=False),
|
||||
sa.Column('user_id', sa.Integer(), nullable=False),
|
||||
sa.Column('service_id', sa.Integer(), nullable=False),
|
||||
sa.Column('service_ident', sa.String(length=255, collation='utf8_general_ci'), nullable=False),
|
||||
sa.Column('service_ident', sa.String(length=255), nullable=False),
|
||||
sa.ForeignKeyConstraint(['service_id'], ['loginservice.id'], ),
|
||||
sa.ForeignKeyConstraint(['user_id'], ['user.id'], ),
|
||||
sa.PrimaryKeyConstraint('id')
|
||||
|
@ -375,7 +375,7 @@ def upgrade():
|
|||
sa.Column('command', sa.Text(), nullable=True),
|
||||
sa.Column('repository_id', sa.Integer(), nullable=False),
|
||||
sa.Column('image_size', sa.BigInteger(), nullable=True),
|
||||
sa.Column('ancestors', sa.String(length=60535, collation='latin1_swedish_ci'), nullable=True),
|
||||
sa.Column('ancestors', sa.String(length=60535), nullable=True),
|
||||
sa.Column('storage_id', sa.Integer(), nullable=True),
|
||||
sa.ForeignKeyConstraint(['repository_id'], ['repository.id'], ),
|
||||
sa.ForeignKeyConstraint(['storage_id'], ['imagestorage.id'], ),
|
||||
|
|
|
@ -76,8 +76,7 @@ class UserAlreadyInTeam(DataModelException):
|
|||
|
||||
|
||||
def is_create_user_allowed():
|
||||
return get_active_user_count() < config.app_config['LICENSE_USER_LIMIT']
|
||||
|
||||
return True
|
||||
|
||||
def create_user(username, password, email):
|
||||
""" Creates a regular user, if allowed. """
|
||||
|
@ -188,6 +187,19 @@ def create_robot(robot_shortname, parent):
|
|||
except Exception as ex:
|
||||
raise DataModelException(ex.message)
|
||||
|
||||
def get_robot(robot_shortname, parent):
|
||||
robot_username = format_robot_username(parent.username, robot_shortname)
|
||||
robot = lookup_robot(robot_username)
|
||||
|
||||
if not robot:
|
||||
msg = ('Could not find robot with username: %s' %
|
||||
robot_username)
|
||||
raise InvalidRobotException(msg)
|
||||
|
||||
service = LoginService.get(name='quayrobot')
|
||||
login = FederatedLogin.get(FederatedLogin.user == robot, FederatedLogin.service == service)
|
||||
|
||||
return robot, login.service_ident
|
||||
|
||||
def lookup_robot(robot_username):
|
||||
joined = User.select().join(FederatedLogin).join(LoginService)
|
||||
|
@ -198,7 +210,6 @@ def lookup_robot(robot_username):
|
|||
|
||||
return found[0]
|
||||
|
||||
|
||||
def verify_robot(robot_username, password):
|
||||
joined = User.select().join(FederatedLogin).join(LoginService)
|
||||
found = list(joined.where(FederatedLogin.service_ident == password,
|
||||
|
@ -211,6 +222,25 @@ def verify_robot(robot_username, password):
|
|||
|
||||
return found[0]
|
||||
|
||||
def regenerate_robot_token(robot_shortname, parent):
|
||||
robot_username = format_robot_username(parent.username, robot_shortname)
|
||||
|
||||
robot = lookup_robot(robot_username)
|
||||
if not robot:
|
||||
raise InvalidRobotException('Could not find robot with username: %s' %
|
||||
robot_username)
|
||||
|
||||
password = random_string_generator(length=64)()
|
||||
robot.email = password
|
||||
|
||||
service = LoginService.get(name='quayrobot')
|
||||
login = FederatedLogin.get(FederatedLogin.user == robot, FederatedLogin.service == service)
|
||||
login.service_ident = password
|
||||
|
||||
login.save()
|
||||
robot.save()
|
||||
|
||||
return robot, password
|
||||
|
||||
def delete_robot(robot_username):
|
||||
try:
|
||||
|
@ -872,6 +902,34 @@ def get_all_repo_users(namespace_name, repository_name):
|
|||
Repository.name == repository_name)
|
||||
|
||||
|
||||
def get_all_repo_users_transitive_via_teams(namespace_name, repository_name):
|
||||
select = User.select().distinct()
|
||||
with_team_member = select.join(TeamMember)
|
||||
with_team = with_team_member.join(Team)
|
||||
with_perm = with_team.join(RepositoryPermission)
|
||||
with_repo = with_perm.join(Repository)
|
||||
return with_repo.where(Repository.namespace == namespace_name,
|
||||
Repository.name == repository_name)
|
||||
|
||||
|
||||
def get_all_repo_users_transitive(namespace_name, repository_name):
|
||||
# Load the users found via teams and directly via permissions.
|
||||
via_teams = get_all_repo_users_transitive_via_teams(namespace_name, repository_name)
|
||||
directly = [perm.user for perm in get_all_repo_users(namespace_name, repository_name)]
|
||||
|
||||
# Filter duplicates.
|
||||
user_set = set()
|
||||
|
||||
def check_add(u):
|
||||
if u.username in user_set:
|
||||
return False
|
||||
|
||||
user_set.add(u.username)
|
||||
return True
|
||||
|
||||
return [user for user in list(directly) + list(via_teams) if check_add(user)]
|
||||
|
||||
|
||||
def get_repository_for_resource(resource_key):
|
||||
try:
|
||||
return (Repository
|
||||
|
@ -1706,19 +1764,20 @@ def create_notification(kind_name, target, metadata={}):
|
|||
|
||||
def create_unique_notification(kind_name, target, metadata={}):
|
||||
with config.app_config['DB_TRANSACTION_FACTORY'](db):
|
||||
if list_notifications(target, kind_name).count() == 0:
|
||||
if list_notifications(target, kind_name, limit=1).count() == 0:
|
||||
create_notification(kind_name, target, metadata)
|
||||
|
||||
|
||||
def lookup_notification(user, uuid):
|
||||
results = list(list_notifications(user, id_filter=uuid, include_dismissed=True))
|
||||
results = list(list_notifications(user, id_filter=uuid, include_dismissed=True, limit=1))
|
||||
if not results:
|
||||
return None
|
||||
|
||||
return results[0]
|
||||
|
||||
|
||||
def list_notifications(user, kind_name=None, id_filter=None, include_dismissed=False):
|
||||
def list_notifications(user, kind_name=None, id_filter=None, include_dismissed=False,
|
||||
page=None, limit=None):
|
||||
Org = User.alias()
|
||||
AdminTeam = Team.alias()
|
||||
AdminTeamMember = TeamMember.alias()
|
||||
|
@ -1756,6 +1815,11 @@ def list_notifications(user, kind_name=None, id_filter=None, include_dismissed=F
|
|||
.switch(Notification)
|
||||
.where(Notification.uuid == id_filter))
|
||||
|
||||
if page:
|
||||
query = query.paginate(page, limit)
|
||||
elif limit:
|
||||
query = query.limit(limit)
|
||||
|
||||
return query
|
||||
|
||||
|
||||
|
|
|
@ -4,7 +4,7 @@ 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, show_if, hide_if)
|
||||
require_user_admin, show_if, hide_if, abort)
|
||||
from endpoints.api.subscribe import subscribe, subscription_view
|
||||
from auth.permissions import AdministerOrganizationPermission
|
||||
from auth.auth_context import get_authenticated_user
|
||||
|
@ -23,7 +23,11 @@ def get_card(user):
|
|||
}
|
||||
|
||||
if user.stripe_id:
|
||||
cus = billing.Customer.retrieve(user.stripe_id)
|
||||
try:
|
||||
cus = billing.Customer.retrieve(user.stripe_id)
|
||||
except stripe.APIConnectionError as e:
|
||||
abort(503, message='Cannot contact Stripe')
|
||||
|
||||
if cus and cus.default_card:
|
||||
# Find the default card.
|
||||
default_card = None
|
||||
|
@ -46,7 +50,11 @@ def get_card(user):
|
|||
|
||||
def set_card(user, token):
|
||||
if user.stripe_id:
|
||||
cus = billing.Customer.retrieve(user.stripe_id)
|
||||
try:
|
||||
cus = billing.Customer.retrieve(user.stripe_id)
|
||||
except stripe.APIConnectionError as e:
|
||||
abort(503, message='Cannot contact Stripe')
|
||||
|
||||
if cus:
|
||||
try:
|
||||
cus.card = token
|
||||
|
@ -55,6 +63,8 @@ def set_card(user, token):
|
|||
return carderror_response(exc)
|
||||
except stripe.InvalidRequestError as exc:
|
||||
return carderror_response(exc)
|
||||
except stripe.APIConnectionError as e:
|
||||
return carderror_response(e)
|
||||
|
||||
return get_card(user)
|
||||
|
||||
|
@ -75,7 +85,11 @@ def get_invoices(customer_id):
|
|||
'plan': i.lines.data[0].plan.id if i.lines.data[0].plan else None
|
||||
}
|
||||
|
||||
invoices = billing.Invoice.all(customer=customer_id, count=12)
|
||||
try:
|
||||
invoices = billing.Invoice.all(customer=customer_id, count=12)
|
||||
except stripe.APIConnectionError as e:
|
||||
abort(503, message='Cannot contact Stripe')
|
||||
|
||||
return {
|
||||
'invoices': [invoice_view(i) for i in invoices.data]
|
||||
}
|
||||
|
@ -228,7 +242,10 @@ class UserPlan(ApiResource):
|
|||
private_repos = model.get_private_repo_count(user.username)
|
||||
|
||||
if user.stripe_id:
|
||||
cus = billing.Customer.retrieve(user.stripe_id)
|
||||
try:
|
||||
cus = billing.Customer.retrieve(user.stripe_id)
|
||||
except stripe.APIConnectionError as e:
|
||||
abort(503, message='Cannot contact Stripe')
|
||||
|
||||
if cus.subscription:
|
||||
return subscription_view(cus.subscription, private_repos)
|
||||
|
@ -291,7 +308,10 @@ class OrganizationPlan(ApiResource):
|
|||
private_repos = model.get_private_repo_count(orgname)
|
||||
organization = model.get_organization(orgname)
|
||||
if organization.stripe_id:
|
||||
cus = billing.Customer.retrieve(organization.stripe_id)
|
||||
try:
|
||||
cus = billing.Customer.retrieve(organization.stripe_id)
|
||||
except stripe.APIConnectionError as e:
|
||||
abort(503, message='Cannot contact Stripe')
|
||||
|
||||
if cus.subscription:
|
||||
return subscription_view(cus.subscription, private_repos)
|
||||
|
|
|
@ -35,6 +35,14 @@ class UserRobotList(ApiResource):
|
|||
@internal_only
|
||||
class UserRobot(ApiResource):
|
||||
""" Resource for managing a user's robots. """
|
||||
@require_user_admin
|
||||
@nickname('getUserRobot')
|
||||
def get(self, robot_shortname):
|
||||
""" Returns the user's robot with the specified name. """
|
||||
parent = get_authenticated_user()
|
||||
robot, password = model.get_robot(robot_shortname, parent)
|
||||
return robot_view(robot.username, password)
|
||||
|
||||
@require_user_admin
|
||||
@nickname('createUserRobot')
|
||||
def put(self, robot_shortname):
|
||||
|
@ -79,6 +87,18 @@ class OrgRobotList(ApiResource):
|
|||
@related_user_resource(UserRobot)
|
||||
class OrgRobot(ApiResource):
|
||||
""" Resource for managing an organization's robots. """
|
||||
@require_scope(scopes.ORG_ADMIN)
|
||||
@nickname('getOrgRobot')
|
||||
def get(self, orgname, robot_shortname):
|
||||
""" Returns the organization's robot with the specified name. """
|
||||
permission = AdministerOrganizationPermission(orgname)
|
||||
if permission.can():
|
||||
parent = model.get_organization(orgname)
|
||||
robot, password = model.get_robot(robot_shortname, parent)
|
||||
return robot_view(robot.username, password)
|
||||
|
||||
raise Unauthorized()
|
||||
|
||||
@require_scope(scopes.ORG_ADMIN)
|
||||
@nickname('createOrgRobot')
|
||||
def put(self, orgname, robot_shortname):
|
||||
|
@ -103,3 +123,38 @@ class OrgRobot(ApiResource):
|
|||
return 'Deleted', 204
|
||||
|
||||
raise Unauthorized()
|
||||
|
||||
|
||||
@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
|
||||
@nickname('regenerateUserRobotToken')
|
||||
def post(self, robot_shortname):
|
||||
""" Regenerates the token for a user's robot. """
|
||||
parent = get_authenticated_user()
|
||||
robot, password = model.regenerate_robot_token(robot_shortname, parent)
|
||||
log_action('regenerate_robot_token', parent.username, {'robot': robot_shortname})
|
||||
return robot_view(robot.username, password)
|
||||
|
||||
|
||||
@resource('/v1/organization/<orgname>/robots/<robot_shortname>/regenerate')
|
||||
@path_param('orgname', 'The name of the organization')
|
||||
@path_param('robot_shortname', 'The short name for the robot, without any user or organization prefix')
|
||||
@related_user_resource(RegenerateUserRobot)
|
||||
class RegenerateOrgRobot(ApiResource):
|
||||
""" Resource for regenerate an organization's robot's token. """
|
||||
@require_scope(scopes.ORG_ADMIN)
|
||||
@nickname('regenerateOrgRobotToken')
|
||||
def post(self, orgname, robot_shortname):
|
||||
""" Regenerates the token for an organization robot. """
|
||||
permission = AdministerOrganizationPermission(orgname)
|
||||
if permission.can():
|
||||
parent = model.get_organization(orgname)
|
||||
robot, password = model.regenerate_robot_token(robot_shortname, parent)
|
||||
log_action('regenerate_robot_token', orgname, {'robot': robot_shortname})
|
||||
return robot_view(robot.username, password)
|
||||
|
||||
raise Unauthorized()
|
||||
|
|
|
@ -15,6 +15,9 @@ logger = logging.getLogger(__name__)
|
|||
def carderror_response(exc):
|
||||
return {'carderror': exc.message}, 402
|
||||
|
||||
def connection_response(exc):
|
||||
return {'message': 'Could not contact Stripe. Please try again.'}, 503
|
||||
|
||||
|
||||
def subscription_view(stripe_subscription, used_repos):
|
||||
view = {
|
||||
|
@ -74,19 +77,29 @@ def subscribe(user, plan, token, require_business_plan):
|
|||
log_action('account_change_plan', user.username, {'plan': plan})
|
||||
except stripe.CardError as e:
|
||||
return carderror_response(e)
|
||||
except stripe.APIConnectionError as e:
|
||||
return connection_response(e)
|
||||
|
||||
response_json = subscription_view(cus.subscription, private_repos)
|
||||
status_code = 201
|
||||
|
||||
else:
|
||||
# Change the plan
|
||||
cus = billing.Customer.retrieve(user.stripe_id)
|
||||
try:
|
||||
cus = billing.Customer.retrieve(user.stripe_id)
|
||||
except stripe.APIConnectionError as e:
|
||||
return connection_response(e)
|
||||
|
||||
if plan_found['price'] == 0:
|
||||
if cus.subscription is not None:
|
||||
# We only have to cancel the subscription if they actually have one
|
||||
cus.cancel_subscription()
|
||||
cus.save()
|
||||
try:
|
||||
cus.cancel_subscription()
|
||||
cus.save()
|
||||
except stripe.APIConnectionError as e:
|
||||
return connection_response(e)
|
||||
|
||||
|
||||
check_repository_usage(user, plan_found)
|
||||
log_action('account_change_plan', user.username, {'plan': plan})
|
||||
|
||||
|
@ -101,6 +114,8 @@ def subscribe(user, plan, token, require_business_plan):
|
|||
cus.save()
|
||||
except stripe.CardError as e:
|
||||
return carderror_response(e)
|
||||
except stripe.APIConnectionError as e:
|
||||
return connection_response(e)
|
||||
|
||||
response_json = subscription_view(cus.subscription, private_repos)
|
||||
check_repository_usage(user, plan_found)
|
||||
|
|
|
@ -42,24 +42,6 @@ class SuperUserLogs(ApiResource):
|
|||
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_USER_LIMIT', 0)
|
||||
}
|
||||
|
||||
abort(403)
|
||||
|
||||
|
||||
def user_view(user):
|
||||
return {
|
||||
'username': user.username,
|
||||
|
|
|
@ -350,8 +350,8 @@ class BuildTriggerAnalyze(RepositoryParamResource):
|
|||
(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)]
|
||||
repo_users = list(model.get_all_repo_users_transitive(base_namespace, base_repository))
|
||||
read_robots = [robot_view(user) for user in repo_users if is_valid_robot(user)]
|
||||
|
||||
return {
|
||||
'namespace': base_namespace,
|
||||
|
|
|
@ -7,8 +7,9 @@ from flask.ext.principal import identity_changed, AnonymousIdentity
|
|||
|
||||
from app import app, billing as stripe, authentication
|
||||
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, hide_if, show_if, license_error)
|
||||
log_action, internal_only, NotFound, require_user_admin, parse_args,
|
||||
query_param, InvalidToken, require_scope, format_date, hide_if, show_if,
|
||||
license_error)
|
||||
from endpoints.api.subscribe import subscribe
|
||||
from endpoints.common import common_login
|
||||
from data import model
|
||||
|
@ -403,11 +404,24 @@ class Recovery(ApiResource):
|
|||
@internal_only
|
||||
class UserNotificationList(ApiResource):
|
||||
@require_user_admin
|
||||
@parse_args
|
||||
@query_param('page', 'Offset page number. (int)', type=int, default=0)
|
||||
@query_param('limit', 'Limit on the number of results (int)', type=int, default=5)
|
||||
@nickname('listUserNotifications')
|
||||
def get(self):
|
||||
notifications = model.list_notifications(get_authenticated_user())
|
||||
def get(self, args):
|
||||
page = args['page']
|
||||
limit = args['limit']
|
||||
|
||||
notifications = list(model.list_notifications(get_authenticated_user(), page=page, limit=limit + 1))
|
||||
has_more = False
|
||||
|
||||
if len(notifications) > limit:
|
||||
has_more = True
|
||||
notifications = notifications[0:limit]
|
||||
|
||||
return {
|
||||
'notifications': [notification_view(notification) for notification in notifications]
|
||||
'notifications': [notification_view(notification) for notification in notifications],
|
||||
'additional': has_more
|
||||
}
|
||||
|
||||
|
||||
|
|
|
@ -413,8 +413,39 @@ def put_repository_auth(namespace, repository):
|
|||
|
||||
|
||||
@index.route('/search', methods=['GET'])
|
||||
@process_auth
|
||||
def get_search():
|
||||
abort(501, 'Not Implemented', issue='not-implemented')
|
||||
def result_view(repo):
|
||||
return {
|
||||
"name": repo.namespace + '/' + repo.name,
|
||||
"description": repo.description
|
||||
}
|
||||
|
||||
query = request.args.get('q')
|
||||
|
||||
username = None
|
||||
user = get_authenticated_user()
|
||||
if user is not None:
|
||||
username = user.username
|
||||
|
||||
if query:
|
||||
matching = model.get_matching_repositories(query, username)
|
||||
else:
|
||||
matching = []
|
||||
|
||||
results = [result_view(repo) for repo in matching
|
||||
if (repo.visibility.name == 'public' or
|
||||
ReadRepositoryPermission(repo.namespace, repo.name).can())]
|
||||
|
||||
data = {
|
||||
"query": query,
|
||||
"num_results": len(results),
|
||||
"results" : results
|
||||
}
|
||||
|
||||
resp = make_response(json.dumps(data), 200)
|
||||
resp.mimetype = 'application/json'
|
||||
return resp
|
||||
|
||||
|
||||
@index.route('/_ping')
|
||||
|
|
|
@ -184,6 +184,8 @@ class BuildFailureEvent(NotificationEvent):
|
|||
return 'build_failure'
|
||||
|
||||
def get_sample_data(self, repository):
|
||||
build_uuid = 'fake-build-id'
|
||||
|
||||
return build_event_data(repository, {
|
||||
'build_id': build_uuid,
|
||||
'build_name': 'some-fake-build',
|
||||
|
|
|
@ -370,6 +370,7 @@ def generate_ancestry(image_id, uuid, locations, parent_id=None, parent_uuid=Non
|
|||
if not parent_id:
|
||||
store.put_content(locations, store.image_ancestry_path(uuid), json.dumps([image_id]))
|
||||
return
|
||||
|
||||
data = store.get_content(parent_locations, store.image_ancestry_path(parent_uuid))
|
||||
data = json.loads(data)
|
||||
data.insert(0, image_id)
|
||||
|
@ -470,8 +471,13 @@ def put_image_json(namespace, repository, image_id):
|
|||
store.put_content(repo_image.storage.locations, json_path, request.data)
|
||||
|
||||
profile.debug('Generating image ancestry')
|
||||
generate_ancestry(image_id, uuid, repo_image.storage.locations, parent_id, parent_uuid,
|
||||
parent_locations)
|
||||
|
||||
try:
|
||||
generate_ancestry(image_id, uuid, repo_image.storage.locations, parent_id, parent_uuid,
|
||||
parent_locations)
|
||||
except IOError as ioe:
|
||||
profile.debug('Error when generating ancestry: %s' % ioe.message)
|
||||
abort(404)
|
||||
|
||||
profile.debug('Done')
|
||||
return make_response('true', 200)
|
||||
|
|
|
@ -232,13 +232,15 @@ def initialize_database():
|
|||
LogEntryKind.create(name='delete_application')
|
||||
LogEntryKind.create(name='reset_application_client_secret')
|
||||
|
||||
# Note: These are deprecated.
|
||||
# Note: These next two are deprecated.
|
||||
LogEntryKind.create(name='add_repo_webhook')
|
||||
LogEntryKind.create(name='delete_repo_webhook')
|
||||
|
||||
LogEntryKind.create(name='add_repo_notification')
|
||||
LogEntryKind.create(name='delete_repo_notification')
|
||||
|
||||
LogEntryKind.create(name='regenerate_robot_token')
|
||||
|
||||
ImageStorageLocation.create(name='local_eu')
|
||||
ImageStorageLocation.create(name='local_us')
|
||||
|
||||
|
|
13
license.py
13
license.py
|
@ -1,13 +0,0 @@
|
|||
import pickle
|
||||
|
||||
from Crypto.PublicKey import RSA
|
||||
|
||||
n = 24311791124264168943780535074639421876317270880681911499019414944027362498498429776192966738844514582251884695124256895677070273097239290537016363098432785034818859765271229653729724078304186025013011992335454557504431888746007324285000011384941749613875855493086506022340155196030616409545906383713728780211095701026770053812741971198465120292345817928060114890913931047021503727972067476586739126160044293621653486418983183727572502888923949587290840425930251185737996066354726953382305020440374552871209809125535533731995494145421279907938079885061852265339259634996180877443852561265066616143910755505151318370667L
|
||||
e = 65537L
|
||||
|
||||
def load_license(license_path):
|
||||
decryptor = RSA.construct((n, e))
|
||||
with open(license_path, 'rb') as encrypted_license:
|
||||
decrypted_data = decryptor.encrypt(encrypted_license.read(), 0)
|
||||
|
||||
return pickle.loads(decrypted_data[0])
|
BIN
license.pyc
BIN
license.pyc
Binary file not shown.
|
@ -32,5 +32,7 @@ raven
|
|||
python-ldap
|
||||
pycrypto
|
||||
logentries
|
||||
psycopg2
|
||||
pyyaml
|
||||
git+https://github.com/DevTable/aniso8601-fake.git
|
||||
git+https://github.com/DevTable/anunidecode.git
|
||||
|
|
|
@ -12,6 +12,7 @@ Pillow==2.5.1
|
|||
PyGithub==1.25.0
|
||||
PyMySQL==0.6.2
|
||||
PyPDF2==1.22
|
||||
PyYAML==3.11
|
||||
SQLAlchemy==0.9.7
|
||||
Werkzeug==0.9.6
|
||||
alembic==0.6.5
|
||||
|
@ -44,6 +45,7 @@ python-dateutil==2.2
|
|||
python-ldap==2.4.15
|
||||
python-magic==0.4.6
|
||||
pytz==2014.4
|
||||
psycopg2==2.5.3
|
||||
raven==5.0.0
|
||||
redis==2.10.1
|
||||
reportlab==2.7
|
||||
|
|
|
@ -473,6 +473,22 @@ i.toggle-icon:hover {
|
|||
|
||||
.docker-auth-dialog .token-dialog-body .well {
|
||||
margin-bottom: 0px;
|
||||
position: relative;
|
||||
padding-right: 24px;
|
||||
}
|
||||
|
||||
.docker-auth-dialog .token-dialog-body .well i.fa-refresh {
|
||||
position: absolute;
|
||||
top: 9px;
|
||||
right: 9px;
|
||||
font-size: 20px;
|
||||
color: gray;
|
||||
transition: all 0.5s ease-in-out;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.docker-auth-dialog .token-dialog-body .well i.fa-refresh:hover {
|
||||
color: black;
|
||||
}
|
||||
|
||||
.docker-auth-dialog .token-view {
|
||||
|
@ -738,7 +754,7 @@ i.toggle-icon:hover {
|
|||
}
|
||||
|
||||
.user-notification.notification-animated {
|
||||
width: 21px;
|
||||
min-width: 21px;
|
||||
|
||||
transform: scale(0);
|
||||
-moz-transform: scale(0);
|
||||
|
@ -832,7 +848,7 @@ i.toggle-icon:hover {
|
|||
background-color: red;
|
||||
}
|
||||
|
||||
.phase-icon.waiting, .phase-icon.starting, .phase-icon.initializing {
|
||||
.phase-icon.waiting, .phase-icon.unpacking, .phase-icon.starting, .phase-icon.initializing {
|
||||
background-color: #ddd;
|
||||
}
|
||||
|
||||
|
@ -2266,6 +2282,14 @@ p.editable:hover i {
|
|||
position: relative;
|
||||
}
|
||||
|
||||
.copy-box-element.disabled .input-group-addon {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.copy-box-element.disabled input {
|
||||
border-radius: 4px !important;
|
||||
}
|
||||
|
||||
.global-zeroclipboard-container embed {
|
||||
cursor: pointer;
|
||||
}
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
<div class="copy-box-element">
|
||||
<div class="copy-box-element" ng-class="disabled ? 'disabled' : ''">
|
||||
<div class="id-container">
|
||||
<div class="input-group">
|
||||
<input type="text" class="form-control" value="{{ value }}" readonly>
|
||||
|
|
|
@ -10,19 +10,33 @@
|
|||
</div>
|
||||
<div class="modal-body token-dialog-body">
|
||||
<div class="alert alert-info">The docker <u>username</u> is <b>{{ username }}</b> and the <u>password</u> is the token below. You may use any value for email.</div>
|
||||
<div class="well well-sm">
|
||||
|
||||
<div class="well well-sm" ng-show="regenerating">
|
||||
Regenerating Token...
|
||||
<i class="fa fa-refresh fa-spin"></i>
|
||||
</div>
|
||||
|
||||
<div class="well well-sm" ng-show="!regenerating">
|
||||
<input id="token-view" class="token-view" type="text" value="{{ token }}" onClick="this.select();" readonly>
|
||||
<i class="fa fa-refresh" ng-show="supportsRegenerate" ng-click="askRegenerate()"
|
||||
data-title="Regenerate Token"
|
||||
data-placement="left"
|
||||
bs-tooltip></i>
|
||||
</div>
|
||||
</div>
|
||||
<div class="modal-footer">
|
||||
<div class="modal-footer" ng-show="regenerating">
|
||||
<button type="button" class="btn btn-default" data-dismiss="modal">Close</button>
|
||||
</div>
|
||||
<div class="modal-footer" ng-show="!regenerating">
|
||||
<span class="download-cfg" ng-show="isDownloadSupported()">
|
||||
<i class="fa fa-download"></i>
|
||||
<a href="javascript:void(0)" ng-click="downloadCfg(shownRobot)">Download .dockercfg file</a>
|
||||
</span>
|
||||
<div id="clipboardCopied" style="display: none">
|
||||
Copied to clipboard
|
||||
<div class="clipboard-copied-message" style="display: none">
|
||||
Copied
|
||||
</div>
|
||||
<button id="copyClipboard" type="button" class="btn btn-primary" data-clipboard-target="token-view">Copy to clipboard</button>
|
||||
<input type="hidden" name="command-data" id="command-data" value="{{ command }}">
|
||||
<button id="copyClipboard" type="button" class="btn btn-primary" data-clipboard-target="command-data">Copy Login Command</button>
|
||||
<button type="button" class="btn btn-default" data-dismiss="modal">Close</button>
|
||||
</div>
|
||||
</div><!-- /.modal-content -->
|
||||
|
|
|
@ -37,15 +37,7 @@
|
|||
<a href="javascript:void(0)" class="dropdown-toggle user-dropdown user-view" data-toggle="dropdown">
|
||||
<img src="//www.gravatar.com/avatar/{{ user.gravatar }}?s=32&d=identicon" />
|
||||
{{ user.username }}
|
||||
<span class="badge user-notification notification-animated"
|
||||
ng-show="notificationService.notifications.length"
|
||||
ng-class="notificationService.notificationClasses"
|
||||
bs-tooltip=""
|
||||
data-title="User Notifications"
|
||||
data-placement="left"
|
||||
data-container="body">
|
||||
{{ notificationService.notifications.length }}
|
||||
</span>
|
||||
<span class="notifications-bubble"></span>
|
||||
<b class="caret"></b>
|
||||
</a>
|
||||
<ul class="dropdown-menu">
|
||||
|
@ -58,11 +50,7 @@
|
|||
<a href="javascript:void(0)" data-template="/static/directives/notification-bar.html"
|
||||
data-animation="am-slide-right" bs-aside="aside" data-container="body">
|
||||
Notifications
|
||||
<span class="badge user-notification"
|
||||
ng-class="notificationService.notificationClasses"
|
||||
ng-show="notificationService.notifications.length">
|
||||
{{ notificationService.notifications.length }}
|
||||
</span>
|
||||
<span class="notifications-bubble"></span>
|
||||
</a>
|
||||
</li>
|
||||
<li><a ng-href="/organizations/" target="{{ appLinkTarget() }}">Organizations</a></li>
|
||||
|
|
|
@ -3,7 +3,10 @@
|
|||
<div class="aside-content">
|
||||
<div class="aside-header">
|
||||
<button type="button" class="close" ng-click="$hide()">×</button>
|
||||
<h4 class="aside-title">Notifications</h4>
|
||||
<h4 class="aside-title">
|
||||
Notifications
|
||||
<span class="notifications-bubble"></span>
|
||||
</h4>
|
||||
</div>
|
||||
<div class="aside-body">
|
||||
<div ng-repeat="notification in notificationService.notifications">
|
||||
|
|
7
static/directives/notifications-bubble.html
Normal file
7
static/directives/notifications-bubble.html
Normal file
|
@ -0,0 +1,7 @@
|
|||
<span class="notifications-bubble-element">
|
||||
<span class="badge user-notification notification-animated"
|
||||
ng-show="notificationService.notifications.length"
|
||||
ng-class="notificationService.notificationClasses">
|
||||
{{ notificationService.notifications.length }}<span ng-if="notificationService.additionalNotifications">+</span>
|
||||
</span>
|
||||
</span>
|
|
@ -31,7 +31,7 @@
|
|||
</div>
|
||||
|
||||
<div class="docker-auth-dialog" username="shownRobot.name" token="shownRobot.token"
|
||||
shown="!!shownRobot" counter="showRobotCounter">
|
||||
shown="!!shownRobot" counter="showRobotCounter" supports-regenerate="true" regenerate="regenerateToken(username)">
|
||||
<i class="fa fa-wrench"></i> {{ shownRobot.name }}
|
||||
</div>
|
||||
</div>
|
||||
|
|
351
static/js/app.js
351
static/js/app.js
|
@ -1,6 +1,46 @@
|
|||
var TEAM_PATTERN = '^[a-zA-Z][a-zA-Z0-9]+$';
|
||||
var ROBOT_PATTERN = '^[a-zA-Z][a-zA-Z0-9]{3,29}$';
|
||||
|
||||
$.fn.clipboardCopy = function() {
|
||||
if (zeroClipboardSupported) {
|
||||
(new ZeroClipboard($(this)));
|
||||
return true;
|
||||
}
|
||||
|
||||
this.hide();
|
||||
return false;
|
||||
};
|
||||
|
||||
var zeroClipboardSupported = true;
|
||||
ZeroClipboard.config({
|
||||
'swfPath': 'static/lib/ZeroClipboard.swf'
|
||||
});
|
||||
|
||||
ZeroClipboard.on("error", function(e) {
|
||||
zeroClipboardSupported = false;
|
||||
});
|
||||
|
||||
ZeroClipboard.on('aftercopy', function(e) {
|
||||
var container = e.target.parentNode.parentNode.parentNode;
|
||||
var message = $(container).find('.clipboard-copied-message')[0];
|
||||
|
||||
// Resets the animation.
|
||||
var elem = message;
|
||||
elem.style.display = 'none';
|
||||
elem.classList.remove('animated');
|
||||
|
||||
// Show the notification.
|
||||
setTimeout(function() {
|
||||
elem.style.display = 'inline-block';
|
||||
elem.classList.add('animated');
|
||||
}, 10);
|
||||
|
||||
// Reset the notification.
|
||||
setTimeout(function() {
|
||||
elem.style.display = 'none';
|
||||
}, 5000);
|
||||
});
|
||||
|
||||
function getRestUrl(args) {
|
||||
var url = '';
|
||||
for (var i = 0; i < arguments.length; ++i) {
|
||||
|
@ -59,18 +99,8 @@ function getFirstTextLine(commentString) {
|
|||
}
|
||||
|
||||
function createRobotAccount(ApiService, is_org, orgname, name, callback) {
|
||||
ApiService.createRobot(is_org ? orgname : null, null, {'robot_shortname': name}).then(callback, function(resp) {
|
||||
bootbox.dialog({
|
||||
"message": resp.data ? resp.data['message'] : 'The robot account could not be created',
|
||||
"title": "Cannot create robot account",
|
||||
"buttons": {
|
||||
"close": {
|
||||
"label": "Close",
|
||||
"className": "btn-primary"
|
||||
}
|
||||
}
|
||||
});
|
||||
});
|
||||
ApiService.createRobot(is_org ? orgname : null, null, {'robot_shortname': name})
|
||||
.then(callback, ApiService.errorDisplay('Cannot create robot account'));
|
||||
}
|
||||
|
||||
function createOrganizationTeam(ApiService, orgname, teamname, callback) {
|
||||
|
@ -84,18 +114,8 @@ function createOrganizationTeam(ApiService, orgname, teamname, callback) {
|
|||
'teamname': teamname
|
||||
};
|
||||
|
||||
ApiService.updateOrganizationTeam(data, params).then(callback, function(resp) {
|
||||
bootbox.dialog({
|
||||
"message": resp.data ? resp.data : 'The team could not be created',
|
||||
"title": "Cannot create team",
|
||||
"buttons": {
|
||||
"close": {
|
||||
"label": "Close",
|
||||
"className": "btn-primary"
|
||||
}
|
||||
}
|
||||
});
|
||||
});
|
||||
ApiService.updateOrganizationTeam(data, params)
|
||||
.then(callback, ApiService.errorDisplay('Cannot create team'));
|
||||
}
|
||||
|
||||
function getMarkedDown(string) {
|
||||
|
@ -870,6 +890,38 @@ quayApp = angular.module('quay', quayDependencies, function($provide, cfpLoading
|
|||
buildMethodsForEndpointResource(endpointResource, resourceMap);
|
||||
}
|
||||
|
||||
apiService.getErrorMessage = function(resp, defaultMessage) {
|
||||
var message = defaultMessage;
|
||||
if (resp['data']) {
|
||||
message = resp['data']['error_message'] || resp['data']['message'] || resp['data']['error_description'] || message;
|
||||
}
|
||||
|
||||
return message;
|
||||
};
|
||||
|
||||
apiService.errorDisplay = function(defaultMessage, opt_handler) {
|
||||
return function(resp) {
|
||||
var message = apiService.getErrorMessage(resp, defaultMessage);
|
||||
if (opt_handler) {
|
||||
var handlerMessage = opt_handler(resp);
|
||||
if (handlerMessage) {
|
||||
message = handlerMessage;
|
||||
}
|
||||
}
|
||||
|
||||
bootbox.dialog({
|
||||
"message": message,
|
||||
"title": defaultMessage,
|
||||
"buttons": {
|
||||
"close": {
|
||||
"label": "Close",
|
||||
"className": "btn-primary"
|
||||
}
|
||||
}
|
||||
});
|
||||
};
|
||||
};
|
||||
|
||||
return apiService;
|
||||
}]);
|
||||
|
||||
|
@ -1126,7 +1178,8 @@ quayApp = angular.module('quay', quayDependencies, function($provide, cfpLoading
|
|||
'user': null,
|
||||
'notifications': [],
|
||||
'notificationClasses': [],
|
||||
'notificationSummaries': []
|
||||
'notificationSummaries': [],
|
||||
'additionalNotifications': false
|
||||
};
|
||||
|
||||
var pollTimerHandle = null;
|
||||
|
@ -1244,7 +1297,9 @@ quayApp = angular.module('quay', quayDependencies, function($provide, cfpLoading
|
|||
'uuid': notification.id
|
||||
};
|
||||
|
||||
ApiService.updateUserNotification(notification, params);
|
||||
ApiService.updateUserNotification(notification, params, function() {
|
||||
notificationService.update();
|
||||
}, ApiService.errorDisplay('Could not update notification'));
|
||||
|
||||
var index = $.inArray(notification, notificationService.notifications);
|
||||
if (index >= 0) {
|
||||
|
@ -1310,6 +1365,7 @@ quayApp = angular.module('quay', quayDependencies, function($provide, cfpLoading
|
|||
|
||||
ApiService.listUserNotifications().then(function(resp) {
|
||||
notificationService.notifications = resp['notifications'];
|
||||
notificationService.additionalNotifications = resp['additional'];
|
||||
notificationService.notificationClasses = notificationService.getClasses(notificationService.notifications);
|
||||
});
|
||||
};
|
||||
|
@ -1541,7 +1597,7 @@ quayApp = angular.module('quay', quayDependencies, function($provide, cfpLoading
|
|||
});
|
||||
};
|
||||
|
||||
planService.changePlan = function($scope, orgname, planId, callbacks) {
|
||||
planService.changePlan = function($scope, orgname, planId, callbacks, opt_async) {
|
||||
if (!Features.BILLING) { return; }
|
||||
|
||||
if (callbacks['started']) {
|
||||
|
@ -1554,7 +1610,7 @@ quayApp = angular.module('quay', quayDependencies, function($provide, cfpLoading
|
|||
planService.getCardInfo(orgname, function(cardInfo) {
|
||||
if (plan.price > 0 && (previousSubscribeFailure || !cardInfo.last4)) {
|
||||
var title = cardInfo.last4 ? 'Subscribe' : 'Start Trial ({{amount}} plan)';
|
||||
planService.showSubscribeDialog($scope, orgname, planId, callbacks, title);
|
||||
planService.showSubscribeDialog($scope, orgname, planId, callbacks, title, /* async */true);
|
||||
return;
|
||||
}
|
||||
|
||||
|
@ -1627,9 +1683,34 @@ quayApp = angular.module('quay', quayDependencies, function($provide, cfpLoading
|
|||
return email;
|
||||
};
|
||||
|
||||
planService.showSubscribeDialog = function($scope, orgname, planId, callbacks, opt_title) {
|
||||
planService.showSubscribeDialog = function($scope, orgname, planId, callbacks, opt_title, opt_async) {
|
||||
if (!Features.BILLING) { return; }
|
||||
|
||||
// If the async parameter is true and this is a browser that does not allow async popup of the
|
||||
// Stripe dialog (such as Mobile Safari or IE), show a bootbox to show the dialog instead.
|
||||
var isIE = navigator.appName.indexOf("Internet Explorer") != -1;
|
||||
var isMobileSafari = navigator.userAgent.match(/(iPod|iPhone|iPad)/) && navigator.userAgent.match(/AppleWebKit/);
|
||||
|
||||
if (opt_async && (isIE || isMobileSafari)) {
|
||||
bootbox.dialog({
|
||||
"message": "Please click 'Subscribe' to continue",
|
||||
"buttons": {
|
||||
"subscribe": {
|
||||
"label": "Subscribe",
|
||||
"className": "btn-primary",
|
||||
"callback": function() {
|
||||
planService.showSubscribeDialog($scope, orgname, planId, callbacks, opt_title, false);
|
||||
}
|
||||
},
|
||||
"close": {
|
||||
"label": "Cancel",
|
||||
"className": "btn-default"
|
||||
}
|
||||
}
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
if (callbacks['opening']) {
|
||||
callbacks['opening']();
|
||||
}
|
||||
|
@ -2084,18 +2165,7 @@ quayApp.directive('applicationReference', function () {
|
|||
template: '/static/directives/application-reference-dialog.html',
|
||||
show: true
|
||||
});
|
||||
}, function() {
|
||||
bootbox.dialog({
|
||||
"message": 'The application could not be found; it might have been deleted.',
|
||||
"title": "Cannot find application",
|
||||
"buttons": {
|
||||
"close": {
|
||||
"label": "Close",
|
||||
"className": "btn-primary"
|
||||
}
|
||||
}
|
||||
});
|
||||
});
|
||||
}, ApiService.errorDisplay('Application could not be found'));
|
||||
};
|
||||
}
|
||||
};
|
||||
|
@ -2176,6 +2246,8 @@ quayApp.directive('copyBox', function () {
|
|||
'hoveringMessage': '=hoveringMessage'
|
||||
},
|
||||
controller: function($scope, $element, $rootScope) {
|
||||
$scope.disabled = false;
|
||||
|
||||
var number = $rootScope.__copyBoxIdCounter || 0;
|
||||
$rootScope.__copyBoxIdCounter = number + 1;
|
||||
$scope.inputId = "copy-box-input-" + number;
|
||||
|
@ -2185,27 +2257,7 @@ quayApp.directive('copyBox', function () {
|
|||
|
||||
input.attr('id', $scope.inputId);
|
||||
button.attr('data-clipboard-target', $scope.inputId);
|
||||
|
||||
var clip = new ZeroClipboard($(button), { 'moviePath': 'static/lib/ZeroClipboard.swf' });
|
||||
clip.on('complete', function(e) {
|
||||
var message = $(this.parentNode.parentNode.parentNode).find('.clipboard-copied-message')[0];
|
||||
|
||||
// Resets the animation.
|
||||
var elem = message;
|
||||
elem.style.display = 'none';
|
||||
elem.classList.remove('animated');
|
||||
|
||||
// Show the notification.
|
||||
setTimeout(function() {
|
||||
elem.style.display = 'inline-block';
|
||||
elem.classList.add('animated');
|
||||
}, 10);
|
||||
|
||||
// Reset the notification.
|
||||
setTimeout(function() {
|
||||
elem.style.display = 'none';
|
||||
}, 5000);
|
||||
});
|
||||
$scope.disabled = !button.clipboardCopy();
|
||||
}
|
||||
};
|
||||
return directiveDefinitionObject;
|
||||
|
@ -2439,11 +2491,38 @@ quayApp.directive('dockerAuthDialog', function (Config) {
|
|||
'username': '=username',
|
||||
'token': '=token',
|
||||
'shown': '=shown',
|
||||
'counter': '=counter'
|
||||
'counter': '=counter',
|
||||
'supportsRegenerate': '@supportsRegenerate',
|
||||
'regenerate': '®enerate'
|
||||
},
|
||||
controller: function($scope, $element) {
|
||||
controller: function($scope, $element) {
|
||||
var updateCommand = function() {
|
||||
$scope.command = 'docker login -e="." -u="' + $scope.username +
|
||||
'" -p="' + $scope.token + '" ' + Config['SERVER_HOSTNAME'];
|
||||
};
|
||||
|
||||
$scope.$watch('username', updateCommand);
|
||||
$scope.$watch('token', updateCommand);
|
||||
|
||||
$scope.regenerating = true;
|
||||
|
||||
$scope.askRegenerate = function() {
|
||||
bootbox.confirm('Are you sure you want to regenerate the token? All existing login credentials will become invalid', function(resp) {
|
||||
if (resp) {
|
||||
$scope.regenerating = true;
|
||||
$scope.regenerate({'username': $scope.username, 'token': $scope.token});
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
$scope.isDownloadSupported = function() {
|
||||
try { return !!new Blob(); } catch(e){}
|
||||
var isSafari = /^((?!chrome).)*safari/i.test(navigator.userAgent);
|
||||
if (isSafari) {
|
||||
// Doesn't work properly in Safari, sadly.
|
||||
return false;
|
||||
}
|
||||
|
||||
try { return !!new Blob(); } catch(e) {}
|
||||
return false;
|
||||
};
|
||||
|
||||
|
@ -2461,6 +2540,8 @@ quayApp.directive('dockerAuthDialog', function (Config) {
|
|||
};
|
||||
|
||||
var show = function(r) {
|
||||
$scope.regenerating = false;
|
||||
|
||||
if (!$scope.shown || !$scope.username || !$scope.token) {
|
||||
$('#dockerauthmodal').modal('hide');
|
||||
return;
|
||||
|
@ -2706,6 +2787,8 @@ quayApp.directive('logsView', function () {
|
|||
return 'Delete notification of event "' + eventData['title'] + '" for repository {repo}';
|
||||
},
|
||||
|
||||
'regenerate_robot_token': 'Regenerated token for robot {robot}',
|
||||
|
||||
// Note: These are deprecated.
|
||||
'add_repo_webhook': 'Add webhook in repository {repo}',
|
||||
'delete_repo_webhook': 'Delete webhook in repository {repo}'
|
||||
|
@ -2752,6 +2835,7 @@ quayApp.directive('logsView', function () {
|
|||
'reset_application_client_secret': 'Reset Client Secret',
|
||||
'add_repo_notification': 'Add repository notification',
|
||||
'delete_repo_notification': 'Delete repository notification',
|
||||
'regenerate_robot_token': 'Regenerate Robot Token',
|
||||
|
||||
// Note: these are deprecated.
|
||||
'add_repo_webhook': 'Add webhook',
|
||||
|
@ -2878,18 +2962,7 @@ quayApp.directive('applicationManager', function () {
|
|||
|
||||
ApiService.createOrganizationApplication(data, params).then(function(resp) {
|
||||
$scope.applications.push(resp);
|
||||
}, function(resp) {
|
||||
bootbox.dialog({
|
||||
"message": resp['message'] || 'The application could not be created',
|
||||
"title": "Cannot create application",
|
||||
"buttons": {
|
||||
"close": {
|
||||
"label": "Close",
|
||||
"className": "btn-primary"
|
||||
}
|
||||
}
|
||||
});
|
||||
});
|
||||
}, ApiService.errorDisplay('Cannot create application'));
|
||||
};
|
||||
|
||||
var update = function() {
|
||||
|
@ -2934,6 +3007,20 @@ quayApp.directive('robotsManager', function () {
|
|||
$scope.shownRobot = null;
|
||||
$scope.showRobotCounter = 0;
|
||||
|
||||
$scope.regenerateToken = function(username) {
|
||||
if (!username) { return; }
|
||||
|
||||
var shortName = $scope.getShortenedName(username);
|
||||
ApiService.regenerateRobotToken($scope.organization, null, {'robot_shortname': shortName}).then(function(updated) {
|
||||
var index = $scope.findRobotIndexByName(username);
|
||||
if (index >= 0) {
|
||||
$scope.robots.splice(index, 1);
|
||||
$scope.robots.push(updated);
|
||||
}
|
||||
$scope.shownRobot = updated;
|
||||
}, ApiService.errorDisplay('Cannot regenerate robot account token'));
|
||||
};
|
||||
|
||||
$scope.showRobot = function(info) {
|
||||
$scope.shownRobot = info;
|
||||
$scope.showRobotCounter++;
|
||||
|
@ -2974,18 +3061,7 @@ quayApp.directive('robotsManager', function () {
|
|||
if (index >= 0) {
|
||||
$scope.robots.splice(index, 1);
|
||||
}
|
||||
}, function() {
|
||||
bootbox.dialog({
|
||||
"message": 'The selected robot account could not be deleted',
|
||||
"title": "Cannot delete robot account",
|
||||
"buttons": {
|
||||
"close": {
|
||||
"label": "Close",
|
||||
"className": "btn-primary"
|
||||
}
|
||||
}
|
||||
});
|
||||
});
|
||||
}, ApiService.errorDisplay('Cannot delete robot account'));
|
||||
};
|
||||
|
||||
var update = function() {
|
||||
|
@ -3050,18 +3126,7 @@ quayApp.directive('prototypeManager', function () {
|
|||
|
||||
ApiService.updateOrganizationPrototypePermission(data, params).then(function(resp) {
|
||||
prototype.role = role;
|
||||
}, function(resp) {
|
||||
bootbox.dialog({
|
||||
"message": resp.data ? resp.data : 'The permission could not be modified',
|
||||
"title": "Cannot modify permission",
|
||||
"buttons": {
|
||||
"close": {
|
||||
"label": "Close",
|
||||
"className": "btn-primary"
|
||||
}
|
||||
}
|
||||
});
|
||||
});
|
||||
}, ApiService.errorDisplay('Cannot modify permission'));
|
||||
};
|
||||
|
||||
$scope.comparePrototypes = function(p) {
|
||||
|
@ -3101,23 +3166,16 @@ quayApp.directive('prototypeManager', function () {
|
|||
data['activating_user'] = $scope.activatingForNew;
|
||||
}
|
||||
|
||||
var errorHandler = ApiService.errorDisplay('Cannot create permission',
|
||||
function(resp) {
|
||||
$('#addPermissionDialogModal').modal('hide');
|
||||
});
|
||||
|
||||
ApiService.createOrganizationPrototypePermission(data, params).then(function(resp) {
|
||||
$scope.prototypes.push(resp);
|
||||
$scope.loading = false;
|
||||
$('#addPermissionDialogModal').modal('hide');
|
||||
}, function(resp) {
|
||||
$('#addPermissionDialogModal').modal('hide');
|
||||
bootbox.dialog({
|
||||
"message": resp.data ? resp.data : 'The permission could not be created',
|
||||
"title": "Cannot create permission",
|
||||
"buttons": {
|
||||
"close": {
|
||||
"label": "Close",
|
||||
"className": "btn-primary"
|
||||
}
|
||||
}
|
||||
});
|
||||
});
|
||||
}, errorHandler);
|
||||
};
|
||||
|
||||
$scope.deletePrototype = function(prototype) {
|
||||
|
@ -3131,18 +3189,7 @@ quayApp.directive('prototypeManager', function () {
|
|||
ApiService.deleteOrganizationPrototypePermission(null, params).then(function(resp) {
|
||||
$scope.prototypes.splice($scope.prototypes.indexOf(prototype), 1);
|
||||
$scope.loading = false;
|
||||
}, function(resp) {
|
||||
bootbox.dialog({
|
||||
"message": resp.data ? resp.data : 'The permission could not be deleted',
|
||||
"title": "Cannot delete permission",
|
||||
"buttons": {
|
||||
"close": {
|
||||
"label": "Close",
|
||||
"className": "btn-primary"
|
||||
}
|
||||
}
|
||||
});
|
||||
});
|
||||
}, ApiService.errorDisplay('Cannot delete permission'));
|
||||
};
|
||||
|
||||
var update = function() {
|
||||
|
@ -3990,7 +4037,7 @@ quayApp.directive('planManager', function () {
|
|||
return true;
|
||||
};
|
||||
|
||||
$scope.changeSubscription = function(planId) {
|
||||
$scope.changeSubscription = function(planId, opt_async) {
|
||||
if ($scope.planChanging) { return; }
|
||||
|
||||
var callbacks = {
|
||||
|
@ -4004,7 +4051,7 @@ quayApp.directive('planManager', function () {
|
|||
}
|
||||
};
|
||||
|
||||
PlanService.changePlan($scope, $scope.organization, planId, callbacks);
|
||||
PlanService.changePlan($scope, $scope.organization, planId, callbacks, opt_async);
|
||||
};
|
||||
|
||||
$scope.cancelSubscription = function() {
|
||||
|
@ -4067,7 +4114,7 @@ quayApp.directive('planManager', function () {
|
|||
if ($scope.readyForPlan) {
|
||||
var planRequested = $scope.readyForPlan();
|
||||
if (planRequested && planRequested != PlanService.getFreePlan()) {
|
||||
$scope.changeSubscription(planRequested);
|
||||
$scope.changeSubscription(planRequested, /* async */true);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
@ -4485,26 +4532,17 @@ quayApp.directive('setupTriggerDialog', function () {
|
|||
|
||||
$scope.activating = true;
|
||||
|
||||
var errorHandler = ApiService.errorDisplay('Cannot activate build trigger', function(resp) {
|
||||
$scope.hide();
|
||||
$scope.canceled({'trigger': $scope.trigger});
|
||||
});
|
||||
|
||||
ApiService.activateBuildTrigger(data, params).then(function(resp) {
|
||||
$scope.hide();
|
||||
$scope.trigger['is_active'] = true;
|
||||
$scope.trigger['pull_robot'] = resp['pull_robot'];
|
||||
$scope.activated({'trigger': $scope.trigger});
|
||||
}, function(resp) {
|
||||
$scope.hide();
|
||||
$scope.canceled({'trigger': $scope.trigger});
|
||||
|
||||
bootbox.dialog({
|
||||
"message": resp['data']['message'] || 'The build trigger setup could not be completed',
|
||||
"title": "Could not activate build trigger",
|
||||
"buttons": {
|
||||
"close": {
|
||||
"label": "Close",
|
||||
"className": "btn-primary"
|
||||
}
|
||||
}
|
||||
});
|
||||
});
|
||||
}, errorHandler);
|
||||
};
|
||||
|
||||
var check = function() {
|
||||
|
@ -4844,6 +4882,9 @@ quayApp.directive('buildMessage', function () {
|
|||
case 'waiting':
|
||||
return 'Waiting for available build worker';
|
||||
|
||||
case 'unpacking':
|
||||
return 'Unpacking build package';
|
||||
|
||||
case 'pulling':
|
||||
return 'Pulling base image';
|
||||
|
||||
|
@ -4899,6 +4940,7 @@ quayApp.directive('buildProgress', function () {
|
|||
case 'starting':
|
||||
case 'waiting':
|
||||
case 'cannot_load':
|
||||
case 'unpacking':
|
||||
return 0;
|
||||
break;
|
||||
}
|
||||
|
@ -5118,6 +5160,23 @@ quayApp.directive('twitterView', function () {
|
|||
});
|
||||
|
||||
|
||||
quayApp.directive('notificationsBubble', function () {
|
||||
var directiveDefinitionObject = {
|
||||
priority: 0,
|
||||
templateUrl: '/static/directives/notifications-bubble.html',
|
||||
replace: false,
|
||||
transclude: false,
|
||||
restrict: 'C',
|
||||
scope: {
|
||||
},
|
||||
controller: function($scope, UserService, NotificationService) {
|
||||
$scope.notificationService = NotificationService;
|
||||
}
|
||||
};
|
||||
return directiveDefinitionObject;
|
||||
});
|
||||
|
||||
|
||||
quayApp.directive('notificationView', function () {
|
||||
var directiveDefinitionObject = {
|
||||
priority: 0,
|
||||
|
@ -5705,8 +5764,8 @@ quayApp.run(['$location', '$rootScope', 'Restangular', 'UserService', 'PlanServi
|
|||
}
|
||||
}
|
||||
|
||||
if (!Features.BILLING && response.status == 402) {
|
||||
$('#overlicenseModal').modal({});
|
||||
if (response.status == 503) {
|
||||
$('#cannotContactService').modal({});
|
||||
return false;
|
||||
}
|
||||
|
||||
|
|
|
@ -1,34 +1,4 @@
|
|||
$.fn.clipboardCopy = function() {
|
||||
var clip = new ZeroClipboard($(this), { 'moviePath': 'static/lib/ZeroClipboard.swf' });
|
||||
|
||||
clip.on('complete', function() {
|
||||
// Resets the animation.
|
||||
var elem = $('#clipboardCopied')[0];
|
||||
if (!elem) {
|
||||
return;
|
||||
}
|
||||
|
||||
elem.style.display = 'none';
|
||||
elem.classList.remove('animated');
|
||||
|
||||
// Show the notification.
|
||||
setTimeout(function() {
|
||||
if (!elem) { return; }
|
||||
elem.style.display = 'inline-block';
|
||||
elem.classList.add('animated');
|
||||
}, 10);
|
||||
});
|
||||
};
|
||||
|
||||
function SignInCtrl($scope, $location) {
|
||||
var redirect = $location.search()['redirect'];
|
||||
if (redirect && redirect.indexOf('/') < 0) {
|
||||
delete $location.search()['redirect'];
|
||||
$scope.redirectUrl = '/' + redirect;
|
||||
return;
|
||||
}
|
||||
|
||||
$scope.redirectUrl = '/';
|
||||
}
|
||||
|
||||
function GuideCtrl() {
|
||||
|
@ -543,23 +513,15 @@ function RepoCtrl($scope, $sanitize, Restangular, ImageMetadataService, ApiServi
|
|||
'image': image.id
|
||||
};
|
||||
|
||||
var errorHandler = ApiService.errorDisplay('Cannot create or move tag', function(resp) {
|
||||
$('#addTagModal').modal('hide');
|
||||
});
|
||||
|
||||
ApiService.changeTagImage(data, params).then(function(resp) {
|
||||
$scope.creatingTag = false;
|
||||
loadViewInfo();
|
||||
$('#addTagModal').modal('hide');
|
||||
}, function(resp) {
|
||||
$('#addTagModal').modal('hide');
|
||||
bootbox.dialog({
|
||||
"message": resp.data ? resp.data : 'Could not create or move tag',
|
||||
"title": "Cannot create or move tag",
|
||||
"buttons": {
|
||||
"close": {
|
||||
"label": "Close",
|
||||
"className": "btn-primary"
|
||||
}
|
||||
}
|
||||
});
|
||||
});
|
||||
}, errorHandler);
|
||||
};
|
||||
|
||||
$scope.deleteTag = function(tagName) {
|
||||
|
@ -573,18 +535,7 @@ function RepoCtrl($scope, $sanitize, Restangular, ImageMetadataService, ApiServi
|
|||
|
||||
ApiService.deleteFullTag(null, params).then(function() {
|
||||
loadViewInfo();
|
||||
}, function(resp) {
|
||||
bootbox.dialog({
|
||||
"message": resp.data ? resp.data : 'Could not delete tag',
|
||||
"title": "Cannot delete tag",
|
||||
"buttons": {
|
||||
"close": {
|
||||
"label": "Close",
|
||||
"className": "btn-primary"
|
||||
}
|
||||
}
|
||||
});
|
||||
});
|
||||
}, ApiService.errorDisplay('Cannot delete tag'));
|
||||
};
|
||||
|
||||
$scope.getImagesForTagBySize = function(tag) {
|
||||
|
@ -763,8 +714,6 @@ function RepoCtrl($scope, $sanitize, Restangular, ImageMetadataService, ApiServi
|
|||
|
||||
// Load the builds for this repository. If none are active it will cancel the poll.
|
||||
startBuildInfoTimer(repo);
|
||||
|
||||
$('#copyClipboard').clipboardCopy();
|
||||
});
|
||||
};
|
||||
|
||||
|
@ -1373,17 +1322,16 @@ function RepoAdminCtrl($scope, Restangular, ApiService, KeyService, $routeParams
|
|||
};
|
||||
|
||||
$scope.deleteRole = function(entityName, kind) {
|
||||
var errorHandler = ApiService.errorDisplay('Cannot change permission', function(resp) {
|
||||
if (resp.status == 409) {
|
||||
return 'Cannot change permission as you do not have the authority';
|
||||
}
|
||||
});
|
||||
|
||||
var permissionDelete = Restangular.one(getRestUrl('repository', namespace, name, 'permissions', kind, entityName));
|
||||
permissionDelete.customDELETE().then(function() {
|
||||
delete $scope.permissions[kind][entityName];
|
||||
}, function(resp) {
|
||||
if (resp.status == 409) {
|
||||
$scope.changePermError = resp.data || '';
|
||||
$('#channgechangepermModal').modal({});
|
||||
} else {
|
||||
$('#cannotchangeModal').modal({});
|
||||
}
|
||||
});
|
||||
}, errorHandler);
|
||||
};
|
||||
|
||||
$scope.addRole = function(entityName, role, kind) {
|
||||
|
@ -1394,9 +1342,7 @@ function RepoAdminCtrl($scope, Restangular, ApiService, KeyService, $routeParams
|
|||
var permissionPost = Restangular.one(getRestUrl('repository', namespace, name, 'permissions', kind, entityName));
|
||||
permissionPost.customPUT(permission).then(function(result) {
|
||||
$scope.permissions[kind][entityName] = result;
|
||||
}, function(result) {
|
||||
$('#cannotchangeModal').modal({});
|
||||
});
|
||||
}, ApiService.errorDisplay('Cannot change permission'));
|
||||
};
|
||||
|
||||
$scope.roles = [
|
||||
|
@ -1611,18 +1557,7 @@ function RepoAdminCtrl($scope, Restangular, ApiService, KeyService, $routeParams
|
|||
window.console.log(resp);
|
||||
var url = '/repository/' + namespace + '/' + name + '/build?current=' + resp['id'];
|
||||
document.location = url;
|
||||
}, function(resp) {
|
||||
bootbox.dialog({
|
||||
"message": resp['message'] || 'The build could not be started',
|
||||
"title": "Could not start build",
|
||||
"buttons": {
|
||||
"close": {
|
||||
"label": "Close",
|
||||
"className": "btn-primary"
|
||||
}
|
||||
}
|
||||
});
|
||||
});
|
||||
}, ApiService.errorDisplay('Could not start build'));
|
||||
};
|
||||
|
||||
$scope.deleteTrigger = function(trigger) {
|
||||
|
@ -1750,18 +1685,7 @@ function UserAdminCtrl($scope, $timeout, $location, ApiService, PlanService, Use
|
|||
|
||||
ApiService.deleteUserAuthorization(null, params).then(function(resp) {
|
||||
$scope.authorizedApps.splice($scope.authorizedApps.indexOf(accessTokenInfo), 1);
|
||||
}, function(resp) {
|
||||
bootbox.dialog({
|
||||
"message": resp.message || 'Could not revoke authorization',
|
||||
"title": "Cannot revoke authorization",
|
||||
"buttons": {
|
||||
"close": {
|
||||
"label": "Close",
|
||||
"className": "btn-primary"
|
||||
}
|
||||
}
|
||||
});
|
||||
});
|
||||
}, ApiService.errorDisplay('Could not revoke authorization'));
|
||||
};
|
||||
|
||||
$scope.loadLogs = function() {
|
||||
|
@ -1770,7 +1694,6 @@ function UserAdminCtrl($scope, $timeout, $location, ApiService, PlanService, Use
|
|||
};
|
||||
|
||||
$scope.loadInvoices = function() {
|
||||
if (!$scope.hasPaidBusinessPlan) { return; }
|
||||
$scope.invoicesShown++;
|
||||
};
|
||||
|
||||
|
@ -1956,9 +1879,6 @@ function ImageViewCtrl($scope, $routeParams, $rootScope, $timeout, ApiService, I
|
|||
|
||||
// Fetch the image's changes.
|
||||
fetchChanges();
|
||||
|
||||
$('#copyClipboard').clipboardCopy();
|
||||
|
||||
return image;
|
||||
});
|
||||
};
|
||||
|
@ -2226,13 +2146,14 @@ function OrgViewCtrl($rootScope, $scope, ApiService, $routeParams) {
|
|||
'teamname': teamname
|
||||
};
|
||||
|
||||
var errorHandler = ApiService.errorDisplay('Cannot delete team', function() {
|
||||
$scope.currentDeleteTeam = null;
|
||||
});
|
||||
|
||||
ApiService.deleteOrganizationTeam(null, params).then(function() {
|
||||
delete $scope.organization.teams[teamname];
|
||||
$scope.currentDeleteTeam = null;
|
||||
}, function() {
|
||||
$('#cannotchangeModal').modal({});
|
||||
$scope.currentDeleteTeam = null;
|
||||
});
|
||||
}, errorHandler);
|
||||
};
|
||||
|
||||
var loadOrganization = function() {
|
||||
|
@ -2575,9 +2496,9 @@ function NewOrgCtrl($scope, $routeParams, $timeout, $location, UserService, Plan
|
|||
};
|
||||
|
||||
PlanService.changePlan($scope, org.name, $scope.holder.currentPlan.stripeId, callbacks);
|
||||
}, function(result) {
|
||||
}, function(resp) {
|
||||
$scope.creating = false;
|
||||
$scope.createError = result.data.error_description || result.data;
|
||||
$scope.createError = ApiService.getErrorMessage(resp);
|
||||
$timeout(function() {
|
||||
$('#orgName').popover('show');
|
||||
});
|
||||
|
@ -2654,18 +2575,7 @@ function ManageApplicationCtrl($scope, $routeParams, $rootScope, $location, $tim
|
|||
$timeout(function() {
|
||||
$location.path('/organization/' + orgname + '/admin');
|
||||
}, 500);
|
||||
}, function(resp) {
|
||||
bootbox.dialog({
|
||||
"message": resp.message || 'Could not delete application',
|
||||
"title": "Cannot delete application",
|
||||
"buttons": {
|
||||
"close": {
|
||||
"label": "Close",
|
||||
"className": "btn-primary"
|
||||
}
|
||||
}
|
||||
});
|
||||
});
|
||||
}, ApiService.errorDisplay('Could not delete application'));
|
||||
};
|
||||
|
||||
$scope.updateApplication = function() {
|
||||
|
@ -2683,22 +2593,13 @@ function ManageApplicationCtrl($scope, $routeParams, $rootScope, $location, $tim
|
|||
delete $scope.application['gravatar_email'];
|
||||
}
|
||||
|
||||
var errorHandler = ApiService.errorDisplay('Could not update application', function(resp) {
|
||||
$scope.updating = false;
|
||||
});
|
||||
|
||||
ApiService.updateOrganizationApplication($scope.application, params).then(function(resp) {
|
||||
$scope.application = resp;
|
||||
$scope.updating = false;
|
||||
}, function(resp) {
|
||||
$scope.updating = false;
|
||||
bootbox.dialog({
|
||||
"message": resp.message || 'Could not update application',
|
||||
"title": "Cannot update application",
|
||||
"buttons": {
|
||||
"close": {
|
||||
"label": "Close",
|
||||
"className": "btn-primary"
|
||||
}
|
||||
}
|
||||
});
|
||||
});
|
||||
}, errorHandler);
|
||||
};
|
||||
|
||||
$scope.resetClientSecret = function() {
|
||||
|
@ -2711,18 +2612,7 @@ function ManageApplicationCtrl($scope, $routeParams, $rootScope, $location, $tim
|
|||
|
||||
ApiService.resetOrganizationApplicationClientSecret(null, params).then(function(resp) {
|
||||
$scope.application = resp;
|
||||
}, function(resp) {
|
||||
bootbox.dialog({
|
||||
"message": resp.message || 'Could not reset client secret',
|
||||
"title": "Cannot reset client secret",
|
||||
"buttons": {
|
||||
"close": {
|
||||
"label": "Close",
|
||||
"className": "btn-primary"
|
||||
}
|
||||
}
|
||||
});
|
||||
});
|
||||
}, ApiService.errorDisplay('Could not reset client secret'));
|
||||
};
|
||||
|
||||
var loadOrganization = function() {
|
||||
|
@ -2818,18 +2708,7 @@ function SuperUserAdminCtrl($scope, ApiService, Features, UserService) {
|
|||
|
||||
ApiService.changeInstallUser(data, params).then(function(resp) {
|
||||
$scope.loadUsersInternal();
|
||||
}, function(resp) {
|
||||
bootbox.dialog({
|
||||
"message": resp.data ? resp.data.message : 'Could not change user',
|
||||
"title": "Cannot change user",
|
||||
"buttons": {
|
||||
"close": {
|
||||
"label": "Close",
|
||||
"className": "btn-primary"
|
||||
}
|
||||
}
|
||||
});
|
||||
});
|
||||
}, ApiService.errorDisplay('Could not change user'));
|
||||
};
|
||||
|
||||
$scope.deleteUser = function(user) {
|
||||
|
@ -2841,49 +2720,10 @@ function SuperUserAdminCtrl($scope, ApiService, Features, UserService) {
|
|||
|
||||
ApiService.deleteInstallUser(null, params).then(function(resp) {
|
||||
$scope.loadUsersInternal();
|
||||
}, function(resp) {
|
||||
bootbox.dialog({
|
||||
"message": resp.data ? resp.data.message : 'Could not delete user',
|
||||
"title": "Cannot delete user",
|
||||
"buttons": {
|
||||
"close": {
|
||||
"label": "Close",
|
||||
"className": "btn-primary"
|
||||
}
|
||||
}
|
||||
});
|
||||
});
|
||||
}, ApiService.errorDisplay('Cannot delete user'));
|
||||
};
|
||||
|
||||
var seatUsageLoaded = function(usage) {
|
||||
$scope.usageLoading = false;
|
||||
|
||||
if (usage.count > usage.allowed) {
|
||||
$scope.limit = 'over';
|
||||
} else if (usage.count == usage.allowed) {
|
||||
$scope.limit = 'at';
|
||||
} else if (usage.count >= usage.allowed * 0.7) {
|
||||
$scope.limit = 'near';
|
||||
} else {
|
||||
$scope.limit = 'none';
|
||||
}
|
||||
|
||||
if (!$scope.chart) {
|
||||
$scope.chart = new UsageChart();
|
||||
$scope.chart.draw('seat-usage-chart');
|
||||
}
|
||||
|
||||
$scope.chart.update(usage.count, usage.allowed);
|
||||
};
|
||||
|
||||
var loadSeatUsage = function() {
|
||||
$scope.usageLoading = true;
|
||||
ApiService.getSeatCount().then(function(resp) {
|
||||
seatUsageLoaded(resp);
|
||||
});
|
||||
};
|
||||
|
||||
loadSeatUsage();
|
||||
$scope.loadUsers();
|
||||
}
|
||||
|
||||
function TourCtrl($scope, $location) {
|
||||
|
@ -2913,4 +2753,4 @@ function ConfirmInviteCtrl($scope, $location, UserService, ApiService, Notificat
|
|||
});
|
||||
|
||||
$scope.redirectUrl = 'confirminvite?code=' + $location.search()['code'];
|
||||
}
|
||||
}
|
||||
|
|
|
@ -148,6 +148,8 @@ ImageHistoryTree.prototype.updateDimensions_ = function() {
|
|||
var ch = dimensions.ch;
|
||||
|
||||
// Set the height of the container so that it never goes offscreen.
|
||||
if (!$('#' + container).removeOverscroll) { return; }
|
||||
|
||||
$('#' + container).removeOverscroll();
|
||||
var viewportHeight = $(window).height();
|
||||
var boundingBox = document.getElementById(container).getBoundingClientRect();
|
||||
|
|
17
static/lib/ZeroClipboard.min.js
vendored
Executable file → Normal file
17
static/lib/ZeroClipboard.min.js
vendored
Executable file → Normal file
File diff suppressed because one or more lines are too long
BIN
static/lib/ZeroClipboard.swf
Executable file → Normal file
BIN
static/lib/ZeroClipboard.swf
Executable file → Normal file
Binary file not shown.
|
@ -53,7 +53,7 @@
|
|||
<label for="orgName">Organization Name</label>
|
||||
<input id="orgName" name="orgName" type="text" class="form-control" placeholder="Organization Name"
|
||||
ng-model="org.name" required autofocus data-trigger="manual" data-content="{{ createError }}"
|
||||
data-placement="right" data-container="body" ng-pattern="/^[a-z0-9_]{4,30}$/">
|
||||
data-placement="bottom" data-container="body" ng-pattern="/^[a-z0-9_]{4,30}$/">
|
||||
<span class="description">This will also be the namespace for your repositories</span>
|
||||
</div>
|
||||
|
||||
|
|
|
@ -34,6 +34,13 @@
|
|||
</span>
|
||||
<i class="fa fa-upload visible-lg"></i>
|
||||
</div>
|
||||
<div class="feature">
|
||||
<span class="context-tooltip" bs-tooltip="tooltip.title" data-container="body" data-placement="right"
|
||||
data-title="Administrators can view and download the full invoice history for their organization">
|
||||
Invoice History
|
||||
</span>
|
||||
<i class="fa fa-calendar visible-lg"></i>
|
||||
</div>
|
||||
<div class="feature">
|
||||
<span class="context-tooltip" bs-tooltip="tooltip.title" data-container="body" data-placement="right"
|
||||
data-title="Grant subsets of users in an organization their own permissions, either on a global basis or a per-repository basis">
|
||||
|
@ -48,13 +55,6 @@
|
|||
</span>
|
||||
<i class="fa fa-bar-chart-o visible-lg"></i>
|
||||
</div>
|
||||
<div class="feature">
|
||||
<span class="context-tooltip" bs-tooltip="tooltip.title" data-container="body" data-placement="right"
|
||||
data-title="Administrators can view and download the full invoice history for their organization">
|
||||
Invoice History
|
||||
</span>
|
||||
<i class="fa fa-calendar visible-lg"></i>
|
||||
</div>
|
||||
<div class="feature">
|
||||
<span class="context-tooltip" bs-tooltip="tooltip.title" data-container="body" data-placement="right"
|
||||
data-title="All plans have a free trial">
|
||||
|
@ -81,7 +81,7 @@
|
|||
<div class="feature present"></div>
|
||||
<div class="feature present"></div>
|
||||
<div class="feature present"></div>
|
||||
<div class="feature" ng-class="plan.bus_features ? 'present' : ''"></div>
|
||||
<div class="feature present"></div>
|
||||
<div class="feature" ng-class="plan.bus_features ? 'present' : ''"></div>
|
||||
<div class="feature" ng-class="plan.bus_features ? 'present' : ''"></div>
|
||||
<div class="feature present"></div>
|
||||
|
@ -93,9 +93,9 @@
|
|||
<div class="feature present">SSL Encryption</div>
|
||||
<div class="feature present">Robot accounts</div>
|
||||
<div class="feature present">Dockerfile Build</div>
|
||||
<div class="feature present">Invoice History</div>
|
||||
<div class="feature" ng-class="plan.bus_features ? 'present' : 'notpresent'">Teams</div>
|
||||
<div class="feature" ng-class="plan.bus_features ? 'present' : 'notpresent'">Logging</div>
|
||||
<div class="feature" ng-class="plan.bus_features ? 'present' : 'notpresent'">Invoice History</div>
|
||||
<div class="feature present">Free Trial</div>
|
||||
</div>
|
||||
|
||||
|
|
|
@ -18,7 +18,8 @@
|
|||
<div class="col-md-2">
|
||||
<ul class="nav nav-pills nav-stacked">
|
||||
<li class="active"><a href="javascript:void(0)" data-toggle="tab" data-target="#permissions">Permissions</a></li>
|
||||
<li><a href="javascript:void(0)" data-toggle="tab" data-target="#trigger" ng-click="loadTriggers()">Build Triggers</a></li>
|
||||
<li><a href="javascript:void(0)" data-toggle="tab" data-target="#trigger" ng-click="loadTriggers()"
|
||||
quay-require="['BUILD_SUPPORT']">Build Triggers</a></li>
|
||||
<li><a href="javascript:void(0)" data-toggle="tab" data-target="#badge">Status Badge</a></li>
|
||||
<li><a href="javascript:void(0)" data-toggle="tab" data-target="#notification" ng-click="loadNotifications()">Notifications</a></li>
|
||||
<li><a href="javascript:void(0)" data-toggle="tab" data-target="#publicprivate">Public/Private</a></li>
|
||||
|
@ -225,7 +226,7 @@
|
|||
</div>
|
||||
|
||||
<!-- Triggers tab -->
|
||||
<div id="trigger" class="tab-pane">
|
||||
<div id="trigger" class="tab-pane" quay-require="['BUILD_SUPPORT']">
|
||||
<div class="panel panel-default">
|
||||
<div class="panel-heading">Build Triggers
|
||||
<i class="info-icon fa fa-info-circle" data-placement="left" data-content="Triggers from various services (such as GitHub) which tell the repository to be built and updated."></i>
|
||||
|
@ -377,24 +378,6 @@
|
|||
counter="showNewNotificationCounter"
|
||||
notification-created="handleNotificationCreated(notification)"></div>
|
||||
|
||||
<!-- Modal message dialog -->
|
||||
<div class="modal fade" id="cannotchangeModal">
|
||||
<div class="modal-dialog">
|
||||
<div class="modal-content">
|
||||
<div class="modal-header">
|
||||
<button type="button" class="close" data-dismiss="modal" aria-hidden="true">×</button>
|
||||
<h4 class="modal-title">Cannot change</h4>
|
||||
</div>
|
||||
<div class="modal-body">
|
||||
The selected action could not be performed because you do not have that authority.
|
||||
</div>
|
||||
<div class="modal-footer">
|
||||
<button type="button" class="btn btn-default" data-dismiss="modal">Close</button>
|
||||
</div>
|
||||
</div><!-- /.modal-content -->
|
||||
</div><!-- /.modal-dialog -->
|
||||
</div><!-- /.modal -->
|
||||
|
||||
<!-- Modal message dialog -->
|
||||
<div class="modal fade" id="makepublicModal">
|
||||
<div class="modal-dialog">
|
||||
|
@ -441,26 +424,6 @@
|
|||
</div><!-- /.modal -->
|
||||
|
||||
|
||||
<!-- Modal message dialog -->
|
||||
<div class="modal fade" id="channgechangepermModal">
|
||||
<div class="modal-dialog">
|
||||
<div class="modal-content">
|
||||
<div class="modal-header">
|
||||
<button type="button" class="close" data-dismiss="modal" aria-hidden="true">×</button>
|
||||
<h4 class="modal-title">Cannot change permissions</h4>
|
||||
</div>
|
||||
<div class="modal-body">
|
||||
<span ng-show="!changePermError">You do not have permission to change the permissions on the repository.</span>
|
||||
<span ng-show="changePermError">{{ changePermError }}</span>
|
||||
</div>
|
||||
<div class="modal-footer">
|
||||
<button type="button" class="btn btn-default" data-dismiss="modal">Close</button>
|
||||
</div>
|
||||
</div><!-- /.modal-content -->
|
||||
</div><!-- /.modal-dialog -->
|
||||
</div><!-- /.modal -->
|
||||
|
||||
|
||||
<!-- Modal message dialog -->
|
||||
<div class="modal fade" id="confirmdeleteModal">
|
||||
<div class="modal-dialog">
|
||||
|
|
|
@ -8,9 +8,6 @@
|
|||
<div class="col-md-2">
|
||||
<ul class="nav nav-pills nav-stacked">
|
||||
<li class="active">
|
||||
<a href="javascript:void(0)" data-toggle="tab" data-target="#license">License and Usage</a>
|
||||
</li>
|
||||
<li>
|
||||
<a href="javascript:void(0)" data-toggle="tab" data-target="#users" ng-click="loadUsers()">Users</a>
|
||||
</li>
|
||||
</ul>
|
||||
|
@ -19,19 +16,8 @@
|
|||
<!-- Content -->
|
||||
<div class="col-md-10">
|
||||
<div class="tab-content">
|
||||
<!-- License tab -->
|
||||
<div id="license" class="tab-pane active">
|
||||
<div class="quay-spinner 3x" ng-show="usageLoading"></div>
|
||||
<!-- Chart -->
|
||||
<div>
|
||||
<div id="seat-usage-chart" class="usage-chart limit-{{limit}}"></div>
|
||||
<span class="usage-caption" ng-show="chart">Seat Usage</span>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
|
||||
<!-- Users tab -->
|
||||
<div id="users" class="tab-pane">
|
||||
<div id="users" class="tab-pane active">
|
||||
<div class="quay-spinner" ng-show="!users"></div>
|
||||
<div class="alert alert-error" ng-show="usersError">
|
||||
{{ usersError }}
|
||||
|
|
|
@ -25,7 +25,7 @@
|
|||
<li ng-show="hasPaidPlan" quay-require="['BILLING']">
|
||||
<a href="javascript:void(0)" data-toggle="tab" data-target="#billingoptions">Billing Options</a>
|
||||
</li>
|
||||
<li ng-show="hasPaidBusinessPlan" quay-require="['BILLING']">
|
||||
<li ng-show="hasPaidPlan" quay-require="['BILLING']">
|
||||
<a href="javascript:void(0)" data-toggle="tab" data-target="#billing" ng-click="loadInvoices()">Billing History</a>
|
||||
</li>
|
||||
|
||||
|
|
|
@ -18,7 +18,7 @@
|
|||
<div class="dropdown" data-placement="top" style="display: inline-block"
|
||||
bs-tooltip=""
|
||||
data-title="{{ runningBuilds.length ? 'Dockerfile Builds Running: ' + (runningBuilds.length) : 'Dockerfile Build' }}"
|
||||
ng-show="repo.can_write || buildHistory.length">
|
||||
quay-show="Features.BUILD_SUPPORT && (repo.can_write || buildHistory.length)">
|
||||
<button class="btn btn-default dropdown-toggle" data-toggle="dropdown">
|
||||
<i class="fa fa-tasks fa-lg"></i>
|
||||
<span class="count" ng-class="runningBuilds.length ? 'visible' : ''"><span>{{ runningBuilds.length ? runningBuilds.length : '' }}</span></span>
|
||||
|
@ -58,16 +58,9 @@
|
|||
<span class="pull-command visible-md-inline">
|
||||
<div class="pull-container" data-title="Pull repository" bs-tooltip="tooltip.title">
|
||||
<div class="input-group">
|
||||
<input id="pull-text" type="text" class="form-control" value="{{ 'docker pull ' + Config.getDomain() + '/' + repo.namespace + '/' + repo.name }}" readonly>
|
||||
<span id="copyClipboard" class="input-group-addon" data-title="Copy to Clipboard" data-clipboard-target="pull-text">
|
||||
<i class="fa fa-copy"></i>
|
||||
</span>
|
||||
<div class="copy-box" hovering-message="true" value="'docker pull ' + Config.getDomain() + '/' + repo.namespace + '/' + repo.name"></div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div id="clipboardCopied" class="hovering" style="display: none">
|
||||
Copied to clipboard
|
||||
</div>
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
|
|
|
@ -35,23 +35,18 @@
|
|||
</div><!-- /.modal-dialog -->
|
||||
</div><!-- /.modal -->
|
||||
|
||||
{% if not has_billing %}
|
||||
<!-- Modal message dialog -->
|
||||
<div class="modal fade" id="overlicenseModal" data-backdrop="static">
|
||||
<div class="modal fade" id="cannotContactService" data-backdrop="static">
|
||||
<div class="modal-dialog">
|
||||
<div class="modal-content">
|
||||
<div class="modal-header">
|
||||
<h4 class="modal-title">Cannot create user</h4>
|
||||
<h4 class="modal-title">Cannot Contact External Service</h4>
|
||||
</div>
|
||||
<div class="modal-body">
|
||||
A new user cannot be created as this organization has reached its licensed seat count. Please contact your administrator.
|
||||
</div>
|
||||
<div class="modal-footer">
|
||||
<a href="javascript:void(0)" class="btn btn-primary" data-dismiss="modal" onclick="location = '/signin'">Sign In</a>
|
||||
A connection to an external service has failed. Please reload the page to try again.
|
||||
</div>
|
||||
</div><!-- /.modal-content -->
|
||||
</div><!-- /.modal-dialog -->
|
||||
</div><!-- /.modal -->
|
||||
{% endif %}
|
||||
|
||||
{% endblock %}
|
||||
|
|
Binary file not shown.
|
@ -196,7 +196,7 @@ def build_index_specs():
|
|||
IndexTestSpec(url_for('index.put_repository_auth', repository=PUBLIC_REPO),
|
||||
NO_REPO, 501, 501, 501, 501).set_method('PUT'),
|
||||
|
||||
IndexTestSpec(url_for('index.get_search'), NO_REPO, 501, 501, 501, 501),
|
||||
IndexTestSpec(url_for('index.get_search'), NO_REPO, 200, 200, 200, 200),
|
||||
|
||||
IndexTestSpec(url_for('index.ping'), NO_REPO, 200, 200, 200, 200),
|
||||
|
||||
|
|
|
@ -14,7 +14,9 @@ from endpoints.api.search import FindRepositories, EntitySearch
|
|||
from endpoints.api.image import RepositoryImageChanges, RepositoryImage, RepositoryImageList
|
||||
from endpoints.api.build import (FileDropResource, RepositoryBuildStatus, RepositoryBuildLogs,
|
||||
RepositoryBuildList)
|
||||
from endpoints.api.robot import UserRobotList, OrgRobot, OrgRobotList, UserRobot
|
||||
from endpoints.api.robot import (UserRobotList, OrgRobot, OrgRobotList, UserRobot,
|
||||
RegenerateOrgRobot, RegenerateUserRobot)
|
||||
|
||||
from endpoints.api.trigger import (BuildTriggerActivate, BuildTriggerSources, BuildTriggerSubdirs,
|
||||
TriggerBuildList, ActivateBuildTrigger, BuildTrigger,
|
||||
BuildTriggerList, BuildTriggerAnalyze)
|
||||
|
@ -37,7 +39,7 @@ from endpoints.api.repository import RepositoryList, RepositoryVisibility, Repos
|
|||
from endpoints.api.permission import (RepositoryUserPermission, RepositoryTeamPermission,
|
||||
RepositoryTeamPermissionList, RepositoryUserPermissionList)
|
||||
|
||||
from endpoints.api.superuser import SuperUserLogs, SeatUsage, SuperUserList, SuperUserManagement
|
||||
from endpoints.api.superuser import SuperUserLogs, SuperUserList, SuperUserManagement
|
||||
|
||||
|
||||
try:
|
||||
|
@ -1632,6 +1634,19 @@ class TestOrgRobotBuynlargeZ7pd(ApiTestCase):
|
|||
ApiTestCase.setUp(self)
|
||||
self._set_url(OrgRobot, orgname="buynlarge", robot_shortname="Z7PD")
|
||||
|
||||
def test_get_anonymous(self):
|
||||
self._run_test('GET', 401, None, None)
|
||||
|
||||
def test_get_freshuser(self):
|
||||
self._run_test('GET', 403, 'freshuser', None)
|
||||
|
||||
def test_get_reader(self):
|
||||
self._run_test('GET', 403, 'reader', None)
|
||||
|
||||
def test_get_devtable(self):
|
||||
self._run_test('GET', 400, 'devtable', None)
|
||||
|
||||
|
||||
def test_put_anonymous(self):
|
||||
self._run_test('PUT', 401, None, None)
|
||||
|
||||
|
@ -1644,6 +1659,7 @@ class TestOrgRobotBuynlargeZ7pd(ApiTestCase):
|
|||
def test_put_devtable(self):
|
||||
self._run_test('PUT', 400, 'devtable', None)
|
||||
|
||||
|
||||
def test_delete_anonymous(self):
|
||||
self._run_test('DELETE', 401, None, None)
|
||||
|
||||
|
@ -3040,6 +3056,19 @@ class TestUserRobot5vdy(ApiTestCase):
|
|||
ApiTestCase.setUp(self)
|
||||
self._set_url(UserRobot, robot_shortname="robotname")
|
||||
|
||||
def test_get_anonymous(self):
|
||||
self._run_test('GET', 401, None, None)
|
||||
|
||||
def test_get_freshuser(self):
|
||||
self._run_test('GET', 400, 'freshuser', None)
|
||||
|
||||
def test_get_reader(self):
|
||||
self._run_test('GET', 400, 'reader', None)
|
||||
|
||||
def test_get_devtable(self):
|
||||
self._run_test('GET', 400, 'devtable', None)
|
||||
|
||||
|
||||
def test_put_anonymous(self):
|
||||
self._run_test('PUT', 401, None, None)
|
||||
|
||||
|
@ -3052,6 +3081,7 @@ class TestUserRobot5vdy(ApiTestCase):
|
|||
def test_put_devtable(self):
|
||||
self._run_test('PUT', 201, 'devtable', None)
|
||||
|
||||
|
||||
def test_delete_anonymous(self):
|
||||
self._run_test('DELETE', 401, None, None)
|
||||
|
||||
|
@ -3065,6 +3095,42 @@ class TestUserRobot5vdy(ApiTestCase):
|
|||
self._run_test('DELETE', 400, 'devtable', None)
|
||||
|
||||
|
||||
class TestRegenerateUserRobot(ApiTestCase):
|
||||
def setUp(self):
|
||||
ApiTestCase.setUp(self)
|
||||
self._set_url(RegenerateUserRobot, robot_shortname="robotname")
|
||||
|
||||
def test_post_anonymous(self):
|
||||
self._run_test('POST', 401, None, None)
|
||||
|
||||
def test_post_freshuser(self):
|
||||
self._run_test('POST', 400, 'freshuser', None)
|
||||
|
||||
def test_post_reader(self):
|
||||
self._run_test('POST', 400, 'reader', None)
|
||||
|
||||
def test_post_devtable(self):
|
||||
self._run_test('POST', 400, 'devtable', None)
|
||||
|
||||
|
||||
class TestRegenerateOrgRobot(ApiTestCase):
|
||||
def setUp(self):
|
||||
ApiTestCase.setUp(self)
|
||||
self._set_url(RegenerateOrgRobot, orgname="buynlarge", robot_shortname="robotname")
|
||||
|
||||
def test_post_anonymous(self):
|
||||
self._run_test('POST', 401, None, None)
|
||||
|
||||
def test_post_freshuser(self):
|
||||
self._run_test('POST', 403, 'freshuser', None)
|
||||
|
||||
def test_post_reader(self):
|
||||
self._run_test('POST', 403, 'reader', None)
|
||||
|
||||
def test_post_devtable(self):
|
||||
self._run_test('POST', 400, 'devtable', None)
|
||||
|
||||
|
||||
class TestOrganizationBuynlarge(ApiTestCase):
|
||||
def setUp(self):
|
||||
ApiTestCase.setUp(self)
|
||||
|
|
|
@ -16,7 +16,8 @@ from endpoints.api.tag import RepositoryTagImages, RepositoryTag
|
|||
from endpoints.api.search import FindRepositories, EntitySearch
|
||||
from endpoints.api.image import RepositoryImage, RepositoryImageList
|
||||
from endpoints.api.build import RepositoryBuildStatus, RepositoryBuildLogs, RepositoryBuildList
|
||||
from endpoints.api.robot import UserRobotList, OrgRobot, OrgRobotList, UserRobot
|
||||
from endpoints.api.robot import (UserRobotList, OrgRobot, OrgRobotList, UserRobot,
|
||||
RegenerateUserRobot, RegenerateOrgRobot)
|
||||
from endpoints.api.trigger import (BuildTriggerActivate, BuildTriggerSources, BuildTriggerSubdirs,
|
||||
TriggerBuildList, ActivateBuildTrigger, BuildTrigger,
|
||||
BuildTriggerList, BuildTriggerAnalyze)
|
||||
|
@ -40,7 +41,7 @@ from endpoints.api.organization import (OrganizationList, OrganizationMember,
|
|||
from endpoints.api.repository import RepositoryList, RepositoryVisibility, Repository
|
||||
from endpoints.api.permission import (RepositoryUserPermission, RepositoryTeamPermission,
|
||||
RepositoryTeamPermissionList, RepositoryUserPermissionList)
|
||||
from endpoints.api.superuser import SuperUserLogs, SeatUsage, SuperUserList, SuperUserManagement
|
||||
from endpoints.api.superuser import SuperUserLogs, SuperUserList, SuperUserManagement
|
||||
|
||||
try:
|
||||
app.register_blueprint(api_bp, url_prefix='/api')
|
||||
|
@ -1751,6 +1752,30 @@ class TestUserRobots(ApiTestCase):
|
|||
robots = self.getRobotNames()
|
||||
assert not NO_ACCESS_USER + '+bender' in robots
|
||||
|
||||
def test_regenerate(self):
|
||||
self.login(NO_ACCESS_USER)
|
||||
|
||||
# Create a robot.
|
||||
json = self.putJsonResponse(UserRobot,
|
||||
params=dict(robot_shortname='bender'),
|
||||
expected_code=201)
|
||||
|
||||
token = json['token']
|
||||
|
||||
# Regenerate the robot.
|
||||
json = self.postJsonResponse(RegenerateUserRobot,
|
||||
params=dict(robot_shortname='bender'),
|
||||
expected_code=200)
|
||||
|
||||
# Verify the token changed.
|
||||
self.assertNotEquals(token, json['token'])
|
||||
|
||||
json2 = self.getJsonResponse(UserRobot,
|
||||
params=dict(robot_shortname='bender'),
|
||||
expected_code=200)
|
||||
|
||||
self.assertEquals(json['token'], json2['token'])
|
||||
|
||||
|
||||
class TestOrgRobots(ApiTestCase):
|
||||
def getRobotNames(self):
|
||||
|
@ -1780,6 +1805,31 @@ class TestOrgRobots(ApiTestCase):
|
|||
assert not ORGANIZATION + '+bender' in robots
|
||||
|
||||
|
||||
def test_regenerate(self):
|
||||
self.login(ADMIN_ACCESS_USER)
|
||||
|
||||
# Create a robot.
|
||||
json = self.putJsonResponse(OrgRobot,
|
||||
params=dict(orgname=ORGANIZATION, robot_shortname='bender'),
|
||||
expected_code=201)
|
||||
|
||||
token = json['token']
|
||||
|
||||
# Regenerate the robot.
|
||||
json = self.postJsonResponse(RegenerateOrgRobot,
|
||||
params=dict(orgname=ORGANIZATION, robot_shortname='bender'),
|
||||
expected_code=200)
|
||||
|
||||
# Verify the token changed.
|
||||
self.assertNotEquals(token, json['token'])
|
||||
|
||||
json2 = self.getJsonResponse(OrgRobot,
|
||||
params=dict(orgname=ORGANIZATION, robot_shortname='bender'),
|
||||
expected_code=200)
|
||||
|
||||
self.assertEquals(json['token'], json2['token'])
|
||||
|
||||
|
||||
class TestLogs(ApiTestCase):
|
||||
def test_user_logs(self):
|
||||
self.login(ADMIN_ACCESS_USER)
|
||||
|
|
|
@ -1,38 +0,0 @@
|
|||
import argparse
|
||||
import pickle
|
||||
|
||||
from Crypto.PublicKey import RSA
|
||||
from datetime import datetime, timedelta
|
||||
|
||||
def encrypt(message, output_filename):
|
||||
private_key_file = 'conf/stack/license_key'
|
||||
with open(private_key_file, 'r') as private_key:
|
||||
encryptor = RSA.importKey(private_key)
|
||||
|
||||
encrypted_data = encryptor.decrypt(message)
|
||||
|
||||
with open(output_filename, 'wb') as encrypted_file:
|
||||
encrypted_file.write(encrypted_data)
|
||||
|
||||
parser = argparse.ArgumentParser(description='Create a license file.')
|
||||
parser.add_argument('--users', type=int, default=20,
|
||||
help='Number of users allowed by the license')
|
||||
parser.add_argument('--days', type=int, default=30,
|
||||
help='Number of days for which the license is valid')
|
||||
parser.add_argument('--warn', type=int, default=7,
|
||||
help='Number of days prior to expiration to warn users')
|
||||
parser.add_argument('--output', type=str, required=True,
|
||||
help='File in which to store the license')
|
||||
|
||||
if __name__ == "__main__":
|
||||
args = parser.parse_args()
|
||||
print ('Creating license for %s users for %s days in file: %s' %
|
||||
(args.users, args.days, args.output))
|
||||
|
||||
license_data = {
|
||||
'LICENSE_EXPIRATION': datetime.utcnow() + timedelta(days=args.days),
|
||||
'LICENSE_USER_LIMIT': args.users,
|
||||
'LICENSE_EXPIRATION_WARNING': datetime.utcnow() + timedelta(days=(args.days - args.warn)),
|
||||
}
|
||||
|
||||
encrypt(pickle.dumps(license_data, 2), args.output)
|
|
@ -1,4 +1,4 @@
|
|||
from app import stripe
|
||||
import stripe
|
||||
from app import app
|
||||
|
||||
from util.invoice import renderInvoiceToHtml
|
||||
|
|
31
tools/phpmyadmin/Dockerfile
Normal file
31
tools/phpmyadmin/Dockerfile
Normal file
|
@ -0,0 +1,31 @@
|
|||
FROM phusion/baseimage:0.9.9
|
||||
|
||||
ENV DEBIAN_FRONTEND noninteractive
|
||||
ENV HOME /root
|
||||
ENV UPDATE_APT 2
|
||||
|
||||
RUN apt-get update
|
||||
|
||||
# Install LAMP
|
||||
RUN apt-get install -y lamp-server^
|
||||
|
||||
# Install phpMyAdmin
|
||||
RUN mysqld & \
|
||||
service apache2 start; \
|
||||
sleep 5; \
|
||||
printf y\\n\\n\\n1\\n | apt-get install -y phpmyadmin; \
|
||||
sleep 15; \
|
||||
mysqladmin -u root shutdown
|
||||
|
||||
|
||||
# Setup phpmyadmin to run
|
||||
RUN echo "Include /etc/phpmyadmin/apache.conf" >> /etc/apache2/apache2.conf
|
||||
RUN rm /etc/phpmyadmin/config.inc.php
|
||||
|
||||
ADD config.inc.php /etc/phpmyadmin/config.inc.php
|
||||
|
||||
ADD run-admin.sh /etc/service/phpadmin/run
|
||||
|
||||
EXPOSE 80
|
||||
|
||||
CMD ["/sbin/my_init"]
|
59
tools/phpmyadmin/config.inc.php
Normal file
59
tools/phpmyadmin/config.inc.php
Normal file
|
@ -0,0 +1,59 @@
|
|||
<?php
|
||||
/**
|
||||
* Debian local configuration file
|
||||
*
|
||||
* This file overrides the settings made by phpMyAdmin interactive setup
|
||||
* utility.
|
||||
*
|
||||
* For example configuration see
|
||||
* /usr/share/doc/phpmyadmin/examples/config.sample.inc.php
|
||||
* or
|
||||
* /usr/share/doc/phpmyadmin/examples/config.manyhosts.inc.php
|
||||
*
|
||||
* NOTE: do not add security sensitive data to this file (like passwords)
|
||||
* unless you really know what you're doing. If you do, any user that can
|
||||
* run PHP or CGI on your webserver will be able to read them. If you still
|
||||
* want to do this, make sure to properly secure the access to this file
|
||||
* (also on the filesystem level).
|
||||
*/
|
||||
|
||||
// Load secret generated on postinst
|
||||
include('/var/lib/phpmyadmin/blowfish_secret.inc.php');
|
||||
|
||||
// Load autoconf local config
|
||||
include('/var/lib/phpmyadmin/config.inc.php');
|
||||
|
||||
/**
|
||||
* Server(s) configuration
|
||||
*/
|
||||
$i = 0;
|
||||
// The $cfg['Servers'] array starts with $cfg['Servers'][1]. Do not use $cfg['Servers'][0].
|
||||
// You can disable a server config entry by setting host to ''.
|
||||
$i++;
|
||||
|
||||
/**
|
||||
* Read configuration from dbconfig-common
|
||||
* You can regenerate it using: dpkg-reconfigure -plow phpmyadmin
|
||||
*/
|
||||
if (is_readable('/etc/phpmyadmin/config-db.php')) {
|
||||
require('/etc/phpmyadmin/config-db.php');
|
||||
} else {
|
||||
error_log('phpmyadmin: Failed to load /etc/phpmyadmin/config-db.php.'
|
||||
. ' Check group www-data has read access.');
|
||||
}
|
||||
|
||||
$cfg['Servers'][$i]['auth_type'] = 'HTTP';
|
||||
$cfg['Servers'][$i]['hide_db'] = '(mysql|information_schema|phpmyadmin)';
|
||||
/* Server parameters */
|
||||
$cfg['Servers'][$i]['host'] = 'db1.quay.io';
|
||||
$cfg['Servers'][$i]['ssl'] = true;
|
||||
|
||||
/*
|
||||
* End of servers configuration
|
||||
*/
|
||||
|
||||
/*
|
||||
* Directories for saving/loading files from server
|
||||
*/
|
||||
$cfg['UploadDir'] = '';
|
||||
$cfg['SaveDir'] = '';
|
4
tools/phpmyadmin/run-admin.sh
Executable file
4
tools/phpmyadmin/run-admin.sh
Executable file
|
@ -0,0 +1,4 @@
|
|||
#! /bin/bash
|
||||
|
||||
service apache2 start
|
||||
mysqld
|
|
@ -1,4 +1,3 @@
|
|||
from app import stripe
|
||||
from app import app
|
||||
|
||||
from util.useremails import send_confirmation_email
|
||||
|
|
|
@ -30,7 +30,11 @@ class SendToMixpanel(Process):
|
|||
while True:
|
||||
mp_request = self._mp_queue.get()
|
||||
logger.debug('Got queued mixpanel reqeust.')
|
||||
self._consumer.send(*json.loads(mp_request))
|
||||
try:
|
||||
self._consumer.send(*json.loads(mp_request))
|
||||
except:
|
||||
# Make sure we don't crash if Mixpanel request fails.
|
||||
pass
|
||||
|
||||
|
||||
class FakeMixpanel(object):
|
||||
|
|
|
@ -1,55 +0,0 @@
|
|||
import calendar
|
||||
import sys
|
||||
|
||||
from email.utils import formatdate
|
||||
from apscheduler.schedulers.background import BackgroundScheduler
|
||||
from datetime import datetime, timedelta
|
||||
|
||||
from data import model
|
||||
|
||||
|
||||
class ExpirationScheduler(object):
|
||||
def __init__(self, utc_create_notifications_date, utc_terminate_processes_date):
|
||||
self._scheduler = BackgroundScheduler()
|
||||
self._termination_date = utc_terminate_processes_date
|
||||
|
||||
soon = datetime.now() + timedelta(seconds=1)
|
||||
|
||||
if utc_create_notifications_date > datetime.utcnow():
|
||||
self._scheduler.add_job(model.delete_all_notifications_by_kind, 'date', run_date=soon,
|
||||
args=['expiring_license'])
|
||||
|
||||
local_notifications_date = self._utc_to_local(utc_create_notifications_date)
|
||||
self._scheduler.add_job(self._generate_notifications, 'date',
|
||||
run_date=local_notifications_date)
|
||||
else:
|
||||
self._scheduler.add_job(self._generate_notifications, 'date', run_date=soon)
|
||||
|
||||
local_termination_date = self._utc_to_local(utc_terminate_processes_date)
|
||||
self._scheduler.add_job(self._terminate, 'date', run_date=local_termination_date)
|
||||
|
||||
@staticmethod
|
||||
def _format_date(date):
|
||||
""" Output an RFC822 date format. """
|
||||
if date is None:
|
||||
return None
|
||||
return formatdate(calendar.timegm(date.utctimetuple()))
|
||||
|
||||
@staticmethod
|
||||
def _utc_to_local(utc_dt):
|
||||
# get integer timestamp to avoid precision lost
|
||||
timestamp = calendar.timegm(utc_dt.timetuple())
|
||||
local_dt = datetime.fromtimestamp(timestamp)
|
||||
return local_dt.replace(microsecond=utc_dt.microsecond)
|
||||
|
||||
def _generate_notifications(self):
|
||||
for user in model.get_active_users():
|
||||
model.create_unique_notification('expiring_license', user,
|
||||
{'expires_at': self._format_date(self._termination_date)})
|
||||
|
||||
@staticmethod
|
||||
def _terminate():
|
||||
sys.exit(1)
|
||||
|
||||
def start(self):
|
||||
self._scheduler.start()
|
|
@ -44,9 +44,9 @@ def matches_system_error(status_str):
|
|||
KNOWN_MATCHES = ['lxc-start: invalid', 'lxc-start: failed to', 'lxc-start: Permission denied']
|
||||
|
||||
for match in KNOWN_MATCHES:
|
||||
# 4 because we might have a Unix control code at the start.
|
||||
found = status_str.find(match[0:len(match) + 4])
|
||||
if found >= 0 and found <= 4:
|
||||
# 10 because we might have a Unix control code at the start.
|
||||
found = status_str.find(match[0:len(match) + 10])
|
||||
if found >= 0 and found <= 10:
|
||||
return True
|
||||
|
||||
return False
|
||||
|
@ -477,6 +477,7 @@ class DockerfileBuildWorker(Worker):
|
|||
container['Id'], container['Command'])
|
||||
docker_cl.kill(container['Id'])
|
||||
self._timeout.set()
|
||||
|
||||
except ConnectionError as exc:
|
||||
raise WorkerUnhealthyException(exc.message)
|
||||
|
||||
|
@ -521,16 +522,6 @@ class DockerfileBuildWorker(Worker):
|
|||
logger.info(filetype_msg)
|
||||
log_appender(filetype_msg)
|
||||
|
||||
if c_type not in self._mime_processors:
|
||||
log_appender('error', build_logs.PHASE)
|
||||
repository_build.phase = 'error'
|
||||
repository_build.save()
|
||||
message = 'Unknown mime-type: %s' % c_type
|
||||
log_appender(message, build_logs.ERROR)
|
||||
raise JobException(message)
|
||||
|
||||
build_dir = self._mime_processors[c_type](docker_resource)
|
||||
|
||||
# Spawn a notification that the build has started.
|
||||
event_data = {
|
||||
'build_id': repository_build.uuid,
|
||||
|
@ -544,6 +535,7 @@ class DockerfileBuildWorker(Worker):
|
|||
subpage='build?current=%s' % repository_build.uuid,
|
||||
pathargs=['build', repository_build.uuid])
|
||||
|
||||
|
||||
# Setup a handler for spawning failure messages.
|
||||
def spawn_failure(message, event_data):
|
||||
event_data['error_message'] = message
|
||||
|
@ -551,6 +543,29 @@ class DockerfileBuildWorker(Worker):
|
|||
subpage='build?current=%s' % repository_build.uuid,
|
||||
pathargs=['build', repository_build.uuid])
|
||||
|
||||
if c_type not in self._mime_processors:
|
||||
log_appender('error', build_logs.PHASE)
|
||||
repository_build.phase = 'error'
|
||||
repository_build.save()
|
||||
message = 'Unknown mime-type: %s' % c_type
|
||||
log_appender(message, build_logs.ERROR)
|
||||
spawn_failure(message, event_data)
|
||||
raise JobException(message)
|
||||
|
||||
# Try to build the build directory package from the buildpack.
|
||||
log_appender('unpacking', build_logs.PHASE)
|
||||
repository_build.phase = 'unpacking'
|
||||
repository_build.save()
|
||||
|
||||
build_dir = None
|
||||
try:
|
||||
build_dir = self._mime_processors[c_type](docker_resource)
|
||||
except Exception as ex:
|
||||
cur_message = ex.message or 'Error while unpacking build package'
|
||||
log_appender(cur_message, build_logs.ERROR)
|
||||
spawn_failure(cur_message, event_data)
|
||||
raise JobException(cur_message)
|
||||
|
||||
# Start the build process.
|
||||
try:
|
||||
with DockerfileBuildContext(build_dir, build_subdir, repo, tag_names, access_token,
|
||||
|
@ -599,6 +614,7 @@ class DockerfileBuildWorker(Worker):
|
|||
|
||||
except WorkerUnhealthyException as exc:
|
||||
# Spawn a notification that the build has failed.
|
||||
log_appender('Worker has become unhealthy. Will retry shortly.', build_logs.ERROR)
|
||||
spawn_failure(exc.message, event_data)
|
||||
|
||||
# Raise the exception to the queue.
|
||||
|
|
|
@ -1,41 +0,0 @@
|
|||
import logging
|
||||
import argparse
|
||||
import requests
|
||||
import json
|
||||
|
||||
from app import webhook_queue
|
||||
from workers.worker import Worker
|
||||
|
||||
|
||||
root_logger = logging.getLogger('')
|
||||
root_logger.setLevel(logging.DEBUG)
|
||||
|
||||
FORMAT = '%(asctime)-15s - %(levelname)s - %(pathname)s - %(funcName)s - %(message)s'
|
||||
formatter = logging.Formatter(FORMAT)
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class WebhookWorker(Worker):
|
||||
def process_queue_item(self, job_details):
|
||||
url = job_details['url']
|
||||
payload = job_details['payload']
|
||||
headers = {'Content-type': 'application/json'}
|
||||
|
||||
try:
|
||||
resp = requests.post(url, data=json.dumps(payload), headers=headers)
|
||||
if resp.status_code/100 != 2:
|
||||
logger.error('%s response for webhook to url: %s' % (resp.status_code,
|
||||
url))
|
||||
return False
|
||||
except requests.exceptions.RequestException as ex:
|
||||
logger.exception('Webhook was unable to be sent: %s' % ex.message)
|
||||
return False
|
||||
|
||||
return True
|
||||
|
||||
logging.config.fileConfig('conf/logging.conf', disable_existing_loggers=False)
|
||||
|
||||
worker = WebhookWorker(webhook_queue, poll_period_seconds=15,
|
||||
reservation_seconds=3600)
|
||||
worker.start()
|
|
@ -102,8 +102,8 @@ class Worker(object):
|
|||
logger.debug('Running watchdog.')
|
||||
try:
|
||||
self.watchdog()
|
||||
except WorkerUnhealthyException:
|
||||
logger.error('The worker has encountered an error and will not take new jobs.')
|
||||
except WorkerUnhealthyException as exc:
|
||||
logger.error('The worker has encountered an error via watchdog and will not take new jobs: %s' % exc.message)
|
||||
self.mark_current_incomplete(restore_retry=True)
|
||||
self._stop.set()
|
||||
|
||||
|
@ -133,10 +133,10 @@ class Worker(object):
|
|||
logger.warning('An error occurred processing request: %s', current_queue_item.body)
|
||||
self.mark_current_incomplete(restore_retry=False)
|
||||
|
||||
except WorkerUnhealthyException:
|
||||
logger.error('The worker has encountered an error and will not take new jobs. Job is being requeued.')
|
||||
self._stop.set()
|
||||
except WorkerUnhealthyException as exc:
|
||||
logger.error('The worker has encountered an error via the job and will not take new jobs: %s' % exc.message)
|
||||
self.mark_current_incomplete(restore_retry=True)
|
||||
self._stop.set()
|
||||
|
||||
finally:
|
||||
# Close the db handle periodically
|
||||
|
|
Reference in a new issue