Merge remote-tracking branch 'origin/redalert'
Conflicts: app.py
This commit is contained in:
commit
0372013f70
46 changed files with 2432 additions and 432 deletions
|
@ -36,6 +36,9 @@ ADD conf/init/runmigration.sh /etc/my_init.d/
|
||||||
ADD conf/init/gunicorn /etc/service/gunicorn
|
ADD conf/init/gunicorn /etc/service/gunicorn
|
||||||
ADD conf/init/nginx /etc/service/nginx
|
ADD conf/init/nginx /etc/service/nginx
|
||||||
ADD conf/init/diffsworker /etc/service/diffsworker
|
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
|
ADD conf/init/webhookworker /etc/service/webhookworker
|
||||||
|
|
||||||
# Download any external libs.
|
# Download any external libs.
|
||||||
|
|
6
app.py
6
app.py
|
@ -79,8 +79,14 @@ tf = app.config['DB_TRANSACTION_FACTORY']
|
||||||
image_diff_queue = WorkQueue(app.config['DIFFS_QUEUE_NAME'], tf)
|
image_diff_queue = WorkQueue(app.config['DIFFS_QUEUE_NAME'], tf)
|
||||||
dockerfile_build_queue = WorkQueue(app.config['DOCKERFILE_BUILD_QUEUE_NAME'], tf,
|
dockerfile_build_queue = WorkQueue(app.config['DOCKERFILE_BUILD_QUEUE_NAME'], tf,
|
||||||
reporter=queue_metrics.report)
|
reporter=queue_metrics.report)
|
||||||
|
notification_queue = WorkQueue(app.config['NOTIFICATION_QUEUE_NAME'], tf)
|
||||||
|
|
||||||
|
# TODO: Remove this in the prod push following the notifications change.
|
||||||
webhook_queue = WorkQueue(app.config['WEBHOOK_QUEUE_NAME'], tf)
|
webhook_queue = WorkQueue(app.config['WEBHOOK_QUEUE_NAME'], tf)
|
||||||
|
|
||||||
database.configure(app.config)
|
database.configure(app.config)
|
||||||
model.config.app_config = app.config
|
model.config.app_config = app.config
|
||||||
model.config.store = storage
|
model.config.store = storage
|
||||||
|
|
||||||
|
def get_app_url():
|
||||||
|
return '%s://%s' % (app.config['PREFERRED_URL_SCHEME'], app.config['SERVER_HOSTNAME'])
|
||||||
|
|
2
conf/init/notificationworker/log/run
Executable file
2
conf/init/notificationworker/log/run
Executable file
|
@ -0,0 +1,2 @@
|
||||||
|
#!/bin/sh
|
||||||
|
exec svlogd -t /var/log/notificationworker/
|
8
conf/init/notificationworker/run
Executable file
8
conf/init/notificationworker/run
Executable file
|
@ -0,0 +1,8 @@
|
||||||
|
#! /bin/bash
|
||||||
|
|
||||||
|
echo 'Starting notification worker'
|
||||||
|
|
||||||
|
cd /
|
||||||
|
venv/bin/python -m workers.notificationworker
|
||||||
|
|
||||||
|
echo 'Notification worker exited'
|
|
@ -121,10 +121,13 @@ class DefaultConfig(object):
|
||||||
with open(tag_path) as tag_svg:
|
with open(tag_path) as tag_svg:
|
||||||
STATUS_TAGS[tag_name] = tag_svg.read()
|
STATUS_TAGS[tag_name] = tag_svg.read()
|
||||||
|
|
||||||
WEBHOOK_QUEUE_NAME = 'webhook'
|
NOTIFICATION_QUEUE_NAME = 'notification'
|
||||||
DIFFS_QUEUE_NAME = 'imagediff'
|
DIFFS_QUEUE_NAME = 'imagediff'
|
||||||
DOCKERFILE_BUILD_QUEUE_NAME = 'dockerfilebuild'
|
DOCKERFILE_BUILD_QUEUE_NAME = 'dockerfilebuild'
|
||||||
|
|
||||||
|
# TODO: Remove this in the prod push following the notifications change.
|
||||||
|
WEBHOOK_QUEUE_NAME = 'webhook'
|
||||||
|
|
||||||
# Super user config. Note: This MUST BE an empty list for the default config.
|
# Super user config. Note: This MUST BE an empty list for the default config.
|
||||||
SUPER_USERS = []
|
SUPER_USERS = []
|
||||||
|
|
||||||
|
|
|
@ -187,12 +187,6 @@ class PermissionPrototype(BaseModel):
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
class Webhook(BaseModel):
|
|
||||||
public_id = CharField(default=random_string_generator(length=64),
|
|
||||||
unique=True, index=True)
|
|
||||||
repository = ForeignKeyField(Repository)
|
|
||||||
parameters = TextField()
|
|
||||||
|
|
||||||
|
|
||||||
class AccessToken(BaseModel):
|
class AccessToken(BaseModel):
|
||||||
friendly_name = CharField(null=True)
|
friendly_name = CharField(null=True)
|
||||||
|
@ -369,11 +363,46 @@ class Notification(BaseModel):
|
||||||
target = ForeignKeyField(User, index=True)
|
target = ForeignKeyField(User, index=True)
|
||||||
metadata_json = TextField(default='{}')
|
metadata_json = TextField(default='{}')
|
||||||
created = DateTimeField(default=datetime.now, index=True)
|
created = DateTimeField(default=datetime.now, index=True)
|
||||||
|
dismissed = BooleanField(default=False)
|
||||||
|
|
||||||
|
|
||||||
|
class ExternalNotificationEvent(BaseModel):
|
||||||
|
name = CharField(index=True, unique=True)
|
||||||
|
|
||||||
|
|
||||||
|
class ExternalNotificationMethod(BaseModel):
|
||||||
|
name = CharField(index=True, unique=True)
|
||||||
|
|
||||||
|
|
||||||
|
class RepositoryNotification(BaseModel):
|
||||||
|
uuid = CharField(default=uuid_generator, index=True)
|
||||||
|
repository = ForeignKeyField(Repository, index=True)
|
||||||
|
event = ForeignKeyField(ExternalNotificationEvent)
|
||||||
|
method = ForeignKeyField(ExternalNotificationMethod)
|
||||||
|
config_json = TextField()
|
||||||
|
|
||||||
|
|
||||||
|
class RepositoryAuthorizedEmail(BaseModel):
|
||||||
|
repository = ForeignKeyField(Repository, index=True)
|
||||||
|
email = CharField()
|
||||||
|
code = CharField(default=random_string_generator(), unique=True, index=True)
|
||||||
|
confirmed = BooleanField(default=False)
|
||||||
|
|
||||||
|
class Meta:
|
||||||
|
database = db
|
||||||
|
read_slaves = (read_slave,)
|
||||||
|
indexes = (
|
||||||
|
# create a unique index on email and repository
|
||||||
|
(('email', 'repository'), True),
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
all_models = [User, Repository, Image, AccessToken, Role, RepositoryPermission, Visibility,
|
all_models = [User, Repository, Image, AccessToken, Role, RepositoryPermission, Visibility,
|
||||||
RepositoryTag, EmailConfirmation, FederatedLogin, LoginService, QueueItem,
|
RepositoryTag, EmailConfirmation, FederatedLogin, LoginService, QueueItem,
|
||||||
RepositoryBuild, Team, TeamMember, TeamRole, Webhook, LogEntryKind, LogEntry,
|
RepositoryBuild, Team, TeamMember, TeamRole, LogEntryKind, LogEntry,
|
||||||
PermissionPrototype, ImageStorage, BuildTriggerService, RepositoryBuildTrigger,
|
PermissionPrototype, ImageStorage, BuildTriggerService, RepositoryBuildTrigger,
|
||||||
OAuthApplication, OAuthAuthorizationCode, OAuthAccessToken, NotificationKind,
|
OAuthApplication, OAuthAuthorizationCode, OAuthAccessToken, NotificationKind,
|
||||||
Notification, ImageStorageLocation, ImageStoragePlacement]
|
Notification, ImageStorageLocation, ImageStoragePlacement,
|
||||||
|
ExternalNotificationEvent, ExternalNotificationMethod, RepositoryNotification,
|
||||||
|
RepositoryAuthorizedEmail]
|
||||||
|
|
|
@ -0,0 +1,138 @@
|
||||||
|
"""Prepare the database for the new notifications system
|
||||||
|
|
||||||
|
Revision ID: 325a4d7c79d9
|
||||||
|
Revises: 4b7ef0c7bdb2
|
||||||
|
Create Date: 2014-07-31 13:08:18.667393
|
||||||
|
|
||||||
|
"""
|
||||||
|
|
||||||
|
# revision identifiers, used by Alembic.
|
||||||
|
revision = '325a4d7c79d9'
|
||||||
|
down_revision = '4b7ef0c7bdb2'
|
||||||
|
|
||||||
|
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)
|
||||||
|
|
||||||
|
### commands auto generated by Alembic - please adjust! ###
|
||||||
|
op.create_table('externalnotificationmethod',
|
||||||
|
sa.Column('id', sa.Integer(), nullable=False),
|
||||||
|
sa.Column('name', sa.String(length=255), nullable=False),
|
||||||
|
sa.PrimaryKeyConstraint('id')
|
||||||
|
)
|
||||||
|
op.create_index('externalnotificationmethod_name', 'externalnotificationmethod', ['name'], unique=True)
|
||||||
|
op.bulk_insert(schema.tables['externalnotificationmethod'],
|
||||||
|
[
|
||||||
|
{'id':1, 'name':'quay_notification'},
|
||||||
|
{'id':2, 'name':'email'},
|
||||||
|
{'id':3, 'name':'webhook'},
|
||||||
|
])
|
||||||
|
op.create_table('externalnotificationevent',
|
||||||
|
sa.Column('id', sa.Integer(), nullable=False),
|
||||||
|
sa.Column('name', sa.String(length=255), nullable=False),
|
||||||
|
sa.PrimaryKeyConstraint('id')
|
||||||
|
)
|
||||||
|
op.create_index('externalnotificationevent_name', 'externalnotificationevent', ['name'], unique=True)
|
||||||
|
op.bulk_insert(schema.tables['externalnotificationevent'],
|
||||||
|
[
|
||||||
|
{'id':1, 'name':'repo_push'},
|
||||||
|
{'id':2, 'name':'build_queued'},
|
||||||
|
{'id':3, 'name':'build_start'},
|
||||||
|
{'id':4, 'name':'build_success'},
|
||||||
|
{'id':5, 'name':'build_failure'},
|
||||||
|
])
|
||||||
|
op.create_table('repositoryauthorizedemail',
|
||||||
|
sa.Column('id', sa.Integer(), nullable=False),
|
||||||
|
sa.Column('repository_id', sa.Integer(), nullable=False),
|
||||||
|
sa.Column('email', sa.String(length=255), nullable=False),
|
||||||
|
sa.Column('code', sa.String(length=255), nullable=False),
|
||||||
|
sa.Column('confirmed', sa.Boolean(), nullable=False),
|
||||||
|
sa.ForeignKeyConstraint(['repository_id'], ['repository.id'], ),
|
||||||
|
sa.PrimaryKeyConstraint('id')
|
||||||
|
)
|
||||||
|
op.create_index('repositoryauthorizedemail_code', 'repositoryauthorizedemail', ['code'], unique=True)
|
||||||
|
op.create_index('repositoryauthorizedemail_email_repository_id', 'repositoryauthorizedemail', ['email', 'repository_id'], unique=True)
|
||||||
|
op.create_index('repositoryauthorizedemail_repository_id', 'repositoryauthorizedemail', ['repository_id'], unique=False)
|
||||||
|
op.create_table('repositorynotification',
|
||||||
|
sa.Column('id', sa.Integer(), nullable=False),
|
||||||
|
sa.Column('uuid', sa.String(length=255), nullable=False),
|
||||||
|
sa.Column('repository_id', sa.Integer(), nullable=False),
|
||||||
|
sa.Column('event_id', sa.Integer(), nullable=False),
|
||||||
|
sa.Column('method_id', sa.Integer(), nullable=False),
|
||||||
|
sa.Column('config_json', sa.Text(), nullable=False),
|
||||||
|
sa.ForeignKeyConstraint(['event_id'], ['externalnotificationevent.id'], ),
|
||||||
|
sa.ForeignKeyConstraint(['method_id'], ['externalnotificationmethod.id'], ),
|
||||||
|
sa.ForeignKeyConstraint(['repository_id'], ['repository.id'], ),
|
||||||
|
sa.PrimaryKeyConstraint('id')
|
||||||
|
)
|
||||||
|
op.create_index('repositorynotification_event_id', 'repositorynotification', ['event_id'], unique=False)
|
||||||
|
op.create_index('repositorynotification_method_id', 'repositorynotification', ['method_id'], unique=False)
|
||||||
|
op.create_index('repositorynotification_repository_id', 'repositorynotification', ['repository_id'], unique=False)
|
||||||
|
op.create_index('repositorynotification_uuid', 'repositorynotification', ['uuid'], unique=False)
|
||||||
|
op.add_column(u'notification', sa.Column('dismissed', sa.Boolean(), nullable=False))
|
||||||
|
|
||||||
|
# Manually add the new notificationkind types
|
||||||
|
op.bulk_insert(schema.tables['notificationkind'],
|
||||||
|
[
|
||||||
|
{'id':5, 'name':'repo_push'},
|
||||||
|
{'id':6, 'name':'build_queued'},
|
||||||
|
{'id':7, 'name':'build_start'},
|
||||||
|
{'id':8, 'name':'build_success'},
|
||||||
|
{'id':9, 'name':'build_failure'},
|
||||||
|
])
|
||||||
|
|
||||||
|
### end Alembic commands ###
|
||||||
|
|
||||||
|
|
||||||
|
def downgrade():
|
||||||
|
schema = gen_sqlalchemy_metadata(all_models)
|
||||||
|
|
||||||
|
### commands auto generated by Alembic - please adjust! ###
|
||||||
|
op.drop_column(u'notification', 'dismissed')
|
||||||
|
op.drop_index('repositorynotification_uuid', table_name='repositorynotification')
|
||||||
|
op.drop_index('repositorynotification_repository_id', table_name='repositorynotification')
|
||||||
|
op.drop_index('repositorynotification_method_id', table_name='repositorynotification')
|
||||||
|
op.drop_index('repositorynotification_event_id', table_name='repositorynotification')
|
||||||
|
op.drop_table('repositorynotification')
|
||||||
|
op.drop_index('repositoryauthorizedemail_repository_id', table_name='repositoryauthorizedemail')
|
||||||
|
op.drop_index('repositoryauthorizedemail_email_repository_id', table_name='repositoryauthorizedemail')
|
||||||
|
op.drop_index('repositoryauthorizedemail_code', table_name='repositoryauthorizedemail')
|
||||||
|
op.drop_table('repositoryauthorizedemail')
|
||||||
|
op.drop_index('externalnotificationevent_name', table_name='externalnotificationevent')
|
||||||
|
op.drop_table('externalnotificationevent')
|
||||||
|
op.drop_index('externalnotificationmethod_name', table_name='externalnotificationmethod')
|
||||||
|
op.drop_table('externalnotificationmethod')
|
||||||
|
|
||||||
|
# Manually remove the notificationkind types
|
||||||
|
notificationkind = schema.tables['notificationkind']
|
||||||
|
op.execute(
|
||||||
|
(notificationkind.delete()
|
||||||
|
.where(notificationkind.c.name == op.inline_literal('repo_push')))
|
||||||
|
|
||||||
|
)
|
||||||
|
op.execute(
|
||||||
|
(notificationkind.delete()
|
||||||
|
.where(notificationkind.c.name == op.inline_literal('build_queued')))
|
||||||
|
|
||||||
|
)
|
||||||
|
op.execute(
|
||||||
|
(notificationkind.delete()
|
||||||
|
.where(notificationkind.c.name == op.inline_literal('build_start')))
|
||||||
|
|
||||||
|
)
|
||||||
|
op.execute(
|
||||||
|
(notificationkind.delete()
|
||||||
|
.where(notificationkind.c.name == op.inline_literal('build_success')))
|
||||||
|
|
||||||
|
)
|
||||||
|
op.execute(
|
||||||
|
(notificationkind.delete()
|
||||||
|
.where(notificationkind.c.name == op.inline_literal('build_failure')))
|
||||||
|
|
||||||
|
)
|
||||||
|
### end Alembic commands ###
|
|
@ -0,0 +1,31 @@
|
||||||
|
"""Migrate existing webhooks to notifications.
|
||||||
|
|
||||||
|
Revision ID: 47670cbeced
|
||||||
|
Revises: 325a4d7c79d9
|
||||||
|
Create Date: 2014-07-31 13:49:38.332807
|
||||||
|
Hand Edited By Joseph Schorr
|
||||||
|
|
||||||
|
"""
|
||||||
|
|
||||||
|
# revision identifiers, used by Alembic.
|
||||||
|
revision = '47670cbeced'
|
||||||
|
down_revision = '325a4d7c79d9'
|
||||||
|
|
||||||
|
from alembic import op, context
|
||||||
|
import sqlalchemy as sa
|
||||||
|
|
||||||
|
def get_id(query):
|
||||||
|
conn = op.get_bind()
|
||||||
|
return list(conn.execute(query, ()).fetchall())[0][0]
|
||||||
|
|
||||||
|
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')
|
||||||
|
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')
|
||||||
|
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))
|
|
@ -56,7 +56,7 @@ class InvalidRepositoryBuildException(DataModelException):
|
||||||
pass
|
pass
|
||||||
|
|
||||||
|
|
||||||
class InvalidWebhookException(DataModelException):
|
class InvalidNotificationException(DataModelException):
|
||||||
pass
|
pass
|
||||||
|
|
||||||
|
|
||||||
|
@ -836,6 +836,13 @@ def get_repository_for_resource(resource_key):
|
||||||
return None
|
return None
|
||||||
|
|
||||||
|
|
||||||
|
def lookup_repository(repo_id):
|
||||||
|
try:
|
||||||
|
return Repository.get(Repository.id == repo_id)
|
||||||
|
except Repository.DoesNotExist:
|
||||||
|
return None
|
||||||
|
|
||||||
|
|
||||||
def get_repository(namespace_name, repository_name):
|
def get_repository(namespace_name, repository_name):
|
||||||
try:
|
try:
|
||||||
return Repository.get(Repository.name == repository_name,
|
return Repository.get(Repository.name == repository_name,
|
||||||
|
@ -1533,32 +1540,42 @@ def get_pull_credentials(robotname):
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
def create_webhook(repo, params_obj):
|
def create_repo_notification(repo, event_name, method_name, config):
|
||||||
return Webhook.create(repository=repo, parameters=json.dumps(params_obj))
|
event = ExternalNotificationEvent.get(ExternalNotificationEvent.name == event_name)
|
||||||
|
method = ExternalNotificationMethod.get(ExternalNotificationMethod.name == method_name)
|
||||||
|
|
||||||
|
return RepositoryNotification.create(repository=repo, event=event, method=method,
|
||||||
|
config_json=json.dumps(config))
|
||||||
|
|
||||||
|
|
||||||
def get_webhook(namespace_name, repository_name, public_id):
|
def get_repo_notification(namespace_name, repository_name, uuid):
|
||||||
joined = Webhook.select().join(Repository)
|
joined = RepositoryNotification.select().join(Repository)
|
||||||
found = list(joined.where(Repository.namespace == namespace_name,
|
found = list(joined.where(Repository.namespace == namespace_name,
|
||||||
Repository.name == repository_name,
|
Repository.name == repository_name,
|
||||||
Webhook.public_id == public_id))
|
RepositoryNotification.uuid == uuid))
|
||||||
|
|
||||||
if not found:
|
if not found:
|
||||||
raise InvalidWebhookException('No webhook found with id: %s' % public_id)
|
raise InvalidNotificationException('No repository notification found with id: %s' % uuid)
|
||||||
|
|
||||||
return found[0]
|
return found[0]
|
||||||
|
|
||||||
|
|
||||||
def list_webhooks(namespace_name, repository_name):
|
def delete_repo_notification(namespace_name, repository_name, uuid):
|
||||||
joined = Webhook.select().join(Repository)
|
found = get_repo_notification(namespace_name, repository_name, uuid)
|
||||||
return joined.where(Repository.namespace == namespace_name,
|
found.delete_instance()
|
||||||
|
return found
|
||||||
|
|
||||||
|
|
||||||
|
def list_repo_notifications(namespace_name, repository_name, event_name=None):
|
||||||
|
joined = RepositoryNotification.select().join(Repository)
|
||||||
|
where = joined.where(Repository.namespace == namespace_name,
|
||||||
Repository.name == repository_name)
|
Repository.name == repository_name)
|
||||||
|
|
||||||
|
if event_name:
|
||||||
|
event = ExternalNotificationEvent.get(ExternalNotificationEvent.name == event_name)
|
||||||
|
where = where.where(RepositoryNotification.event == event)
|
||||||
|
|
||||||
def delete_webhook(namespace_name, repository_name, public_id):
|
return where
|
||||||
webhook = get_webhook(namespace_name, repository_name, public_id)
|
|
||||||
webhook.delete_instance()
|
|
||||||
return webhook
|
|
||||||
|
|
||||||
|
|
||||||
def list_logs(start_time, end_time, performer=None, repository=None, namespace=None):
|
def list_logs(start_time, end_time, performer=None, repository=None, namespace=None):
|
||||||
|
@ -1646,7 +1663,15 @@ def create_unique_notification(kind_name, target, metadata={}):
|
||||||
create_notification(kind_name, target, metadata)
|
create_notification(kind_name, target, metadata)
|
||||||
|
|
||||||
|
|
||||||
def list_notifications(user, kind_name=None):
|
def lookup_notification(user, uuid):
|
||||||
|
results = list(list_notifications(user, id_filter=uuid, include_dismissed=True))
|
||||||
|
if not results:
|
||||||
|
return None
|
||||||
|
|
||||||
|
return results[0]
|
||||||
|
|
||||||
|
|
||||||
|
def list_notifications(user, kind_name=None, id_filter=None, include_dismissed=False):
|
||||||
Org = User.alias()
|
Org = User.alias()
|
||||||
AdminTeam = Team.alias()
|
AdminTeam = Team.alias()
|
||||||
AdminTeamMember = TeamMember.alias()
|
AdminTeamMember = TeamMember.alias()
|
||||||
|
@ -1670,12 +1695,20 @@ def list_notifications(user, kind_name=None):
|
||||||
.order_by(Notification.created)
|
.order_by(Notification.created)
|
||||||
.desc())
|
.desc())
|
||||||
|
|
||||||
|
if not include_dismissed:
|
||||||
|
query = query.switch(Notification).where(Notification.dismissed == False)
|
||||||
|
|
||||||
if kind_name:
|
if kind_name:
|
||||||
query = (query
|
query = (query
|
||||||
.switch(Notification)
|
.switch(Notification)
|
||||||
.join(NotificationKind)
|
.join(NotificationKind)
|
||||||
.where(NotificationKind.name == kind_name))
|
.where(NotificationKind.name == kind_name))
|
||||||
|
|
||||||
|
if id_filter:
|
||||||
|
query = (query
|
||||||
|
.switch(Notification)
|
||||||
|
.where(Notification.uuid == id_filter))
|
||||||
|
|
||||||
return query
|
return query
|
||||||
|
|
||||||
|
|
||||||
|
@ -1710,3 +1743,39 @@ def check_health():
|
||||||
return found_count > 0
|
return found_count > 0
|
||||||
except:
|
except:
|
||||||
return False
|
return False
|
||||||
|
|
||||||
|
def get_email_authorized_for_repo(namespace, repository, email):
|
||||||
|
found = list(RepositoryAuthorizedEmail.select()
|
||||||
|
.join(Repository)
|
||||||
|
.where(Repository.namespace == namespace,
|
||||||
|
Repository.name == repository,
|
||||||
|
RepositoryAuthorizedEmail.email == email)
|
||||||
|
.switch(RepositoryAuthorizedEmail)
|
||||||
|
.limit(1))
|
||||||
|
if not found or len(found) < 1:
|
||||||
|
return None
|
||||||
|
|
||||||
|
return found[0]
|
||||||
|
|
||||||
|
|
||||||
|
def create_email_authorization_for_repo(namespace_name, repository_name, email):
|
||||||
|
try:
|
||||||
|
repo = Repository.get(Repository.name == repository_name,
|
||||||
|
Repository.namespace == namespace_name)
|
||||||
|
except Repository.DoesNotExist:
|
||||||
|
raise DataModelException('Invalid repository %s/%s' %
|
||||||
|
(namespace_name, repository_name))
|
||||||
|
|
||||||
|
return RepositoryAuthorizedEmail.create(repository=repo, email=email, confirmed=False)
|
||||||
|
|
||||||
|
|
||||||
|
def confirm_email_authorization_for_repo(code):
|
||||||
|
try:
|
||||||
|
found = RepositoryAuthorizedEmail.get(RepositoryAuthorizedEmail.code == code)
|
||||||
|
except RepositoryAuthorizedEmail.DoesNotExist:
|
||||||
|
raise DataModelException('Invalid confirmation code.')
|
||||||
|
|
||||||
|
found.confirmed = True
|
||||||
|
found.save()
|
||||||
|
|
||||||
|
return found
|
||||||
|
|
|
@ -307,6 +307,8 @@ import endpoints.api.organization
|
||||||
import endpoints.api.permission
|
import endpoints.api.permission
|
||||||
import endpoints.api.prototype
|
import endpoints.api.prototype
|
||||||
import endpoints.api.repository
|
import endpoints.api.repository
|
||||||
|
import endpoints.api.repositorynotification
|
||||||
|
import endpoints.api.repoemail
|
||||||
import endpoints.api.repotoken
|
import endpoints.api.repotoken
|
||||||
import endpoints.api.robot
|
import endpoints.api.robot
|
||||||
import endpoints.api.search
|
import endpoints.api.search
|
||||||
|
@ -315,4 +317,3 @@ import endpoints.api.tag
|
||||||
import endpoints.api.team
|
import endpoints.api.team
|
||||||
import endpoints.api.trigger
|
import endpoints.api.trigger
|
||||||
import endpoints.api.user
|
import endpoints.api.user
|
||||||
import endpoints.api.webhook
|
|
||||||
|
|
56
endpoints/api/repoemail.py
Normal file
56
endpoints/api/repoemail.py
Normal file
|
@ -0,0 +1,56 @@
|
||||||
|
import logging
|
||||||
|
|
||||||
|
from flask import request, abort
|
||||||
|
|
||||||
|
from endpoints.api import (resource, nickname, require_repo_admin, RepositoryParamResource,
|
||||||
|
log_action, validate_json_request, NotFound, internal_only)
|
||||||
|
|
||||||
|
from app import tf
|
||||||
|
from data import model
|
||||||
|
from data.database import db
|
||||||
|
from util.useremails import send_repo_authorization_email
|
||||||
|
|
||||||
|
import features
|
||||||
|
|
||||||
|
|
||||||
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
def record_view(record):
|
||||||
|
return {
|
||||||
|
'email': record.email,
|
||||||
|
'repository': record.repository.name,
|
||||||
|
'namespace': record.repository.namespace,
|
||||||
|
'confirmed': record.confirmed
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
@internal_only
|
||||||
|
@resource('/v1/repository/<repopath:repository>/authorizedemail/<email>')
|
||||||
|
class RepositoryAuthorizedEmail(RepositoryParamResource):
|
||||||
|
""" Resource for checking and authorizing e-mail addresses to receive repo notifications. """
|
||||||
|
@require_repo_admin
|
||||||
|
@nickname('checkRepoEmailAuthorized')
|
||||||
|
def get(self, namespace, repository, email):
|
||||||
|
""" Checks to see if the given e-mail address is authorized on this repository. """
|
||||||
|
record = model.get_email_authorized_for_repo(namespace, repository, email)
|
||||||
|
if not record:
|
||||||
|
abort(404)
|
||||||
|
|
||||||
|
return record_view(record)
|
||||||
|
|
||||||
|
|
||||||
|
@require_repo_admin
|
||||||
|
@nickname('sendAuthorizeRepoEmail')
|
||||||
|
def post(self, namespace, repository, email):
|
||||||
|
""" Starts the authorization process for an e-mail address on a repository. """
|
||||||
|
|
||||||
|
with tf(db):
|
||||||
|
record = model.get_email_authorized_for_repo(namespace, repository, email)
|
||||||
|
if record and record.confirmed:
|
||||||
|
return record_view(record)
|
||||||
|
|
||||||
|
if not record:
|
||||||
|
record = model.create_email_authorization_for_repo(namespace, repository, email)
|
||||||
|
|
||||||
|
send_repo_authorization_email(namespace, repository, email, record.code)
|
||||||
|
return record_view(record)
|
142
endpoints/api/repositorynotification.py
Normal file
142
endpoints/api/repositorynotification.py
Normal file
|
@ -0,0 +1,142 @@
|
||||||
|
import json
|
||||||
|
|
||||||
|
from flask import request, abort
|
||||||
|
|
||||||
|
from app import notification_queue
|
||||||
|
from endpoints.api import (RepositoryParamResource, nickname, resource, require_repo_admin,
|
||||||
|
log_action, validate_json_request, api, NotFound, request_error)
|
||||||
|
from endpoints.notificationevent import NotificationEvent
|
||||||
|
from endpoints.notificationmethod import (NotificationMethod,
|
||||||
|
CannotValidateNotificationMethodException)
|
||||||
|
from endpoints.notificationhelper import build_notification_data
|
||||||
|
from data import model
|
||||||
|
|
||||||
|
|
||||||
|
def notification_view(notification):
|
||||||
|
config = {}
|
||||||
|
try:
|
||||||
|
config = json.loads(notification.config_json)
|
||||||
|
except:
|
||||||
|
config = {}
|
||||||
|
|
||||||
|
return {
|
||||||
|
'uuid': notification.uuid,
|
||||||
|
'event': notification.event.name,
|
||||||
|
'method': notification.method.name,
|
||||||
|
'config': config
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
@resource('/v1/repository/<repopath:repository>/notification/')
|
||||||
|
class RepositoryNotificationList(RepositoryParamResource):
|
||||||
|
""" Resource for dealing with listing and creating notifications on a repository. """
|
||||||
|
schemas = {
|
||||||
|
'NotificationCreateRequest': {
|
||||||
|
'id': 'NotificationCreateRequest',
|
||||||
|
'type': 'object',
|
||||||
|
'description': 'Information for creating a notification on a repository',
|
||||||
|
'required': [
|
||||||
|
'event',
|
||||||
|
'method',
|
||||||
|
'config'
|
||||||
|
],
|
||||||
|
'properties': {
|
||||||
|
'event': {
|
||||||
|
'type': 'string',
|
||||||
|
'description': 'The event on which the notification will respond',
|
||||||
|
},
|
||||||
|
'method': {
|
||||||
|
'type': 'string',
|
||||||
|
'description': 'The method of notification (such as email or web callback)',
|
||||||
|
},
|
||||||
|
'config': {
|
||||||
|
'type': 'object',
|
||||||
|
'description': 'JSON config information for the specific method of notification'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
@require_repo_admin
|
||||||
|
@nickname('createRepoNotification')
|
||||||
|
@validate_json_request('NotificationCreateRequest')
|
||||||
|
def post(self, namespace, repository):
|
||||||
|
""" Create a new notification for the specified repository. """
|
||||||
|
repo = model.get_repository(namespace, repository)
|
||||||
|
json = request.get_json()
|
||||||
|
|
||||||
|
method_handler = NotificationMethod.get_method(json['method'])
|
||||||
|
if not method_handler:
|
||||||
|
raise request_error(message='Unknown method')
|
||||||
|
|
||||||
|
try:
|
||||||
|
method_handler.validate(repo, json['config'])
|
||||||
|
except CannotValidateNotificationMethodException as ex:
|
||||||
|
raise request_error(message=ex.message)
|
||||||
|
|
||||||
|
notification = model.create_repo_notification(repo, json['event'], json['method'],
|
||||||
|
json['config'])
|
||||||
|
|
||||||
|
resp = notification_view(notification)
|
||||||
|
log_action('add_repo_notification', namespace,
|
||||||
|
{'repo': repository, 'notification_id': notification.uuid,
|
||||||
|
'event': json['event'], 'method': json['method']},
|
||||||
|
repo=repo)
|
||||||
|
return resp, 201
|
||||||
|
|
||||||
|
@require_repo_admin
|
||||||
|
@nickname('listRepoNotifications')
|
||||||
|
def get(self, namespace, repository):
|
||||||
|
""" List the notifications for the specified repository. """
|
||||||
|
notifications = model.list_repo_notifications(namespace, repository)
|
||||||
|
return {
|
||||||
|
'notifications': [notification_view(n) for n in notifications]
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
@resource('/v1/repository/<repopath:repository>/notification/<uuid>')
|
||||||
|
class RepositoryNotification(RepositoryParamResource):
|
||||||
|
""" Resource for dealing with specific notifications. """
|
||||||
|
@require_repo_admin
|
||||||
|
@nickname('getRepoNotification')
|
||||||
|
def get(self, namespace, repository, uuid):
|
||||||
|
""" Get information for the specified notification. """
|
||||||
|
try:
|
||||||
|
notification = model.get_repo_notification(namespace, repository, uuid)
|
||||||
|
except model.InvalidNotificationException:
|
||||||
|
raise NotFound()
|
||||||
|
|
||||||
|
return notification_view(notification)
|
||||||
|
|
||||||
|
@require_repo_admin
|
||||||
|
@nickname('deleteRepoNotification')
|
||||||
|
def delete(self, namespace, repository, uuid):
|
||||||
|
""" Deletes the specified notification. """
|
||||||
|
notification = model.delete_repo_notification(namespace, repository, uuid)
|
||||||
|
log_action('delete_repo_notification', namespace,
|
||||||
|
{'repo': repository, 'notification_id': uuid,
|
||||||
|
'event': notification.event.name, 'method': notification.method.name},
|
||||||
|
repo=model.get_repository(namespace, repository))
|
||||||
|
|
||||||
|
return 'No Content', 204
|
||||||
|
|
||||||
|
|
||||||
|
@resource('/v1/repository/<repopath:repository>/notification/<uuid>/test')
|
||||||
|
class TestRepositoryNotification(RepositoryParamResource):
|
||||||
|
""" Resource for queuing a test of a notification. """
|
||||||
|
@require_repo_admin
|
||||||
|
@nickname('testRepoNotification')
|
||||||
|
def post(self, namespace, repository, uuid):
|
||||||
|
""" Queues a test notification for this repository. """
|
||||||
|
try:
|
||||||
|
notification = model.get_repo_notification(namespace, repository, uuid)
|
||||||
|
except model.InvalidNotificationException:
|
||||||
|
raise NotFound()
|
||||||
|
|
||||||
|
event_info = NotificationEvent.get_event(notification.event.name)
|
||||||
|
sample_data = event_info.get_sample_data(repository=notification.repository)
|
||||||
|
notification_data = build_notification_data(notification, sample_data)
|
||||||
|
notification_queue.put([namespace, repository, notification.event.name],
|
||||||
|
json.dumps(notification_data))
|
||||||
|
|
||||||
|
return {}
|
|
@ -2,9 +2,11 @@ from endpoints.api import (ApiResource, parse_args, query_param, truthy_bool, ni
|
||||||
require_scope)
|
require_scope)
|
||||||
from data import model
|
from data import model
|
||||||
from auth.permissions import (OrganizationMemberPermission, ViewTeamPermission,
|
from auth.permissions import (OrganizationMemberPermission, ViewTeamPermission,
|
||||||
ReadRepositoryPermission, UserAdminPermission)
|
ReadRepositoryPermission, UserAdminPermission,
|
||||||
|
AdministerOrganizationPermission)
|
||||||
from auth.auth_context import get_authenticated_user
|
from auth.auth_context import get_authenticated_user
|
||||||
from auth import scopes
|
from auth import scopes
|
||||||
|
from util.gravatar import compute_hash
|
||||||
|
|
||||||
|
|
||||||
@resource('/v1/entities/<prefix>')
|
@resource('/v1/entities/<prefix>')
|
||||||
|
@ -14,10 +16,12 @@ class EntitySearch(ApiResource):
|
||||||
@query_param('namespace', 'Namespace to use when querying for org entities.', type=str,
|
@query_param('namespace', 'Namespace to use when querying for org entities.', type=str,
|
||||||
default='')
|
default='')
|
||||||
@query_param('includeTeams', 'Whether to include team names.', type=truthy_bool, default=False)
|
@query_param('includeTeams', 'Whether to include team names.', type=truthy_bool, default=False)
|
||||||
|
@query_param('includeOrgs', 'Whether to include orgs names.', type=truthy_bool, default=False)
|
||||||
@nickname('getMatchingEntities')
|
@nickname('getMatchingEntities')
|
||||||
def get(self, args, prefix):
|
def get(self, args, prefix):
|
||||||
""" Get a list of entities that match the specified prefix. """
|
""" Get a list of entities that match the specified prefix. """
|
||||||
teams = []
|
teams = []
|
||||||
|
org_data = []
|
||||||
|
|
||||||
namespace_name = args['namespace']
|
namespace_name = args['namespace']
|
||||||
robot_namespace = None
|
robot_namespace = None
|
||||||
|
@ -34,6 +38,15 @@ class EntitySearch(ApiResource):
|
||||||
if args['includeTeams']:
|
if args['includeTeams']:
|
||||||
teams = model.get_matching_teams(prefix, organization)
|
teams = model.get_matching_teams(prefix, organization)
|
||||||
|
|
||||||
|
if args['includeOrgs'] and AdministerOrganizationPermission(namespace_name) \
|
||||||
|
and namespace_name.startswith(prefix):
|
||||||
|
org_data = [{
|
||||||
|
'name': namespace_name,
|
||||||
|
'kind': 'org',
|
||||||
|
'is_org_member': True,
|
||||||
|
'gravatar': compute_hash(organization.email),
|
||||||
|
}]
|
||||||
|
|
||||||
except model.InvalidOrganizationException:
|
except model.InvalidOrganizationException:
|
||||||
# namespace name was a user
|
# namespace name was a user
|
||||||
user = get_authenticated_user()
|
user = get_authenticated_user()
|
||||||
|
@ -69,7 +82,7 @@ class EntitySearch(ApiResource):
|
||||||
user_data = [user_view(user) for user in users]
|
user_data = [user_view(user) for user in users]
|
||||||
|
|
||||||
return {
|
return {
|
||||||
'results': team_data + user_data
|
'results': team_data + user_data + org_data
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
|
@ -74,10 +74,12 @@ def user_view(user):
|
||||||
|
|
||||||
def notification_view(notification):
|
def notification_view(notification):
|
||||||
return {
|
return {
|
||||||
|
'id': notification.uuid,
|
||||||
'organization': notification.target.username if notification.target.organization else None,
|
'organization': notification.target.username if notification.target.organization else None,
|
||||||
'kind': notification.kind.name,
|
'kind': notification.kind.name,
|
||||||
'created': format_date(notification.created),
|
'created': format_date(notification.created),
|
||||||
'metadata': json.loads(notification.metadata_json),
|
'metadata': json.loads(notification.metadata_json),
|
||||||
|
'dismissed': notification.dismissed
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
@ -409,6 +411,46 @@ class UserNotificationList(ApiResource):
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
@resource('/v1/user/notifications/<uuid>')
|
||||||
|
@internal_only
|
||||||
|
class UserNotification(ApiResource):
|
||||||
|
schemas = {
|
||||||
|
'UpdateNotification': {
|
||||||
|
'id': 'UpdateNotification',
|
||||||
|
'type': 'object',
|
||||||
|
'description': 'Information for updating a notification',
|
||||||
|
'properties': {
|
||||||
|
'dismissed': {
|
||||||
|
'type': 'boolean',
|
||||||
|
'description': 'Whether the notification is dismissed by the user',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
@require_user_admin
|
||||||
|
@nickname('getUserNotification')
|
||||||
|
def get(self, uuid):
|
||||||
|
notification = model.lookup_notification(get_authenticated_user(), uuid)
|
||||||
|
if not notification:
|
||||||
|
raise NotFound()
|
||||||
|
|
||||||
|
return notification_view(notification)
|
||||||
|
|
||||||
|
@require_user_admin
|
||||||
|
@nickname('updateUserNotification')
|
||||||
|
@validate_json_request('UpdateNotification')
|
||||||
|
def put(self, uuid):
|
||||||
|
notification = model.lookup_notification(get_authenticated_user(), uuid)
|
||||||
|
if not notification:
|
||||||
|
raise NotFound()
|
||||||
|
|
||||||
|
notification.dismissed = request.get_json().get('dismissed', False)
|
||||||
|
notification.save()
|
||||||
|
|
||||||
|
return notification_view(notification)
|
||||||
|
|
||||||
|
|
||||||
def authorization_view(access_token):
|
def authorization_view(access_token):
|
||||||
oauth_app = access_token.application
|
oauth_app = access_token.application
|
||||||
return {
|
return {
|
||||||
|
|
|
@ -1,77 +0,0 @@
|
||||||
import json
|
|
||||||
|
|
||||||
from flask import request
|
|
||||||
|
|
||||||
from endpoints.api import (RepositoryParamResource, nickname, resource, require_repo_admin,
|
|
||||||
log_action, validate_json_request, api, NotFound)
|
|
||||||
from data import model
|
|
||||||
|
|
||||||
|
|
||||||
def webhook_view(webhook):
|
|
||||||
return {
|
|
||||||
'public_id': webhook.public_id,
|
|
||||||
'parameters': json.loads(webhook.parameters),
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
@resource('/v1/repository/<repopath:repository>/webhook/')
|
|
||||||
class WebhookList(RepositoryParamResource):
|
|
||||||
""" Resource for dealing with listing and creating webhooks. """
|
|
||||||
schemas = {
|
|
||||||
'WebhookCreateRequest': {
|
|
||||||
'id': 'WebhookCreateRequest',
|
|
||||||
'type': 'object',
|
|
||||||
'description': 'Arbitrary json.',
|
|
||||||
},
|
|
||||||
}
|
|
||||||
|
|
||||||
@require_repo_admin
|
|
||||||
@nickname('createWebhook')
|
|
||||||
@validate_json_request('WebhookCreateRequest')
|
|
||||||
def post(self, namespace, repository):
|
|
||||||
""" Create a new webhook for the specified repository. """
|
|
||||||
repo = model.get_repository(namespace, repository)
|
|
||||||
webhook = model.create_webhook(repo, request.get_json())
|
|
||||||
resp = webhook_view(webhook)
|
|
||||||
repo_string = '%s/%s' % (namespace, repository)
|
|
||||||
headers = {
|
|
||||||
'Location': api.url_for(Webhook, repository=repo_string, public_id=webhook.public_id),
|
|
||||||
}
|
|
||||||
log_action('add_repo_webhook', namespace,
|
|
||||||
{'repo': repository, 'webhook_id': webhook.public_id},
|
|
||||||
repo=repo)
|
|
||||||
return resp, 201, headers
|
|
||||||
|
|
||||||
@require_repo_admin
|
|
||||||
@nickname('listWebhooks')
|
|
||||||
def get(self, namespace, repository):
|
|
||||||
""" List the webhooks for the specified repository. """
|
|
||||||
webhooks = model.list_webhooks(namespace, repository)
|
|
||||||
return {
|
|
||||||
'webhooks': [webhook_view(webhook) for webhook in webhooks]
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
@resource('/v1/repository/<repopath:repository>/webhook/<public_id>')
|
|
||||||
class Webhook(RepositoryParamResource):
|
|
||||||
""" Resource for dealing with specific webhooks. """
|
|
||||||
@require_repo_admin
|
|
||||||
@nickname('getWebhook')
|
|
||||||
def get(self, namespace, repository, public_id):
|
|
||||||
""" Get information for the specified webhook. """
|
|
||||||
try:
|
|
||||||
webhook = model.get_webhook(namespace, repository, public_id)
|
|
||||||
except model.InvalidWebhookException:
|
|
||||||
raise NotFound()
|
|
||||||
|
|
||||||
return webhook_view(webhook)
|
|
||||||
|
|
||||||
@require_repo_admin
|
|
||||||
@nickname('deleteWebhook')
|
|
||||||
def delete(self, namespace, repository, public_id):
|
|
||||||
""" Delete the specified webhook. """
|
|
||||||
model.delete_webhook(namespace, repository, public_id)
|
|
||||||
log_action('delete_repo_webhook', namespace,
|
|
||||||
{'repo': repository, 'webhook_id': public_id},
|
|
||||||
repo=model.get_repository(namespace, repository))
|
|
||||||
return 'No Content', 204
|
|
|
@ -9,7 +9,7 @@ from flask.ext.principal import identity_changed
|
||||||
from random import SystemRandom
|
from random import SystemRandom
|
||||||
|
|
||||||
from data import model
|
from data import model
|
||||||
from app import app, login_manager, dockerfile_build_queue
|
from app import app, login_manager, dockerfile_build_queue, notification_queue
|
||||||
from auth.permissions import QuayDeferredPermissionUser
|
from auth.permissions import QuayDeferredPermissionUser
|
||||||
from auth import scopes
|
from auth import scopes
|
||||||
from endpoints.api.discovery import swagger_route_data
|
from endpoints.api.discovery import swagger_route_data
|
||||||
|
@ -17,10 +17,12 @@ from werkzeug.routing import BaseConverter
|
||||||
from functools import wraps
|
from functools import wraps
|
||||||
from config import getFrontendVisibleConfig
|
from config import getFrontendVisibleConfig
|
||||||
from external_libraries import get_external_javascript, get_external_css
|
from external_libraries import get_external_javascript, get_external_css
|
||||||
|
from endpoints.notificationhelper import spawn_notification
|
||||||
|
|
||||||
import features
|
import features
|
||||||
|
|
||||||
logger = logging.getLogger(__name__)
|
logger = logging.getLogger(__name__)
|
||||||
|
profile = logging.getLogger('application.profiler')
|
||||||
|
|
||||||
route_data = None
|
route_data = None
|
||||||
|
|
||||||
|
@ -220,6 +222,7 @@ def start_build(repository, dockerfile_id, tags, build_name, subdir, manual,
|
||||||
'pull_credentials': model.get_pull_credentials(pull_robot_name) if pull_robot_name else None
|
'pull_credentials': model.get_pull_credentials(pull_robot_name) if pull_robot_name else None
|
||||||
}), retries_remaining=1)
|
}), retries_remaining=1)
|
||||||
|
|
||||||
|
# Add the build to the repo's log.
|
||||||
metadata = {
|
metadata = {
|
||||||
'repo': repository.name,
|
'repo': repository.name,
|
||||||
'namespace': repository.namespace,
|
'namespace': repository.namespace,
|
||||||
|
@ -236,4 +239,21 @@ def start_build(repository, dockerfile_id, tags, build_name, subdir, manual,
|
||||||
ip=request.remote_addr, metadata=metadata,
|
ip=request.remote_addr, metadata=metadata,
|
||||||
repository=repository)
|
repository=repository)
|
||||||
|
|
||||||
|
# Add notifications for the build queue.
|
||||||
|
profile.debug('Adding notifications for repository')
|
||||||
|
event_data = {
|
||||||
|
'build_id': build_request.uuid,
|
||||||
|
'build_name': build_name,
|
||||||
|
'docker_tags': tags,
|
||||||
|
'is_manual': manual
|
||||||
|
}
|
||||||
|
|
||||||
|
if trigger:
|
||||||
|
event_data['trigger_id'] = trigger.uuid
|
||||||
|
event_data['trigger_kind'] = trigger.service.name
|
||||||
|
|
||||||
|
spawn_notification(repository, 'build_queued', event_data,
|
||||||
|
subpage='build?current=%s' % build_request.uuid,
|
||||||
|
pathargs=['build', build_request.uuid])
|
||||||
return build_request
|
return build_request
|
||||||
|
|
||||||
|
|
|
@ -8,7 +8,7 @@ from collections import OrderedDict
|
||||||
|
|
||||||
from data import model
|
from data import model
|
||||||
from data.model import oauth
|
from data.model import oauth
|
||||||
from app import analytics, app, webhook_queue, authentication, userevents, storage
|
from app import analytics, app, authentication, userevents, storage
|
||||||
from auth.auth import process_auth
|
from auth.auth import process_auth
|
||||||
from auth.auth_context import get_authenticated_user, get_validated_token, get_validated_oauth_token
|
from auth.auth_context import get_authenticated_user, get_validated_token, get_validated_oauth_token
|
||||||
from util.names import parse_repository_name
|
from util.names import parse_repository_name
|
||||||
|
@ -17,6 +17,7 @@ from auth.permissions import (ModifyRepositoryPermission, UserAdminPermission,
|
||||||
ReadRepositoryPermission, CreateRepositoryPermission)
|
ReadRepositoryPermission, CreateRepositoryPermission)
|
||||||
|
|
||||||
from util.http import abort
|
from util.http import abort
|
||||||
|
from endpoints.notificationhelper import spawn_notification
|
||||||
|
|
||||||
|
|
||||||
logger = logging.getLogger(__name__)
|
logger = logging.getLogger(__name__)
|
||||||
|
@ -315,28 +316,16 @@ def update_images(namespace, repository):
|
||||||
profile.debug('GCing repository')
|
profile.debug('GCing repository')
|
||||||
num_removed = model.garbage_collect_repository(namespace, repository)
|
num_removed = model.garbage_collect_repository(namespace, repository)
|
||||||
|
|
||||||
# Generate a job for each webhook that has been added to this repo
|
# Generate a job for each notification that has been added to this repo
|
||||||
profile.debug('Adding webhooks for repository')
|
profile.debug('Adding notifications for repository')
|
||||||
|
|
||||||
webhooks = model.list_webhooks(namespace, repository)
|
event_data = {
|
||||||
for webhook in webhooks:
|
|
||||||
webhook_data = json.loads(webhook.parameters)
|
|
||||||
repo_string = '%s/%s' % (namespace, repository)
|
|
||||||
profile.debug('Creating webhook for repository \'%s\' for url \'%s\'',
|
|
||||||
repo_string, webhook_data['url'])
|
|
||||||
webhook_data['payload'] = {
|
|
||||||
'repository': repo_string,
|
|
||||||
'namespace': namespace,
|
|
||||||
'name': repository,
|
|
||||||
'docker_url': 'quay.io/%s' % repo_string,
|
|
||||||
'homepage': 'https://quay.io/repository/%s' % repo_string,
|
|
||||||
'visibility': repo.visibility.name,
|
|
||||||
'updated_tags': updated_tags,
|
'updated_tags': updated_tags,
|
||||||
'pushed_image_count': len(image_with_checksums),
|
'pushed_image_count': len(image_with_checksums),
|
||||||
'pruned_image_count': num_removed,
|
'pruned_image_count': num_removed
|
||||||
}
|
}
|
||||||
webhook_queue.put([namespace, repository], json.dumps(webhook_data))
|
|
||||||
|
|
||||||
|
spawn_notification(repo, 'repo_push', event_data)
|
||||||
return make_response('Updated', 204)
|
return make_response('Updated', 204)
|
||||||
|
|
||||||
abort(403)
|
abort(403)
|
||||||
|
|
207
endpoints/notificationevent.py
Normal file
207
endpoints/notificationevent.py
Normal file
|
@ -0,0 +1,207 @@
|
||||||
|
import logging
|
||||||
|
import io
|
||||||
|
import os.path
|
||||||
|
import tarfile
|
||||||
|
import base64
|
||||||
|
|
||||||
|
from notificationhelper import build_event_data
|
||||||
|
|
||||||
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
class InvalidNotificationEventException(Exception):
|
||||||
|
pass
|
||||||
|
|
||||||
|
class NotificationEvent(object):
|
||||||
|
def __init__(self):
|
||||||
|
pass
|
||||||
|
|
||||||
|
def get_summary(self, event_data, notification_data):
|
||||||
|
"""
|
||||||
|
Returns a human readable one-line summary for the given notification data.
|
||||||
|
"""
|
||||||
|
raise NotImplementedError
|
||||||
|
|
||||||
|
def get_message(self, event_data, notification_data):
|
||||||
|
"""
|
||||||
|
Returns a human readable HTML message for the given notification data.
|
||||||
|
"""
|
||||||
|
raise NotImplementedError
|
||||||
|
|
||||||
|
def get_sample_data(self, repository=None):
|
||||||
|
"""
|
||||||
|
Returns sample data for testing the raising of this notification, with an optional
|
||||||
|
repository.
|
||||||
|
"""
|
||||||
|
raise NotImplementedError
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def event_name(cls):
|
||||||
|
"""
|
||||||
|
Particular event implemented by subclasses.
|
||||||
|
"""
|
||||||
|
raise NotImplementedError
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def get_event(cls, eventname):
|
||||||
|
for subc in cls.__subclasses__():
|
||||||
|
if subc.event_name() == eventname:
|
||||||
|
return subc()
|
||||||
|
|
||||||
|
raise InvalidNotificationEventException('Unable to find event: %s' % eventname)
|
||||||
|
|
||||||
|
|
||||||
|
class RepoPushEvent(NotificationEvent):
|
||||||
|
@classmethod
|
||||||
|
def event_name(cls):
|
||||||
|
return 'repo_push'
|
||||||
|
|
||||||
|
def get_summary(self, event_data, notification_data):
|
||||||
|
return 'Repository %s updated' % (event_data['repository'])
|
||||||
|
|
||||||
|
def get_message(self, event_data, notification_data):
|
||||||
|
if not event_data.get('updated_tags', []):
|
||||||
|
html = """
|
||||||
|
Repository <a href="%s">%s</a> has been updated via a push.
|
||||||
|
""" % (event_data['homepage'],
|
||||||
|
event_data['repository'])
|
||||||
|
else:
|
||||||
|
html = """
|
||||||
|
Repository <a href="%s">%s</a> has been updated via a push.
|
||||||
|
<br><br>
|
||||||
|
Tags Updated: %s
|
||||||
|
""" % (event_data['homepage'],
|
||||||
|
event_data['repository'],
|
||||||
|
', '.join(event_data['updated_tags']))
|
||||||
|
|
||||||
|
return html
|
||||||
|
|
||||||
|
def get_sample_data(self, repository):
|
||||||
|
return build_event_data(repository, {
|
||||||
|
'updated_tags': ['latest', 'foo', 'bar'],
|
||||||
|
'pushed_image_count': 10,
|
||||||
|
'pruned_image_count': 3
|
||||||
|
})
|
||||||
|
|
||||||
|
|
||||||
|
class BuildQueueEvent(NotificationEvent):
|
||||||
|
@classmethod
|
||||||
|
def event_name(cls):
|
||||||
|
return 'build_queued'
|
||||||
|
|
||||||
|
def get_sample_data(self, repository):
|
||||||
|
build_uuid = 'fake-build-id'
|
||||||
|
|
||||||
|
return build_event_data(repository, {
|
||||||
|
'is_manual': False,
|
||||||
|
'build_id': build_uuid,
|
||||||
|
'build_name': 'some-fake-build',
|
||||||
|
'docker_tags': ['latest', 'foo', 'bar'],
|
||||||
|
'trigger_kind': 'GitHub'
|
||||||
|
}, subpage='/build?current=%s' % build_uuid)
|
||||||
|
|
||||||
|
def get_summary(self, event_data, notification_data):
|
||||||
|
return 'Build queued for repository %s' % (event_data['repository'])
|
||||||
|
|
||||||
|
def get_message(self, event_data, notification_data):
|
||||||
|
is_manual = event_data['is_manual']
|
||||||
|
if is_manual:
|
||||||
|
html = """
|
||||||
|
A <a href="%s">new build</a> has been manually queued to start on repository %s.
|
||||||
|
<br><br>
|
||||||
|
Build ID: %s
|
||||||
|
""" % (event_data['homepage'], event_data['repository'], event_data['build_id'])
|
||||||
|
else:
|
||||||
|
html = """
|
||||||
|
A <a href="%s">new build</a> has been queued via a %s trigger to start on repository %s.
|
||||||
|
<br><br>
|
||||||
|
Build ID: %s
|
||||||
|
""" % (event_data['homepage'], event_data['trigger_kind'],
|
||||||
|
event_data['repository'], event_data['build_id'])
|
||||||
|
|
||||||
|
return html
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
class BuildStartEvent(NotificationEvent):
|
||||||
|
@classmethod
|
||||||
|
def event_name(cls):
|
||||||
|
return 'build_start'
|
||||||
|
|
||||||
|
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',
|
||||||
|
'docker_tags': ['latest', 'foo', 'bar'],
|
||||||
|
'trigger_kind': 'GitHub'
|
||||||
|
}, subpage='/build?current=%s' % build_uuid)
|
||||||
|
|
||||||
|
def get_summary(self, event_data, notification_data):
|
||||||
|
return 'Build started for repository %s' % (event_data['repository'])
|
||||||
|
|
||||||
|
def get_message(self, event_data, notification_data):
|
||||||
|
html = """
|
||||||
|
A <a href="%s">new build</a> has started on repository %s.
|
||||||
|
<br><br>
|
||||||
|
Build ID: %s
|
||||||
|
""" % (event_data['homepage'], event_data['repository'], event_data['build_id'])
|
||||||
|
|
||||||
|
return html
|
||||||
|
|
||||||
|
|
||||||
|
class BuildSuccessEvent(NotificationEvent):
|
||||||
|
@classmethod
|
||||||
|
def event_name(cls):
|
||||||
|
return 'build_success'
|
||||||
|
|
||||||
|
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',
|
||||||
|
'docker_tags': ['latest', 'foo', 'bar'],
|
||||||
|
'trigger_kind': 'GitHub'
|
||||||
|
}, subpage='/build?current=%s' % build_uuid)
|
||||||
|
|
||||||
|
def get_summary(self, event_data, notification_data):
|
||||||
|
return 'Build succeeded for repository %s' % (event_data['repository'])
|
||||||
|
|
||||||
|
def get_message(self, event_data, notification_data):
|
||||||
|
html = """
|
||||||
|
A <a href="%s">build</a> has finished on repository %s.
|
||||||
|
<br><br>
|
||||||
|
Build ID: %s
|
||||||
|
""" % (event_data['homepage'], event_data['repository'], event_data['build_id'])
|
||||||
|
|
||||||
|
return html
|
||||||
|
|
||||||
|
|
||||||
|
class BuildFailureEvent(NotificationEvent):
|
||||||
|
@classmethod
|
||||||
|
def event_name(cls):
|
||||||
|
return 'build_failure'
|
||||||
|
|
||||||
|
def get_sample_data(self, repository):
|
||||||
|
return build_event_data(repository, {
|
||||||
|
'build_id': build_uuid,
|
||||||
|
'build_name': 'some-fake-build',
|
||||||
|
'docker_tags': ['latest', 'foo', 'bar'],
|
||||||
|
'trigger_kind': 'GitHub',
|
||||||
|
'error_message': 'This is a fake error message'
|
||||||
|
}, subpage='/build?current=%s' % build_uuid)
|
||||||
|
|
||||||
|
def get_summary(self, event_data, notification_data):
|
||||||
|
return 'Build failure for repository %s' % (event_data['repository'])
|
||||||
|
|
||||||
|
def get_message(self, event_data, notification_data):
|
||||||
|
html = """
|
||||||
|
A <a href="%s">build</a> has failed on repository %s.
|
||||||
|
<br><br>
|
||||||
|
Reason: %s<br>
|
||||||
|
Build ID: %s<br>
|
||||||
|
""" % (event_data['homepage'], event_data['repository'],
|
||||||
|
event_data['error_message'], event_data['build_id'])
|
||||||
|
|
||||||
|
return html
|
46
endpoints/notificationhelper.py
Normal file
46
endpoints/notificationhelper.py
Normal file
|
@ -0,0 +1,46 @@
|
||||||
|
from app import app, notification_queue
|
||||||
|
from data import model
|
||||||
|
|
||||||
|
import json
|
||||||
|
|
||||||
|
def build_event_data(repo, extra_data={}, subpage=None):
|
||||||
|
repo_string = '%s/%s' % (repo.namespace, repo.name)
|
||||||
|
homepage = '%s://%s/repository/%s' % (app.config['PREFERRED_URL_SCHEME'],
|
||||||
|
app.config['SERVER_HOSTNAME'],
|
||||||
|
repo_string)
|
||||||
|
|
||||||
|
if subpage:
|
||||||
|
if not subpage.startswith('/'):
|
||||||
|
subpage = '/' + subpage
|
||||||
|
|
||||||
|
homepage = homepage + subpage
|
||||||
|
|
||||||
|
event_data = {
|
||||||
|
'repository': repo_string,
|
||||||
|
'namespace': repo.namespace,
|
||||||
|
'name': repo.name,
|
||||||
|
'docker_url': '%s/%s' % (app.config['SERVER_HOSTNAME'], repo_string),
|
||||||
|
'homepage': homepage,
|
||||||
|
'visibility': repo.visibility.name
|
||||||
|
}
|
||||||
|
|
||||||
|
event_data.update(extra_data)
|
||||||
|
return event_data
|
||||||
|
|
||||||
|
def build_notification_data(notification, event_data):
|
||||||
|
return {
|
||||||
|
'notification_uuid': notification.uuid,
|
||||||
|
'repository_namespace': notification.repository.namespace,
|
||||||
|
'repository_name': notification.repository.name,
|
||||||
|
'event_data': event_data
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
def spawn_notification(repo, event_name, extra_data={}, subpage=None, pathargs=[]):
|
||||||
|
event_data = build_event_data(repo, extra_data=extra_data, subpage=subpage)
|
||||||
|
|
||||||
|
notifications = model.list_repo_notifications(repo.namespace, repo.name, event_name=event_name)
|
||||||
|
for notification in notifications:
|
||||||
|
notification_data = build_notification_data(notification, event_data)
|
||||||
|
path = [repo.namespace, repo.name, event_name] + pathargs
|
||||||
|
notification_queue.put(path, json.dumps(notification_data))
|
189
endpoints/notificationmethod.py
Normal file
189
endpoints/notificationmethod.py
Normal file
|
@ -0,0 +1,189 @@
|
||||||
|
import logging
|
||||||
|
import io
|
||||||
|
import os.path
|
||||||
|
import tarfile
|
||||||
|
import base64
|
||||||
|
import json
|
||||||
|
|
||||||
|
from flask.ext.mail import Message
|
||||||
|
from app import mail, app
|
||||||
|
from data import model
|
||||||
|
|
||||||
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
class InvalidNotificationMethodException(Exception):
|
||||||
|
pass
|
||||||
|
|
||||||
|
class CannotValidateNotificationMethodException(Exception):
|
||||||
|
pass
|
||||||
|
|
||||||
|
|
||||||
|
class NotificationMethod(object):
|
||||||
|
def __init__(self):
|
||||||
|
pass
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def method_name(cls):
|
||||||
|
"""
|
||||||
|
Particular method implemented by subclasses.
|
||||||
|
"""
|
||||||
|
raise NotImplementedError
|
||||||
|
|
||||||
|
def validate(self, repository, config_data):
|
||||||
|
"""
|
||||||
|
Validates that the notification can be created with the given data. Throws
|
||||||
|
a CannotValidateNotificationMethodException on failure.
|
||||||
|
"""
|
||||||
|
raise NotImplementedError
|
||||||
|
|
||||||
|
def perform(self, notification, event_handler, notification_data):
|
||||||
|
"""
|
||||||
|
Performs the notification method.
|
||||||
|
|
||||||
|
notification: The noticication record itself.
|
||||||
|
event_handler: The NotificationEvent handler.
|
||||||
|
notification_data: The dict of notification data placed in the queue.
|
||||||
|
"""
|
||||||
|
raise NotImplementedError
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def get_method(cls, methodname):
|
||||||
|
for subc in cls.__subclasses__():
|
||||||
|
if subc.method_name() == methodname:
|
||||||
|
return subc()
|
||||||
|
|
||||||
|
raise InvalidNotificationMethodException('Unable to find method: %s' % methodname)
|
||||||
|
|
||||||
|
|
||||||
|
class QuayNotificationMethod(NotificationMethod):
|
||||||
|
@classmethod
|
||||||
|
def method_name(cls):
|
||||||
|
return 'quay_notification'
|
||||||
|
|
||||||
|
def validate(self, repository, config_data):
|
||||||
|
status, err_message, target_users = self.find_targets(repository, config_data)
|
||||||
|
if err_message:
|
||||||
|
raise CannotValidateNotificationMethodException(err_message)
|
||||||
|
|
||||||
|
def find_targets(self, repository, config_data):
|
||||||
|
target_info = config_data['target']
|
||||||
|
|
||||||
|
if target_info['kind'] == 'user':
|
||||||
|
target = model.get_user(target_info['name'])
|
||||||
|
if not target:
|
||||||
|
# Just to be safe.
|
||||||
|
return (True, 'Unknown user %s' % target_info['name'], [])
|
||||||
|
|
||||||
|
return (True, None, [target])
|
||||||
|
elif target_info['kind'] == 'org':
|
||||||
|
target = model.get_organization(target_info['name'])
|
||||||
|
if not target:
|
||||||
|
# Just to be safe.
|
||||||
|
return (True, 'Unknown organization %s' % target_info['name'], None)
|
||||||
|
|
||||||
|
# Only repositories under the organization can cause notifications to that org.
|
||||||
|
if target_info['name'] != repository.namespace:
|
||||||
|
return (False, 'Organization name must match repository namespace')
|
||||||
|
|
||||||
|
return (True, None, [target])
|
||||||
|
elif target_info['kind'] == 'team':
|
||||||
|
# Lookup the team.
|
||||||
|
team = None
|
||||||
|
try:
|
||||||
|
team = model.get_organization_team(repository.namespace, target_info['name'])
|
||||||
|
except model.InvalidTeamException:
|
||||||
|
# Probably deleted.
|
||||||
|
return (True, 'Unknown team %s' % target_info['name'], None)
|
||||||
|
|
||||||
|
# Lookup the team's members
|
||||||
|
return (True, None, model.get_organization_team_members(team.id))
|
||||||
|
|
||||||
|
|
||||||
|
def perform(self, notification, event_handler, notification_data):
|
||||||
|
repository = notification.repository
|
||||||
|
if not repository:
|
||||||
|
# Probably deleted.
|
||||||
|
return True
|
||||||
|
|
||||||
|
# Lookup the target user or team to which we'll send the notification.
|
||||||
|
config_data = json.loads(notification.config_json)
|
||||||
|
status, err_message, target_users = self.find_targets(repository, config_data)
|
||||||
|
if not status:
|
||||||
|
return False
|
||||||
|
|
||||||
|
# For each of the target users, create a notification.
|
||||||
|
for target_user in set(target_users or []):
|
||||||
|
model.create_notification(event_handler.event_name(), target_user,
|
||||||
|
metadata=notification_data['event_data'])
|
||||||
|
return True
|
||||||
|
|
||||||
|
|
||||||
|
class EmailMethod(NotificationMethod):
|
||||||
|
@classmethod
|
||||||
|
def method_name(cls):
|
||||||
|
return 'email'
|
||||||
|
|
||||||
|
def validate(self, repository, config_data):
|
||||||
|
email = config_data.get('email', '')
|
||||||
|
if not email:
|
||||||
|
raise CannotValidateNotificationMethodException('Missing e-mail address')
|
||||||
|
|
||||||
|
record = model.get_email_authorized_for_repo(repository.namespace, repository.name, email)
|
||||||
|
if not record or not record.confirmed:
|
||||||
|
raise CannotValidateNotificationMethodException('The specified e-mail address '
|
||||||
|
'is not authorized to receive '
|
||||||
|
'notifications for this repository')
|
||||||
|
|
||||||
|
|
||||||
|
def perform(self, notification, event_handler, notification_data):
|
||||||
|
config_data = json.loads(notification.config_json)
|
||||||
|
email = config_data.get('email', '')
|
||||||
|
if not email:
|
||||||
|
return False
|
||||||
|
|
||||||
|
msg = Message(event_handler.get_summary(notification_data['event_data'], notification_data),
|
||||||
|
sender='support@quay.io',
|
||||||
|
recipients=[email])
|
||||||
|
msg.html = event_handler.get_message(notification_data['event_data'], notification_data)
|
||||||
|
|
||||||
|
try:
|
||||||
|
with app.app_context():
|
||||||
|
mail.send(msg)
|
||||||
|
except Exception as ex:
|
||||||
|
logger.exception('Email was unable to be sent: %s' % ex.message)
|
||||||
|
return False
|
||||||
|
|
||||||
|
return True
|
||||||
|
|
||||||
|
|
||||||
|
class WebhookMethod(NotificationMethod):
|
||||||
|
@classmethod
|
||||||
|
def method_name(cls):
|
||||||
|
return 'webhook'
|
||||||
|
|
||||||
|
def validate(self, repository, config_data):
|
||||||
|
url = config_data.get('url', '')
|
||||||
|
if not url:
|
||||||
|
raise CannotValidateNotificationMethodException('Missing webhook URL')
|
||||||
|
|
||||||
|
def perform(self, notification, event_handler, notification_data):
|
||||||
|
config_data = json.loads(notification.config_json)
|
||||||
|
url = config_data.get('url', '')
|
||||||
|
if not url:
|
||||||
|
return False
|
||||||
|
|
||||||
|
payload = notification_data['event_data']
|
||||||
|
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
|
|
@ -214,6 +214,26 @@ def receipt():
|
||||||
abort(404)
|
abort(404)
|
||||||
|
|
||||||
|
|
||||||
|
@web.route('/authrepoemail', methods=['GET'])
|
||||||
|
def confirm_repo_email():
|
||||||
|
code = request.values['code']
|
||||||
|
record = None
|
||||||
|
|
||||||
|
try:
|
||||||
|
record = model.confirm_email_authorization_for_repo(code)
|
||||||
|
except model.DataModelException as ex:
|
||||||
|
return render_page_template('confirmerror.html', error_message=ex.message)
|
||||||
|
|
||||||
|
message = """
|
||||||
|
Your E-mail address has been authorized to receive notifications for repository
|
||||||
|
<a href="%s://%s/repository/%s/%s">%s/%s</a>.
|
||||||
|
""" % (app.config['PREFERRED_URL_SCHEME'], app.config['SERVER_HOSTNAME'],
|
||||||
|
record.repository.namespace, record.repository.name,
|
||||||
|
record.repository.namespace, record.repository.name)
|
||||||
|
|
||||||
|
return render_page_template('message.html', message=message)
|
||||||
|
|
||||||
|
|
||||||
@web.route('/confirm', methods=['GET'])
|
@web.route('/confirm', methods=['GET'])
|
||||||
def confirm_email():
|
def confirm_email():
|
||||||
code = request.values['code']
|
code = request.values['code']
|
||||||
|
|
|
@ -18,7 +18,7 @@ EXTERNAL_JS = [
|
||||||
]
|
]
|
||||||
|
|
||||||
EXTERNAL_CSS = [
|
EXTERNAL_CSS = [
|
||||||
'netdna.bootstrapcdn.com/font-awesome/4.0.3/css/font-awesome.css',
|
'netdna.bootstrapcdn.com/font-awesome/4.1.0/css/font-awesome.css',
|
||||||
'netdna.bootstrapcdn.com/bootstrap/3.0.0/css/bootstrap.no-icons.min.css',
|
'netdna.bootstrapcdn.com/bootstrap/3.0.0/css/bootstrap.no-icons.min.css',
|
||||||
'fonts.googleapis.com/css?family=Droid+Sans:400,700',
|
'fonts.googleapis.com/css?family=Droid+Sans:400,700',
|
||||||
]
|
]
|
||||||
|
|
39
initdb.py
39
initdb.py
|
@ -206,8 +206,6 @@ def initialize_database():
|
||||||
LogEntryKind.create(name='change_repo_visibility')
|
LogEntryKind.create(name='change_repo_visibility')
|
||||||
LogEntryKind.create(name='add_repo_accesstoken')
|
LogEntryKind.create(name='add_repo_accesstoken')
|
||||||
LogEntryKind.create(name='delete_repo_accesstoken')
|
LogEntryKind.create(name='delete_repo_accesstoken')
|
||||||
LogEntryKind.create(name='add_repo_webhook')
|
|
||||||
LogEntryKind.create(name='delete_repo_webhook')
|
|
||||||
LogEntryKind.create(name='set_repo_description')
|
LogEntryKind.create(name='set_repo_description')
|
||||||
|
|
||||||
LogEntryKind.create(name='build_dockerfile')
|
LogEntryKind.create(name='build_dockerfile')
|
||||||
|
@ -231,6 +229,34 @@ def initialize_database():
|
||||||
LogEntryKind.create(name='delete_application')
|
LogEntryKind.create(name='delete_application')
|
||||||
LogEntryKind.create(name='reset_application_client_secret')
|
LogEntryKind.create(name='reset_application_client_secret')
|
||||||
|
|
||||||
|
# Note: These 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')
|
||||||
|
|
||||||
|
ImageStorageLocation.create(name='local_eu')
|
||||||
|
ImageStorageLocation.create(name='local_us')
|
||||||
|
|
||||||
|
# NOTE: These MUST be copied over to NotificationKind, since every external
|
||||||
|
# notification can also generate a Quay.io notification.
|
||||||
|
ExternalNotificationEvent.create(name='repo_push')
|
||||||
|
ExternalNotificationEvent.create(name='build_queued')
|
||||||
|
ExternalNotificationEvent.create(name='build_start')
|
||||||
|
ExternalNotificationEvent.create(name='build_success')
|
||||||
|
ExternalNotificationEvent.create(name='build_failure')
|
||||||
|
|
||||||
|
ExternalNotificationMethod.create(name='quay_notification')
|
||||||
|
ExternalNotificationMethod.create(name='email')
|
||||||
|
ExternalNotificationMethod.create(name='webhook')
|
||||||
|
|
||||||
|
NotificationKind.create(name='repo_push')
|
||||||
|
NotificationKind.create(name='build_queued')
|
||||||
|
NotificationKind.create(name='build_start')
|
||||||
|
NotificationKind.create(name='build_success')
|
||||||
|
NotificationKind.create(name='build_failure')
|
||||||
|
|
||||||
NotificationKind.create(name='password_required')
|
NotificationKind.create(name='password_required')
|
||||||
NotificationKind.create(name='over_private_usage')
|
NotificationKind.create(name='over_private_usage')
|
||||||
NotificationKind.create(name='expiring_license')
|
NotificationKind.create(name='expiring_license')
|
||||||
|
@ -238,9 +264,6 @@ def initialize_database():
|
||||||
|
|
||||||
NotificationKind.create(name='test_notification')
|
NotificationKind.create(name='test_notification')
|
||||||
|
|
||||||
ImageStorageLocation.create(name='local_eu')
|
|
||||||
ImageStorageLocation.create(name='local_us')
|
|
||||||
|
|
||||||
|
|
||||||
def wipe_database():
|
def wipe_database():
|
||||||
logger.debug('Wiping all data from the DB.')
|
logger.debug('Wiping all data from the DB.')
|
||||||
|
@ -361,6 +384,12 @@ def populate_database():
|
||||||
'build_subdir': '',
|
'build_subdir': '',
|
||||||
}
|
}
|
||||||
|
|
||||||
|
record = model.create_email_authorization_for_repo(new_user_1.username, 'simple', 'jschorr@devtable.com')
|
||||||
|
record.confirmed = True
|
||||||
|
record.save()
|
||||||
|
|
||||||
|
model.create_email_authorization_for_repo(new_user_1.username, 'simple', 'jschorr+other@devtable.com')
|
||||||
|
|
||||||
build2 = model.create_repository_build(building, token, job_config,
|
build2 = model.create_repository_build(building, token, job_config,
|
||||||
'68daeebd-a5b9-457f-80a0-4363b882f8ea',
|
'68daeebd-a5b9-457f-80a0-4363b882f8ea',
|
||||||
'build-name', trigger)
|
'build-name', trigger)
|
||||||
|
|
BIN
license.pyc
BIN
license.pyc
Binary file not shown.
|
@ -89,6 +89,11 @@ nav.navbar-default .navbar-nav>li>a {
|
||||||
background: rgba(66, 139, 202, 0.1);
|
background: rgba(66, 139, 202, 0.1);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.notification-view-element .right-controls {
|
||||||
|
text-align: right;
|
||||||
|
font-size: 12px;
|
||||||
|
}
|
||||||
|
|
||||||
.dockerfile-path {
|
.dockerfile-path {
|
||||||
margin-top: 10px;
|
margin-top: 10px;
|
||||||
padding: 20px;
|
padding: 20px;
|
||||||
|
@ -219,11 +224,12 @@ nav.navbar-default .navbar-nav>li>a {
|
||||||
|
|
||||||
.entity-search-element {
|
.entity-search-element {
|
||||||
position: relative;
|
position: relative;
|
||||||
|
display: block;
|
||||||
}
|
}
|
||||||
|
|
||||||
.entity-search-element .entity-reference {
|
.entity-search-element .entity-reference {
|
||||||
position: absolute !important;
|
position: absolute !important;
|
||||||
top: 0px;
|
top: 7px;
|
||||||
left: 8px;
|
left: 8px;
|
||||||
right: 36px;
|
right: 36px;
|
||||||
z-index: 0;
|
z-index: 0;
|
||||||
|
@ -244,6 +250,7 @@ nav.navbar-default .navbar-nav>li>a {
|
||||||
|
|
||||||
.entity-search-element input {
|
.entity-search-element input {
|
||||||
vertical-align: middle;
|
vertical-align: middle;
|
||||||
|
width: 100%;
|
||||||
}
|
}
|
||||||
|
|
||||||
.entity-search-element.persistent input {
|
.entity-search-element.persistent input {
|
||||||
|
@ -253,12 +260,15 @@ nav.navbar-default .navbar-nav>li>a {
|
||||||
|
|
||||||
.entity-search-element .twitter-typeahead {
|
.entity-search-element .twitter-typeahead {
|
||||||
vertical-align: middle;
|
vertical-align: middle;
|
||||||
|
display: block !important;
|
||||||
|
margin-right: 36px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.entity-search-element .dropdown {
|
.entity-search-element .dropdown {
|
||||||
vertical-align: middle;
|
vertical-align: middle;
|
||||||
display: inline-block;
|
position: absolute;
|
||||||
margin-top: 0px;
|
top: 0px;
|
||||||
|
right: 0px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.dropdown-menu i.fa {
|
.dropdown-menu i.fa {
|
||||||
|
@ -2508,10 +2518,6 @@ p.editable:hover i {
|
||||||
text-align: right;
|
text-align: right;
|
||||||
}
|
}
|
||||||
|
|
||||||
.repo-admin .entity-search input {
|
|
||||||
width: 300px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.repo-admin .panel {
|
.repo-admin .panel {
|
||||||
display: inline-block;
|
display: inline-block;
|
||||||
width: 720px;
|
width: 720px;
|
||||||
|
@ -3023,7 +3029,6 @@ p.editable:hover i {
|
||||||
|
|
||||||
.team-view .entity-search {
|
.team-view .entity-search {
|
||||||
margin-top: 10px;
|
margin-top: 10px;
|
||||||
display: inline-block;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.team-view .delete-ui {
|
.team-view .delete-ui {
|
||||||
|
@ -3889,6 +3894,11 @@ pre.command:before {
|
||||||
display: none;
|
display: none;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.dropdown-select input.form-control[readonly] {
|
||||||
|
cursor: pointer;
|
||||||
|
background-color: #fff;
|
||||||
|
}
|
||||||
|
|
||||||
.dropdown-select .lookahead-input {
|
.dropdown-select .lookahead-input {
|
||||||
padding-left: 32px;
|
padding-left: 32px;
|
||||||
}
|
}
|
||||||
|
@ -4429,3 +4439,46 @@ have a fixed width and height (but it's not required).
|
||||||
float: right;
|
float: right;
|
||||||
font-size: 22px;
|
font-size: 22px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
i.quay-icon {
|
||||||
|
background-image: url(/static/img/favicon.ico);
|
||||||
|
background-size: 16px;
|
||||||
|
width: 16px;
|
||||||
|
height: 16px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.external-notification-view-element {
|
||||||
|
margin: 10px;
|
||||||
|
padding: 6px;
|
||||||
|
border: 1px solid #eee;
|
||||||
|
border-radius: 6px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.external-notification-view-element i.fa {
|
||||||
|
margin-right: 6px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.external-notification-view-element .view-row {
|
||||||
|
margin-bottom: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.external-notification-view-element .view-row:last-child {
|
||||||
|
margin-bottom: 0px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.external-notification-view-element .flow-text {
|
||||||
|
display: inline-block;
|
||||||
|
color: #aaa;
|
||||||
|
text-transform: lowercase;
|
||||||
|
font-variant: small-caps;
|
||||||
|
width: 50px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.external-notification-view-element .side-controls button {
|
||||||
|
border: 1px solid transparent;
|
||||||
|
transition: all 300ms ease-in-out;
|
||||||
|
}
|
||||||
|
|
||||||
|
.external-notification-view-element:hover .side-controls button {
|
||||||
|
border: 1px solid #eee;
|
||||||
|
}
|
129
static/directives/create-external-notification-dialog.html
Normal file
129
static/directives/create-external-notification-dialog.html
Normal file
|
@ -0,0 +1,129 @@
|
||||||
|
<!-- Modal message dialog -->
|
||||||
|
<div class="modal fade" id="createNotificationModal">
|
||||||
|
<div class="modal-dialog">
|
||||||
|
<div class="modal-content">
|
||||||
|
<form id="createForm" name="createForm" ng-submit="createNotification()">
|
||||||
|
<div class="modal-header">
|
||||||
|
<button type="button" class="close" data-dismiss="modal" aria-hidden="true" ng-disabled="creating">×</button>
|
||||||
|
<h4 class="modal-title">
|
||||||
|
Create Repository Notification
|
||||||
|
</h4>
|
||||||
|
</div>
|
||||||
|
<div class="modal-body">
|
||||||
|
<!-- Creating spinner -->
|
||||||
|
<div class="quay-spinner" ng-show="status == 'creating' || status == 'authorizing-email'"></div>
|
||||||
|
|
||||||
|
<!-- Authorize e-mail view -->
|
||||||
|
<div ng-show="status == 'authorizing-email-sent'">
|
||||||
|
An e-mail has been sent to <code>{{ currentConfig.email }}</code>. Please click the link contained
|
||||||
|
in the e-mail.
|
||||||
|
<br><br>
|
||||||
|
Waiting... <span class="quay-spinner"></span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Authorize e-mail view -->
|
||||||
|
<div ng-show="status == 'unauthorized-email'">
|
||||||
|
The e-mail address <code>{{ currentConfig.email }}</code> has not been authorized to receive
|
||||||
|
notifications from this repository. Please click "Send Authorization E-mail" below to start
|
||||||
|
the authorization process.
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Create View -->
|
||||||
|
<table style="width: 100%" ng-show="status == ''">
|
||||||
|
<tr>
|
||||||
|
<td style="width: 120px">When this occurs:</td>
|
||||||
|
<td>
|
||||||
|
<div class="dropdown-select" placeholder="'(Notification Event)'" selected-item="currentEvent.title"
|
||||||
|
handle-item-selected="handleEventSelected(datum)" clear-value="clearCounter">
|
||||||
|
<!-- Icons -->
|
||||||
|
<i class="dropdown-select-icon fa fa-lg" ng-class="currentEvent.icon"></i>
|
||||||
|
|
||||||
|
<!-- Dropdown menu -->
|
||||||
|
<ul class="dropdown-select-menu pull-right" role="menu">
|
||||||
|
<li ng-repeat="event in events">
|
||||||
|
<a href="javascript:void(0)" ng-click="setEvent(event)">
|
||||||
|
<i class="fa fa-lg" ng-class="event.icon"></i> {{ event.title }}
|
||||||
|
</a>
|
||||||
|
</li>
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
|
||||||
|
<tr>
|
||||||
|
<td>Then issue a:</td>
|
||||||
|
<td>
|
||||||
|
<div class="dropdown-select" placeholder="'(Notification Action)'" selected-item="currentMethod.title"
|
||||||
|
handle-item-selected="handleMethodSelected(datum)" clear-value="clearCounter">
|
||||||
|
<!-- Icons -->
|
||||||
|
<i class="dropdown-select-icon fa fa-lg" ng-class="currentMethod.icon"></i>
|
||||||
|
|
||||||
|
<!-- Dropdown menu -->
|
||||||
|
<ul class="dropdown-select-menu pull-right" role="menu">
|
||||||
|
<li ng-repeat="method in methods">
|
||||||
|
<a href="javascript:void(0)" ng-click="setMethod(method)">
|
||||||
|
<i class="fa fa-lg" ng-class="method.icon"></i> {{ method.title }}
|
||||||
|
</a>
|
||||||
|
</li>
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
|
||||||
|
<tr ng-if="currentMethod.fields.length"><td colspan="2"><hr></td></tr>
|
||||||
|
|
||||||
|
<tr ng-repeat="field in currentMethod.fields">
|
||||||
|
<td>{{ field.title }}:</td>
|
||||||
|
<td>
|
||||||
|
<div ng-switch on="field.type">
|
||||||
|
<span ng-switch-when="email">
|
||||||
|
<input type="email" class="form-control" ng-model="currentConfig[field.name]" required>
|
||||||
|
</span>
|
||||||
|
<input type="url" class="form-control" ng-model="currentConfig[field.name]" ng-switch-when="url" required>
|
||||||
|
<input type="text" class="form-control" ng-model="currentConfig[field.name]" ng-switch-when="string" required>
|
||||||
|
<div class="entity-search" namespace="repository.namespace"
|
||||||
|
placeholder="''"
|
||||||
|
current-entity="currentConfig[field.name]"
|
||||||
|
ng-model="currentConfig[field.name]"
|
||||||
|
allowed-entities="['user', 'team', 'org']"
|
||||||
|
ng-switch-when="entity">
|
||||||
|
</div>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
|
||||||
|
<tr ng-if="currentMethod.id == 'webhook'">
|
||||||
|
<td colspan="2">
|
||||||
|
<div class="alert alert-info" style="margin-top: 20px; margin-bottom: 0px">
|
||||||
|
JSON metadata representing the event will be <b>POST</b>ed to the URL.
|
||||||
|
<br><br>
|
||||||
|
The contents for each event can be found in the user guide:
|
||||||
|
<a href="http://docs.quay.io/guides/notifications.html#webhook{{ currentEvent.id ? '_' + currentEvent.id : '' }}"
|
||||||
|
target="_blank">
|
||||||
|
http://docs.quay.io/guides/notifications.html
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Auth e-mail button bar -->
|
||||||
|
<div class="modal-footer" ng-if="status == 'unauthorized-email'">
|
||||||
|
<button type="button" class="btn btn-success" ng-click="sendAuthEmail()">
|
||||||
|
Send Authorization E-mail
|
||||||
|
</button>
|
||||||
|
<button type="button" class="btn btn-default" data-dismiss="modal" ng-disabled="creating">Cancel</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Normal button bar -->
|
||||||
|
<div class="modal-footer" ng-if="status == '' || status == 'creating'">
|
||||||
|
<button type="submit" class="btn btn-primary"
|
||||||
|
ng-disabled="createForm.$invalid || !currentMethod.id || !currentEvent.id || creating">
|
||||||
|
Create Notification
|
||||||
|
</button>
|
||||||
|
<button type="button" class="btn btn-default" data-dismiss="modal" ng-disabled="creating">Cancel</button>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
</div><!-- /.modal-content -->
|
||||||
|
</div><!-- /.modal-dialog -->
|
||||||
|
</div><!-- /.modal -->
|
|
@ -1,7 +1,8 @@
|
||||||
<div class="dropdown-select-element" ng-class="selectedItem ? 'has-item' : ''">
|
<div class="dropdown-select-element" ng-class="selectedItem ? 'has-item' : ''">
|
||||||
<div class="current-item">
|
<div class="current-item">
|
||||||
<div class="dropdown-select-icon-transclude"></div>
|
<div class="dropdown-select-icon-transclude"></div>
|
||||||
<input type="text" class="lookahead-input form-control" placeholder="{{ placeholder }}"></input>
|
<input type="text" class="lookahead-input form-control" placeholder="{{ placeholder }}"
|
||||||
|
ng-readonly="!lookaheadItems || !lookaheadItems.length"></input>
|
||||||
</div>
|
</div>
|
||||||
<div class="dropdown">
|
<div class="dropdown">
|
||||||
<button class="btn btn-default dropdown-toggle" type="button" data-toggle="dropdown">
|
<button class="btn btn-default dropdown-toggle" type="button" data-toggle="dropdown">
|
||||||
|
|
|
@ -6,7 +6,14 @@
|
||||||
<span ng-if="getIsAdmin(namespace)"><a href="/organization/{{ namespace }}/teams/{{ entity.name }}">{{entity.name}}</a></span>
|
<span ng-if="getIsAdmin(namespace)"><a href="/organization/{{ namespace }}/teams/{{ entity.name }}">{{entity.name}}</a></span>
|
||||||
</span>
|
</span>
|
||||||
</span>
|
</span>
|
||||||
<span ng-if="entity.kind != 'team'">
|
<span ng-if="entity.kind == 'org'">
|
||||||
|
<img src="//www.gravatar.com/avatar/{{ entity.gravatar }}?s=16&d=identicon">
|
||||||
|
<span class="entity-name">
|
||||||
|
<span ng-if="!getIsAdmin(entity.name)">{{entity.name}}</span>
|
||||||
|
<span ng-if="getIsAdmin(entity.name)"><a href="/organization/{{ entity.name }}">{{entity.name}}</a></span>
|
||||||
|
</span>
|
||||||
|
</span>
|
||||||
|
<span ng-if="entity.kind != 'team' && entity.kind != 'org'">
|
||||||
<i class="fa fa-user" ng-show="!entity.is_robot" data-title="User" bs-tooltip="tooltip.title" data-container="body"></i>
|
<i class="fa fa-user" ng-show="!entity.is_robot" data-title="User" bs-tooltip="tooltip.title" data-container="body"></i>
|
||||||
<i class="fa fa-wrench" ng-show="entity.is_robot" data-title="Robot Account" bs-tooltip="tooltip.title" data-container="body"></i>
|
<i class="fa fa-wrench" ng-show="entity.is_robot" data-title="Robot Account" bs-tooltip="tooltip.title" data-container="body"></i>
|
||||||
<span class="entity-name" ng-if="entity.is_robot">
|
<span class="entity-name" ng-if="entity.is_robot">
|
||||||
|
|
|
@ -1,5 +1,5 @@
|
||||||
<span class="entity-search-element" ng-class="isPersistent ? 'persistent' : ''"><input class="entity-search-control form-control">
|
<span class="entity-search-element" ng-class="autoClear ? '' : 'persistent'"><input class="entity-search-control form-control">
|
||||||
<span class="entity-reference block-reference" ng-show="isPersistent && currentEntityInternal" entity="currentEntityInternal"></span>
|
<span class="entity-reference block-reference" ng-show="!autoClear && currentEntityInternal" entity="currentEntityInternal"></span>
|
||||||
<div class="dropdown">
|
<div class="dropdown">
|
||||||
<button class="btn btn-default dropdown-toggle" type="button" id="entityDropdownMenu" data-toggle="dropdown"
|
<button class="btn btn-default dropdown-toggle" type="button" id="entityDropdownMenu" data-toggle="dropdown"
|
||||||
ng-click="lazyLoad()">
|
ng-click="lazyLoad()">
|
||||||
|
@ -12,6 +12,29 @@
|
||||||
You do not have permission to manage teams and robots for this organization
|
You do not have permission to manage teams and robots for this organization
|
||||||
</li>
|
</li>
|
||||||
|
|
||||||
|
<li role="presentation" class="dropdown-header"
|
||||||
|
ng-show="!lazyLoading && !teams.length && !robots.length && !((includeTeams && isOrganization && isAdmin) || (includeRobots && isAdmin))">
|
||||||
|
<span ng-if="includeRobots && includeTeams && isOrganization">
|
||||||
|
No robot accounts or teams found
|
||||||
|
</span>
|
||||||
|
<span ng-if="!includeRobots && includeTeams && isOrganization">
|
||||||
|
No teams found
|
||||||
|
</span>
|
||||||
|
<span ng-if="includeRobots && !includeTeams && isOrganization">
|
||||||
|
No robot accounts found
|
||||||
|
</span>
|
||||||
|
<span ng-if="!includeRobots && !includeTeams && isOrganization">
|
||||||
|
Robot accounts and teams are not permitted
|
||||||
|
</span>
|
||||||
|
|
||||||
|
<span ng-if="includeRobots && !isOrganization">
|
||||||
|
No robot accounts found
|
||||||
|
</span>
|
||||||
|
<span ng-if="!includeRobots && !isOrganization">
|
||||||
|
Robot accounts are not permitted
|
||||||
|
</span>
|
||||||
|
</li>
|
||||||
|
|
||||||
<li role="presentation" ng-repeat="team in teams" ng-show="!lazyLoading"
|
<li role="presentation" ng-repeat="team in teams" ng-show="!lazyLoading"
|
||||||
ng-click="setEntity(team.name, 'team', false)">
|
ng-click="setEntity(team.name, 'team', false)">
|
||||||
<a role="menuitem" tabindex="-1" href="javascript:void(0)">
|
<a role="menuitem" tabindex="-1" href="javascript:void(0)">
|
||||||
|
@ -34,7 +57,7 @@
|
||||||
<i class="fa fa-group"></i> Create team
|
<i class="fa fa-group"></i> Create team
|
||||||
</a>
|
</a>
|
||||||
</li>
|
</li>
|
||||||
<li role="presentation" ng-show="!lazyLoading && isAdmin">
|
<li role="presentation" ng-show="includeRobots && !lazyLoading && isAdmin">
|
||||||
<a role="menuitem" class="new-action" tabindex="-1" href="javascript:void(0)" ng-click="createRobot()">
|
<a role="menuitem" class="new-action" tabindex="-1" href="javascript:void(0)" ng-click="createRobot()">
|
||||||
<i class="fa fa-wrench"></i>
|
<i class="fa fa-wrench"></i>
|
||||||
Create robot account
|
Create robot account
|
||||||
|
|
62
static/directives/external-notification-view.html
Normal file
62
static/directives/external-notification-view.html
Normal file
|
@ -0,0 +1,62 @@
|
||||||
|
<div class="external-notification-view-element">
|
||||||
|
<div class="side-controls">
|
||||||
|
<div class="dropdown" style="display: inline-block">
|
||||||
|
<button class="btn btn-default dropdown-toggle" data-toggle="dropdown">
|
||||||
|
<i class="fa fa-cog"></i>
|
||||||
|
<b class="caret"></b>
|
||||||
|
</button>
|
||||||
|
<ul class="dropdown-menu dropdown-menu-right pull-right">
|
||||||
|
<li ng-if="methodInfo.id == 'webhook'">
|
||||||
|
<a href="http://docs.quay.io/guides/notifications.html#webhook_{{ eventInfo.id }}"
|
||||||
|
target="_blank">
|
||||||
|
<i class="fa fa-book"></i>
|
||||||
|
Webhook Documentation</a>
|
||||||
|
</li>
|
||||||
|
<li class="divider" ng-if="methodInfo.id == 'webhook'"></li>
|
||||||
|
<li><a href="javascript:void(0)" ng-click="testNotification()">
|
||||||
|
<i class="fa fa-send"></i>
|
||||||
|
Issue Test Notification</a>
|
||||||
|
</li>
|
||||||
|
<li><a href="javascript:void(0)" ng-click="deleteNotification()">
|
||||||
|
<i class="fa fa-times"></i>
|
||||||
|
Delete</a>
|
||||||
|
</li>
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="view-row">
|
||||||
|
<span class="flow-text">On</span>
|
||||||
|
<span class="notification-event">
|
||||||
|
<i class="fa fa-lg" ng-class="eventInfo.icon"></i>
|
||||||
|
{{ eventInfo.title }}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="view-row">
|
||||||
|
<span class="flow-text">Issue A</span>
|
||||||
|
<span class="notification-method">
|
||||||
|
<i class="fa fa-lg" ng-class="methodInfo.icon"></i>
|
||||||
|
{{ methodInfo.title }}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="view-row">
|
||||||
|
<span ng-switch on="methodInfo.id">
|
||||||
|
<span ng-switch-when="email">
|
||||||
|
<span class="flow-text">To</span>
|
||||||
|
<code>{{ config.email }}</code>
|
||||||
|
</span>
|
||||||
|
|
||||||
|
<span ng-switch-when="webhook">
|
||||||
|
<span class="flow-text">To</span>
|
||||||
|
<code>{{ config.url }}</code>
|
||||||
|
</span>
|
||||||
|
|
||||||
|
<span ng-switch-when="quay_notification">
|
||||||
|
<span class="flow-text">To</span>
|
||||||
|
<span class="entity-reference" entity="config.target" namespace="repository.namespace"></span>
|
||||||
|
</span>
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
|
@ -6,6 +6,11 @@
|
||||||
<img src="//www.gravatar.com/avatar/{{ getGravatar(notification.organization) }}?s=24&d=identicon" />
|
<img src="//www.gravatar.com/avatar/{{ getGravatar(notification.organization) }}?s=24&d=identicon" />
|
||||||
<span class="orgname">{{ notification.organization }}</span>
|
<span class="orgname">{{ notification.organization }}</span>
|
||||||
</div>
|
</div>
|
||||||
|
</div>
|
||||||
<div class="datetime">{{ parseDate(notification.created) | date:'medium'}}</div>
|
<div class="datetime">{{ parseDate(notification.created) | date:'medium'}}</div>
|
||||||
|
<div class="right-controls">
|
||||||
|
<a href="javascript:void(0)" ng-if="canDismiss(notification)" ng-click="dismissNotification(notification)">
|
||||||
|
Dismiss Notification
|
||||||
|
</a>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
@ -83,9 +83,11 @@
|
||||||
<tr ng-show="!newForWholeOrg">
|
<tr ng-show="!newForWholeOrg">
|
||||||
<td>Repository Creator:</td>
|
<td>Repository Creator:</td>
|
||||||
<td>
|
<td>
|
||||||
<span class="entity-search" namespace="organization.name" input-title="'User/Robot'"
|
<span class="entity-search" namespace="organization.name"
|
||||||
is-organization="true" include-teams="false" current-entity="activatingForNew" is-persistent="true"
|
placeholder="'User/Robot'"
|
||||||
clear-now="clearCounter">
|
allowed-entities="['user', 'robot']"
|
||||||
|
current-entity="activatingForNew"
|
||||||
|
clear-value="clearCounter">
|
||||||
</span>
|
</span>
|
||||||
</td>
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
|
@ -98,9 +100,9 @@
|
||||||
<tr>
|
<tr>
|
||||||
<td>Applied To:</td>
|
<td>Applied To:</td>
|
||||||
<td>
|
<td>
|
||||||
<span class="entity-search" namespace="organization.name" input-title="'User/Robot/Team'"
|
<span class="entity-search" namespace="organization.name" placeholder="'User/Robot/Team'"
|
||||||
is-organization="true" include-teams="true" current-entity="delegateForNew" is-persistent="true"
|
current-entity="delegateForNew"
|
||||||
clear-now="clearCounter">
|
clear-value="clearCounter">
|
||||||
</span>
|
</span>
|
||||||
</td>
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
|
|
|
@ -71,12 +71,10 @@
|
||||||
<td>
|
<td>
|
||||||
</td>
|
</td>
|
||||||
<td>
|
<td>
|
||||||
<div class="entity-search" namespace="repository.namespace" include-teams="false"
|
<div class="entity-search" namespace="repository.namespace"
|
||||||
input-title="'Select robot account for pulling...'"
|
placeholder="'Select robot account for pulling...'"
|
||||||
is-organization="repository.is_organization"
|
|
||||||
is-persistent="true"
|
|
||||||
current-entity="pullEntity"
|
current-entity="pullEntity"
|
||||||
filter="['robot']"></div>
|
allowed-entities="['robot']"></div>
|
||||||
|
|
||||||
<div class="alert alert-info" ng-if="pullRequirements.robots.length" style="margin-top: 20px; margin-bottom: 0px;">
|
<div class="alert alert-info" ng-if="pullRequirements.robots.length" style="margin-top: 20px; margin-bottom: 0px;">
|
||||||
Note: We've automatically selected robot account <span class="entity-reference" entity="pullRequirements.robots[0]"></span>, since it has access to the Quay.io repository.
|
Note: We've automatically selected robot account <span class="entity-reference" entity="pullRequirements.robots[0]"></span>, since it has access to the Quay.io repository.
|
||||||
|
|
548
static/js/app.js
548
static/js/app.js
|
@ -969,6 +969,106 @@ quayApp = angular.module('quay', quayDependencies, function($provide, cfpLoading
|
||||||
return userService;
|
return userService;
|
||||||
}]);
|
}]);
|
||||||
|
|
||||||
|
$provide.factory('ExternalNotificationData', [function() {
|
||||||
|
var externalNotificationData = {};
|
||||||
|
|
||||||
|
var events = [
|
||||||
|
{
|
||||||
|
'id': 'repo_push',
|
||||||
|
'title': 'Push to Repository',
|
||||||
|
'icon': 'fa-upload'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
'id': 'build_queued',
|
||||||
|
'title': 'Dockerfile Build Queued',
|
||||||
|
'icon': 'fa-tasks'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
'id': 'build_start',
|
||||||
|
'title': 'Dockerfile Build Started',
|
||||||
|
'icon': 'fa-circle-o-notch'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
'id': 'build_success',
|
||||||
|
'title': 'Dockerfile Build Successfully Completed',
|
||||||
|
'icon': 'fa-check-circle-o'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
'id': 'build_failure',
|
||||||
|
'title': 'Dockerfile Build Failed',
|
||||||
|
'icon': 'fa-times-circle-o'
|
||||||
|
}
|
||||||
|
];
|
||||||
|
|
||||||
|
var methods = [
|
||||||
|
{
|
||||||
|
'id': 'quay_notification',
|
||||||
|
'title': 'Quay.io Notification',
|
||||||
|
'icon': 'quay-icon',
|
||||||
|
'fields': [
|
||||||
|
{
|
||||||
|
'name': 'target',
|
||||||
|
'type': 'entity',
|
||||||
|
'title': 'Recipient'
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
'id': 'email',
|
||||||
|
'title': 'E-mail',
|
||||||
|
'icon': 'fa-envelope',
|
||||||
|
'fields': [
|
||||||
|
{
|
||||||
|
'name': 'email',
|
||||||
|
'type': 'email',
|
||||||
|
'title': 'E-mail address'
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
'id': 'webhook',
|
||||||
|
'title': 'Webhook POST',
|
||||||
|
'icon': 'fa-link',
|
||||||
|
'fields': [
|
||||||
|
{
|
||||||
|
'name': 'url',
|
||||||
|
'type': 'url',
|
||||||
|
'title': 'Webhook URL'
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
];
|
||||||
|
|
||||||
|
var methodMap = {};
|
||||||
|
var eventMap = {};
|
||||||
|
|
||||||
|
for (var i = 0; i < methods.length; ++i) {
|
||||||
|
methodMap[methods[i].id] = methods[i];
|
||||||
|
}
|
||||||
|
|
||||||
|
for (var i = 0; i < events.length; ++i) {
|
||||||
|
eventMap[events[i].id] = events[i];
|
||||||
|
}
|
||||||
|
|
||||||
|
externalNotificationData.getSupportedEvents = function() {
|
||||||
|
return events;
|
||||||
|
};
|
||||||
|
|
||||||
|
externalNotificationData.getSupportedMethods = function() {
|
||||||
|
return methods;
|
||||||
|
};
|
||||||
|
|
||||||
|
externalNotificationData.getEventInfo = function(event) {
|
||||||
|
return eventMap[event];
|
||||||
|
};
|
||||||
|
|
||||||
|
externalNotificationData.getMethodInfo = function(method) {
|
||||||
|
return methodMap[method];
|
||||||
|
};
|
||||||
|
|
||||||
|
return externalNotificationData;
|
||||||
|
}]);
|
||||||
|
|
||||||
$provide.factory('NotificationService', ['$rootScope', '$interval', 'UserService', 'ApiService', 'StringBuilderService', 'PlanService', 'UserService', 'Config',
|
$provide.factory('NotificationService', ['$rootScope', '$interval', 'UserService', 'ApiService', 'StringBuilderService', 'PlanService', 'UserService', 'Config',
|
||||||
function($rootScope, $interval, UserService, ApiService, StringBuilderService, PlanService, UserService, Config) {
|
function($rootScope, $interval, UserService, ApiService, StringBuilderService, PlanService, UserService, Config) {
|
||||||
var notificationService = {
|
var notificationService = {
|
||||||
|
@ -984,7 +1084,8 @@ quayApp = angular.module('quay', quayDependencies, function($provide, cfpLoading
|
||||||
'test_notification': {
|
'test_notification': {
|
||||||
'level': 'primary',
|
'level': 'primary',
|
||||||
'message': 'This notification is a long message for testing',
|
'message': 'This notification is a long message for testing',
|
||||||
'page': '/about/'
|
'page': '/about/',
|
||||||
|
'dismissable': true
|
||||||
},
|
},
|
||||||
'password_required': {
|
'password_required': {
|
||||||
'level': 'error',
|
'level': 'error',
|
||||||
|
@ -1015,9 +1116,59 @@ quayApp = angular.module('quay', quayDependencies, function($provide, cfpLoading
|
||||||
'message': 'We will be down for schedule maintenance from {from_date} to {to_date} ' +
|
'message': 'We will be down for schedule maintenance from {from_date} to {to_date} ' +
|
||||||
'for {reason}. We are sorry about any inconvenience.',
|
'for {reason}. We are sorry about any inconvenience.',
|
||||||
'page': 'http://status.quay.io/'
|
'page': 'http://status.quay.io/'
|
||||||
|
},
|
||||||
|
'repo_push': {
|
||||||
|
'level': 'info',
|
||||||
|
'message': 'Repository {repository} has been pushed with the following tags updated: {updated_tags}',
|
||||||
|
'page': function(metadata) {
|
||||||
|
return '/repository/' + metadata.repository;
|
||||||
|
},
|
||||||
|
'dismissable': true
|
||||||
|
},
|
||||||
|
'build_queued': {
|
||||||
|
'level': 'info',
|
||||||
|
'message': 'A build has been queued for repository {repository}',
|
||||||
|
'page': function(metadata) {
|
||||||
|
return '/repository/' + metadata.repository + '/build?current=' + metadata.build_id;
|
||||||
|
},
|
||||||
|
'dismissable': true
|
||||||
|
},
|
||||||
|
'build_start': {
|
||||||
|
'level': 'info',
|
||||||
|
'message': 'A build has been started for repository {repository}',
|
||||||
|
'page': function(metadata) {
|
||||||
|
return '/repository/' + metadata.repository + '/build?current=' + metadata.build_id;
|
||||||
|
},
|
||||||
|
'dismissable': true
|
||||||
|
},
|
||||||
|
'build_failure': {
|
||||||
|
'level': 'error',
|
||||||
|
'message': 'A build has failed for repository {repository}',
|
||||||
|
'page': function(metadata) {
|
||||||
|
return '/repository/' + metadata.repository + '/build?current=' + metadata.build_id;
|
||||||
|
},
|
||||||
|
'dismissable': true
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
notificationService.dismissNotification = function(notification) {
|
||||||
|
notification.dismissed = true;
|
||||||
|
var params = {
|
||||||
|
'uuid': notification.id
|
||||||
|
};
|
||||||
|
|
||||||
|
ApiService.updateUserNotification(notification, params);
|
||||||
|
|
||||||
|
var index = $.inArray(notification, notificationService.notifications);
|
||||||
|
if (index >= 0) {
|
||||||
|
notificationService.notifications.splice(index, 1);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
notificationService.canDismiss = function(notification) {
|
||||||
|
return !!notificationKinds[notification['kind']]['dismissable'];
|
||||||
|
};
|
||||||
|
|
||||||
notificationService.getPage = function(notification) {
|
notificationService.getPage = function(notification) {
|
||||||
var page = notificationKinds[notification['kind']]['page'];
|
var page = notificationKinds[notification['kind']]['page'];
|
||||||
if (typeof page != 'string') {
|
if (typeof page != 'string') {
|
||||||
|
@ -2274,7 +2425,8 @@ quayApp.directive('logsView', function () {
|
||||||
'repository': '=repository',
|
'repository': '=repository',
|
||||||
'performer': '=performer'
|
'performer': '=performer'
|
||||||
},
|
},
|
||||||
controller: function($scope, $element, $sce, Restangular, ApiService, TriggerDescriptionBuilder, StringBuilderService) {
|
controller: function($scope, $element, $sce, Restangular, ApiService, TriggerDescriptionBuilder,
|
||||||
|
StringBuilderService, ExternalNotificationData) {
|
||||||
$scope.loading = true;
|
$scope.loading = true;
|
||||||
$scope.logs = null;
|
$scope.logs = null;
|
||||||
$scope.kindsAllowed = null;
|
$scope.kindsAllowed = null;
|
||||||
|
@ -2335,8 +2487,6 @@ quayApp.directive('logsView', function () {
|
||||||
'change_repo_visibility': 'Change visibility for repository {repo} to {visibility}',
|
'change_repo_visibility': 'Change visibility for repository {repo} to {visibility}',
|
||||||
'add_repo_accesstoken': 'Create access token {token} in repository {repo}',
|
'add_repo_accesstoken': 'Create access token {token} in repository {repo}',
|
||||||
'delete_repo_accesstoken': 'Delete access token {token} in repository {repo}',
|
'delete_repo_accesstoken': 'Delete access token {token} in repository {repo}',
|
||||||
'add_repo_webhook': 'Add webhook in repository {repo}',
|
|
||||||
'delete_repo_webhook': 'Delete webhook in repository {repo}',
|
|
||||||
'set_repo_description': 'Change description for repository {repo}: {description}',
|
'set_repo_description': 'Change description for repository {repo}: {description}',
|
||||||
'build_dockerfile': function(metadata) {
|
'build_dockerfile': function(metadata) {
|
||||||
if (metadata.trigger_id) {
|
if (metadata.trigger_id) {
|
||||||
|
@ -2387,7 +2537,21 @@ quayApp.directive('logsView', function () {
|
||||||
'update_application': 'Update application to {application_name} for client ID {client_id}',
|
'update_application': 'Update application to {application_name} for client ID {client_id}',
|
||||||
'delete_application': 'Delete application {application_name} with client ID {client_id}',
|
'delete_application': 'Delete application {application_name} with client ID {client_id}',
|
||||||
'reset_application_client_secret': 'Reset the Client Secret of application {application_name} ' +
|
'reset_application_client_secret': 'Reset the Client Secret of application {application_name} ' +
|
||||||
'with client ID {client_id}'
|
'with client ID {client_id}',
|
||||||
|
|
||||||
|
'add_repo_notification': function(metadata) {
|
||||||
|
var eventData = ExternalNotificationData.getEventInfo(metadata.event);
|
||||||
|
return 'Add notification of event "' + eventData['title'] + '" for repository {repo}';
|
||||||
|
},
|
||||||
|
|
||||||
|
'delete_repo_notification': function(metadata) {
|
||||||
|
var eventData = ExternalNotificationData.getEventInfo(metadata.event);
|
||||||
|
return 'Delete notification of event "' + eventData['title'] + '" for repository {repo}';
|
||||||
|
},
|
||||||
|
|
||||||
|
// Note: These are deprecated.
|
||||||
|
'add_repo_webhook': 'Add webhook in repository {repo}',
|
||||||
|
'delete_repo_webhook': 'Delete webhook in repository {repo}'
|
||||||
};
|
};
|
||||||
|
|
||||||
var logKinds = {
|
var logKinds = {
|
||||||
|
@ -2406,8 +2570,6 @@ quayApp.directive('logsView', function () {
|
||||||
'change_repo_visibility': 'Change repository visibility',
|
'change_repo_visibility': 'Change repository visibility',
|
||||||
'add_repo_accesstoken': 'Create access token',
|
'add_repo_accesstoken': 'Create access token',
|
||||||
'delete_repo_accesstoken': 'Delete access token',
|
'delete_repo_accesstoken': 'Delete access token',
|
||||||
'add_repo_webhook': 'Add webhook',
|
|
||||||
'delete_repo_webhook': 'Delete webhook',
|
|
||||||
'set_repo_description': 'Change repository description',
|
'set_repo_description': 'Change repository description',
|
||||||
'build_dockerfile': 'Build image from Dockerfile',
|
'build_dockerfile': 'Build image from Dockerfile',
|
||||||
'delete_tag': 'Delete Tag',
|
'delete_tag': 'Delete Tag',
|
||||||
|
@ -2427,7 +2589,13 @@ quayApp.directive('logsView', function () {
|
||||||
'create_application': 'Create Application',
|
'create_application': 'Create Application',
|
||||||
'update_application': 'Update Application',
|
'update_application': 'Update Application',
|
||||||
'delete_application': 'Delete Application',
|
'delete_application': 'Delete Application',
|
||||||
'reset_application_client_secret': 'Reset Client Secret'
|
'reset_application_client_secret': 'Reset Client Secret',
|
||||||
|
'add_repo_notification': 'Add repository notification',
|
||||||
|
'delete_repo_notification': 'Delete repository notification',
|
||||||
|
|
||||||
|
// Note: these are deprecated.
|
||||||
|
'add_repo_webhook': 'Add webhook',
|
||||||
|
'delete_repo_webhook': 'Delete webhook'
|
||||||
};
|
};
|
||||||
|
|
||||||
var getDateString = function(date) {
|
var getDateString = function(date) {
|
||||||
|
@ -3127,48 +3295,73 @@ quayApp.directive('entitySearch', function () {
|
||||||
replace: false,
|
replace: false,
|
||||||
transclude: false,
|
transclude: false,
|
||||||
restrict: 'C',
|
restrict: 'C',
|
||||||
|
require: '?ngModel',
|
||||||
|
link: function(scope, element, attr, ctrl) {
|
||||||
|
scope.ngModel = ctrl;
|
||||||
|
},
|
||||||
scope: {
|
scope: {
|
||||||
'namespace': '=namespace',
|
'namespace': '=namespace',
|
||||||
'inputTitle': '=inputTitle',
|
'placeholder': '=placeholder',
|
||||||
'entitySelected': '=entitySelected',
|
|
||||||
'includeTeams': '=includeTeams',
|
// Default: ['user', 'team', 'robot']
|
||||||
'isOrganization': '=isOrganization',
|
'allowedEntities': '=allowedEntities',
|
||||||
'isPersistent': '=isPersistent',
|
|
||||||
'currentEntity': '=currentEntity',
|
'currentEntity': '=currentEntity',
|
||||||
'clearNow': '=clearNow',
|
'entitySelected': '&entitySelected',
|
||||||
'filter': '=filter',
|
|
||||||
|
// When set to true, the contents of the control will be cleared as soon
|
||||||
|
// as an entity is selected.
|
||||||
|
'autoClear': '=autoClear',
|
||||||
|
|
||||||
|
// Set this property to immediately clear the contents of the control.
|
||||||
|
'clearValue': '=clearValue',
|
||||||
},
|
},
|
||||||
controller: function($scope, $element, Restangular, UserService, ApiService) {
|
controller: function($rootScope, $scope, $element, Restangular, UserService, ApiService) {
|
||||||
$scope.lazyLoading = true;
|
$scope.lazyLoading = true;
|
||||||
|
|
||||||
|
$scope.teams = null;
|
||||||
|
$scope.robots = null;
|
||||||
|
|
||||||
$scope.isAdmin = false;
|
$scope.isAdmin = false;
|
||||||
|
$scope.isOrganization = false;
|
||||||
|
|
||||||
|
$scope.includeTeams = true;
|
||||||
|
$scope.includeRobots = true;
|
||||||
|
$scope.includeOrgs = false;
|
||||||
|
|
||||||
$scope.currentEntityInternal = $scope.currentEntity;
|
$scope.currentEntityInternal = $scope.currentEntity;
|
||||||
|
|
||||||
|
var isSupported = function(kind, opt_array) {
|
||||||
|
return $.inArray(kind, opt_array || $scope.allowedEntities || ['user', 'team', 'robot']) >= 0;
|
||||||
|
};
|
||||||
|
|
||||||
$scope.lazyLoad = function() {
|
$scope.lazyLoad = function() {
|
||||||
if (!$scope.namespace || !$scope.lazyLoading) { return; }
|
if (!$scope.namespace || !$scope.lazyLoading) { return; }
|
||||||
|
|
||||||
// Determine whether we can admin this namespace.
|
// Reset the cached teams and robots.
|
||||||
$scope.isAdmin = UserService.isNamespaceAdmin($scope.namespace);
|
|
||||||
|
|
||||||
// If the scope is an organization and we are not part of it, then nothing more we can do.
|
|
||||||
if (!$scope.isAdmin && $scope.isOrganization && !UserService.getOrganization($scope.namespace)) {
|
|
||||||
$scope.teams = null;
|
$scope.teams = null;
|
||||||
$scope.robots = null;
|
$scope.robots = null;
|
||||||
$scope.lazyLoading = false;
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
if ($scope.isOrganization && $scope.includeTeams) {
|
// Load the organization's teams (if applicable).
|
||||||
|
if ($scope.isOrganization && isSupported('team')) {
|
||||||
|
// Note: We load the org here again so that we always have the fully up-to-date
|
||||||
|
// teams list.
|
||||||
ApiService.getOrganization(null, {'orgname': $scope.namespace}).then(function(resp) {
|
ApiService.getOrganization(null, {'orgname': $scope.namespace}).then(function(resp) {
|
||||||
$scope.teams = resp.teams;
|
$scope.teams = resp.teams;
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Load the user/organization's robots (if applicable).
|
||||||
|
if ($scope.isAdmin && isSupported('robot')) {
|
||||||
ApiService.getRobots($scope.isOrganization ? $scope.namespace : null).then(function(resp) {
|
ApiService.getRobots($scope.isOrganization ? $scope.namespace : null).then(function(resp) {
|
||||||
$scope.robots = resp.robots;
|
$scope.robots = resp.robots;
|
||||||
$scope.lazyLoading = false;
|
$scope.lazyLoading = false;
|
||||||
}, function() {
|
}, function() {
|
||||||
$scope.lazyLoading = false;
|
$scope.lazyLoading = false;
|
||||||
});
|
});
|
||||||
|
} else {
|
||||||
|
$scope.lazyLoading = false;
|
||||||
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
$scope.createTeam = function() {
|
$scope.createTeam = function() {
|
||||||
|
@ -3216,7 +3409,7 @@ quayApp.directive('entitySearch', function () {
|
||||||
'is_robot': is_robot
|
'is_robot': is_robot
|
||||||
};
|
};
|
||||||
|
|
||||||
if ($scope.is_organization) {
|
if ($scope.isOrganization) {
|
||||||
entity['is_org_member'] = true;
|
entity['is_org_member'] = true;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -3226,60 +3419,69 @@ quayApp.directive('entitySearch', function () {
|
||||||
$scope.clearEntityInternal = function() {
|
$scope.clearEntityInternal = function() {
|
||||||
$scope.currentEntityInternal = null;
|
$scope.currentEntityInternal = null;
|
||||||
$scope.currentEntity = null;
|
$scope.currentEntity = null;
|
||||||
|
$scope.entitySelected({'entity': null});
|
||||||
if ($scope.entitySelected) {
|
if ($scope.ngModel) {
|
||||||
$scope.entitySelected(null);
|
$scope.ngModel.$setValidity('entity', false);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
$scope.setEntityInternal = function(entity, updateTypeahead) {
|
$scope.setEntityInternal = function(entity, updateTypeahead) {
|
||||||
if (updateTypeahead) {
|
if (updateTypeahead) {
|
||||||
$(input).typeahead('val', $scope.isPersistent ? entity.name : '');
|
$(input).typeahead('val', $scope.autoClear ? '' : entity.name);
|
||||||
} else {
|
} else {
|
||||||
$(input).val($scope.isPersistent ? entity.name : '');
|
$(input).val($scope.autoClear ? '' : entity.name);
|
||||||
}
|
}
|
||||||
|
|
||||||
if ($scope.isPersistent) {
|
if (!$scope.autoClear) {
|
||||||
$scope.currentEntityInternal = entity;
|
$scope.currentEntityInternal = entity;
|
||||||
$scope.currentEntity = entity;
|
$scope.currentEntity = entity;
|
||||||
}
|
}
|
||||||
|
|
||||||
if ($scope.entitySelected) {
|
$scope.entitySelected({'entity': entity});
|
||||||
$scope.entitySelected(entity);
|
if ($scope.ngModel) {
|
||||||
|
$scope.ngModel.$setValidity('entity', !!entity);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
number++;
|
// Setup the typeahead.
|
||||||
|
var input = $element[0].firstChild.firstChild;
|
||||||
|
|
||||||
|
(function() {
|
||||||
|
// Create the bloodhound search query system.
|
||||||
|
$rootScope.__entity_search_counter = (($rootScope.__entity_search_counter || 0) + 1);
|
||||||
var entitySearchB = new Bloodhound({
|
var entitySearchB = new Bloodhound({
|
||||||
name: 'entities' + number,
|
name: 'entities' + $rootScope.__entity_search_counter,
|
||||||
remote: {
|
remote: {
|
||||||
url: '/api/v1/entities/%QUERY',
|
url: '/api/v1/entities/%QUERY',
|
||||||
replace: function (url, uriEncodedQuery) {
|
replace: function (url, uriEncodedQuery) {
|
||||||
var namespace = $scope.namespace || '';
|
var namespace = $scope.namespace || '';
|
||||||
url = url.replace('%QUERY', uriEncodedQuery);
|
url = url.replace('%QUERY', uriEncodedQuery);
|
||||||
url += '?namespace=' + encodeURIComponent(namespace);
|
url += '?namespace=' + encodeURIComponent(namespace);
|
||||||
if ($scope.includeTeams) {
|
if ($scope.isOrganization && isSupported('team')) {
|
||||||
url += '&includeTeams=true'
|
url += '&includeTeams=true'
|
||||||
}
|
}
|
||||||
|
if (isSupported('org')) {
|
||||||
|
url += '&includeOrgs=true'
|
||||||
|
}
|
||||||
return url;
|
return url;
|
||||||
},
|
},
|
||||||
filter: function(data) {
|
filter: function(data) {
|
||||||
var datums = [];
|
var datums = [];
|
||||||
for (var i = 0; i < data.results.length; ++i) {
|
for (var i = 0; i < data.results.length; ++i) {
|
||||||
var entity = data.results[i];
|
var entity = data.results[i];
|
||||||
if ($scope.filter) {
|
|
||||||
var allowed = $scope.filter;
|
|
||||||
var found = 'user';
|
var found = 'user';
|
||||||
if (entity.kind == 'user') {
|
if (entity.kind == 'user') {
|
||||||
found = entity.is_robot ? 'robot' : 'user';
|
found = entity.is_robot ? 'robot' : 'user';
|
||||||
} else if (entity.kind == 'team') {
|
} else if (entity.kind == 'team') {
|
||||||
found = 'team';
|
found = 'team';
|
||||||
|
} else if (entity.kind == 'org') {
|
||||||
|
found = 'org';
|
||||||
}
|
}
|
||||||
if (allowed.indexOf(found)) {
|
|
||||||
|
if (!isSupported(found)) {
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
datums.push({
|
datums.push({
|
||||||
'value': entity.name,
|
'value': entity.name,
|
||||||
|
@ -3297,8 +3499,7 @@ quayApp.directive('entitySearch', function () {
|
||||||
});
|
});
|
||||||
entitySearchB.initialize();
|
entitySearchB.initialize();
|
||||||
|
|
||||||
var counter = 0;
|
// Setup the typeahead.
|
||||||
var input = $element[0].firstChild.firstChild;
|
|
||||||
$(input).typeahead({
|
$(input).typeahead({
|
||||||
'highlight': true
|
'highlight': true
|
||||||
}, {
|
}, {
|
||||||
|
@ -3316,9 +3517,31 @@ quayApp.directive('entitySearch', function () {
|
||||||
return '<div class="tt-empty">A Quay.io username (not an e-mail address) must be specified</div>';
|
return '<div class="tt-empty">A Quay.io username (not an e-mail address) must be specified</div>';
|
||||||
}
|
}
|
||||||
|
|
||||||
var robots = $scope.isOrganization ? ', robot accounts' : '';
|
var classes = [];
|
||||||
var teams = ($scope.includeTeams && $scope.isOrganization) ? ' or teams' : '';
|
|
||||||
return '<div class="tt-empty">No matching Quay.io users' + robots + teams + ' found</div>';
|
if (isSupported('user')) { classes.push('users'); }
|
||||||
|
if (isSupported('org')) { classes.push('organizations'); }
|
||||||
|
if ($scope.isAdmin && isSupported('robot')) { classes.push('robot accounts'); }
|
||||||
|
if ($scope.isOrganization && isSupported('team')) { classes.push('teams'); }
|
||||||
|
|
||||||
|
if (classes.length > 1) {
|
||||||
|
classes[classes.length - 1] = 'or ' + classes[classes.length - 1];
|
||||||
|
}
|
||||||
|
|
||||||
|
var class_string = '';
|
||||||
|
for (var i = 0; i < classes.length; ++i) {
|
||||||
|
if (i > 0) {
|
||||||
|
if (i == classes.length - 1) {
|
||||||
|
class_string += ' or ';
|
||||||
|
} else {
|
||||||
|
class_string += ', ';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class_string += classes[i];
|
||||||
|
}
|
||||||
|
|
||||||
|
return '<div class="tt-empty">No matching Quay.io ' + class_string + ' found</div>';
|
||||||
}
|
}
|
||||||
|
|
||||||
return null;
|
return null;
|
||||||
|
@ -3331,7 +3554,11 @@ quayApp.directive('entitySearch', function () {
|
||||||
template += '<i class="fa fa-wrench fa-lg"></i>';
|
template += '<i class="fa fa-wrench fa-lg"></i>';
|
||||||
} else if (datum.entity.kind == 'team') {
|
} else if (datum.entity.kind == 'team') {
|
||||||
template += '<i class="fa fa-group fa-lg"></i>';
|
template += '<i class="fa fa-group fa-lg"></i>';
|
||||||
|
} else if (datum.entity.kind == 'org') {
|
||||||
|
template += '<i class="fa"><img src="//www.gravatar.com/avatar/' +
|
||||||
|
datum.entity.gravatar + '?s=16&d=identicon"></i>';
|
||||||
}
|
}
|
||||||
|
|
||||||
template += '<span class="name">' + datum.value + '</span>';
|
template += '<span class="name">' + datum.value + '</span>';
|
||||||
|
|
||||||
if (datum.entity.is_org_member === false && datum.entity.kind == 'user') {
|
if (datum.entity.is_org_member === false && datum.entity.kind == 'user') {
|
||||||
|
@ -3345,9 +3572,7 @@ quayApp.directive('entitySearch', function () {
|
||||||
|
|
||||||
$(input).on('input', function(e) {
|
$(input).on('input', function(e) {
|
||||||
$scope.$apply(function() {
|
$scope.$apply(function() {
|
||||||
if ($scope.isPersistent) {
|
|
||||||
$scope.clearEntityInternal();
|
$scope.clearEntityInternal();
|
||||||
}
|
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
@ -3356,16 +3581,32 @@ quayApp.directive('entitySearch', function () {
|
||||||
$scope.setEntityInternal(datum.entity, true);
|
$scope.setEntityInternal(datum.entity, true);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
})();
|
||||||
|
|
||||||
|
$scope.$watch('clearValue', function() {
|
||||||
|
if (!input) { return; }
|
||||||
|
|
||||||
$scope.$watch('clearNow', function() {
|
|
||||||
$(input).typeahead('val', '');
|
$(input).typeahead('val', '');
|
||||||
$scope.clearEntityInternal();
|
$scope.clearEntityInternal();
|
||||||
});
|
});
|
||||||
|
|
||||||
$scope.$watch('inputTitle', function(title) {
|
$scope.$watch('placeholder', function(title) {
|
||||||
input.setAttribute('placeholder', title);
|
input.setAttribute('placeholder', title);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
$scope.$watch('allowedEntities', function(allowed) {
|
||||||
|
if (!allowed) { return; }
|
||||||
|
$scope.includeTeams = isSupported('team', allowed);
|
||||||
|
$scope.includeRobots = isSupported('robot', allowed);
|
||||||
|
});
|
||||||
|
|
||||||
|
$scope.$watch('namespace', function(namespace) {
|
||||||
|
if (!namespace) { return; }
|
||||||
|
|
||||||
|
$scope.isAdmin = UserService.isNamespaceAdmin(namespace);
|
||||||
|
$scope.isOrganization = !!UserService.getOrganization(namespace);
|
||||||
|
});
|
||||||
|
|
||||||
$scope.$watch('currentEntity', function(entity) {
|
$scope.$watch('currentEntity', function(entity) {
|
||||||
if ($scope.currentEntityInternal != entity) {
|
if ($scope.currentEntityInternal != entity) {
|
||||||
if (entity) {
|
if (entity) {
|
||||||
|
@ -3778,7 +4019,9 @@ quayApp.directive('dropdownSelect', function ($compile) {
|
||||||
'placeholder': '=placeholder',
|
'placeholder': '=placeholder',
|
||||||
'lookaheadItems': '=lookaheadItems',
|
'lookaheadItems': '=lookaheadItems',
|
||||||
'handleItemSelected': '&handleItemSelected',
|
'handleItemSelected': '&handleItemSelected',
|
||||||
'handleInput': '&handleInput'
|
'handleInput': '&handleInput',
|
||||||
|
|
||||||
|
'clearValue': '=clearValue'
|
||||||
},
|
},
|
||||||
controller: function($scope, $element, $rootScope) {
|
controller: function($scope, $element, $rootScope) {
|
||||||
if (!$rootScope.__dropdownSelectCounter) {
|
if (!$rootScope.__dropdownSelectCounter) {
|
||||||
|
@ -3791,6 +4034,13 @@ quayApp.directive('dropdownSelect', function ($compile) {
|
||||||
// Setup lookahead.
|
// Setup lookahead.
|
||||||
var input = $($element).find('.lookahead-input');
|
var input = $($element).find('.lookahead-input');
|
||||||
|
|
||||||
|
$scope.$watch('clearValue', function(cv) {
|
||||||
|
if (cv) {
|
||||||
|
$scope.selectedItem = null;
|
||||||
|
$(input).val('');
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
$scope.$watch('selectedItem', function(item) {
|
$scope.$watch('selectedItem', function(item) {
|
||||||
if ($scope.selectedItem == $scope.internalItem) {
|
if ($scope.selectedItem == $scope.internalItem) {
|
||||||
// The item has already been set due to an internal action.
|
// The item has already been set due to an internal action.
|
||||||
|
@ -4462,6 +4712,192 @@ quayApp.directive('buildProgress', function () {
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
||||||
|
quayApp.directive('externalNotificationView', function () {
|
||||||
|
var directiveDefinitionObject = {
|
||||||
|
priority: 0,
|
||||||
|
templateUrl: '/static/directives/external-notification-view.html',
|
||||||
|
replace: false,
|
||||||
|
transclude: false,
|
||||||
|
restrict: 'C',
|
||||||
|
scope: {
|
||||||
|
'repository': '=repository',
|
||||||
|
'notification': '=notification',
|
||||||
|
'notificationDeleted': '¬ificationDeleted'
|
||||||
|
},
|
||||||
|
controller: function($scope, $element, ExternalNotificationData, ApiService) {
|
||||||
|
$scope.deleteNotification = function() {
|
||||||
|
var params = {
|
||||||
|
'repository': $scope.repository.namespace + '/' + $scope.repository.name,
|
||||||
|
'uuid': $scope.notification.uuid
|
||||||
|
};
|
||||||
|
|
||||||
|
ApiService.deleteRepoNotification(null, params).then(function() {
|
||||||
|
$scope.notificationDeleted({'notification': $scope.notification});
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
$scope.testNotification = function() {
|
||||||
|
var params = {
|
||||||
|
'repository': $scope.repository.namespace + '/' + $scope.repository.name,
|
||||||
|
'uuid': $scope.notification.uuid
|
||||||
|
};
|
||||||
|
|
||||||
|
ApiService.testRepoNotification(null, params).then(function() {
|
||||||
|
bootbox.dialog({
|
||||||
|
"title": "Test Notification Queued",
|
||||||
|
"message": "A test version of this notification has been queued and should appear shortly",
|
||||||
|
"buttons": {
|
||||||
|
"close": {
|
||||||
|
"label": "Close",
|
||||||
|
"className": "btn-primary"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
$scope.$watch('notification', function(notification) {
|
||||||
|
if (notification) {
|
||||||
|
$scope.eventInfo = ExternalNotificationData.getEventInfo(notification.event);
|
||||||
|
$scope.methodInfo = ExternalNotificationData.getMethodInfo(notification.method);
|
||||||
|
$scope.config = notification.config;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
};
|
||||||
|
return directiveDefinitionObject;
|
||||||
|
});
|
||||||
|
|
||||||
|
|
||||||
|
quayApp.directive('createExternalNotificationDialog', function () {
|
||||||
|
var directiveDefinitionObject = {
|
||||||
|
priority: 0,
|
||||||
|
templateUrl: '/static/directives/create-external-notification-dialog.html',
|
||||||
|
replace: false,
|
||||||
|
transclude: false,
|
||||||
|
restrict: 'C',
|
||||||
|
scope: {
|
||||||
|
'repository': '=repository',
|
||||||
|
'counter': '=counter',
|
||||||
|
'notificationCreated': '¬ificationCreated'
|
||||||
|
},
|
||||||
|
controller: function($scope, $element, ExternalNotificationData, ApiService, $timeout) {
|
||||||
|
$scope.currentEvent = null;
|
||||||
|
$scope.currentMethod = null;
|
||||||
|
$scope.status = '';
|
||||||
|
$scope.currentConfig = {};
|
||||||
|
$scope.clearCounter = 0;
|
||||||
|
$scope.unauthorizedEmail = false;
|
||||||
|
|
||||||
|
$scope.events = ExternalNotificationData.getSupportedEvents();
|
||||||
|
$scope.methods = ExternalNotificationData.getSupportedMethods();
|
||||||
|
|
||||||
|
$scope.setEvent = function(event) {
|
||||||
|
$scope.currentEvent = event;
|
||||||
|
};
|
||||||
|
|
||||||
|
$scope.setMethod = function(method) {
|
||||||
|
$scope.currentConfig = {};
|
||||||
|
$scope.currentMethod = method;
|
||||||
|
$scope.unauthorizedEmail = false;
|
||||||
|
};
|
||||||
|
|
||||||
|
$scope.createNotification = function() {
|
||||||
|
if (!$scope.currentConfig.email) {
|
||||||
|
$scope.performCreateNotification();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
$scope.status = 'checking-email';
|
||||||
|
$scope.checkEmailAuthorization();
|
||||||
|
};
|
||||||
|
|
||||||
|
$scope.checkEmailAuthorization = function() {
|
||||||
|
var params = {
|
||||||
|
'repository': $scope.repository.namespace + '/' + $scope.repository.name,
|
||||||
|
'email': $scope.currentConfig.email
|
||||||
|
};
|
||||||
|
|
||||||
|
ApiService.checkRepoEmailAuthorized(null, params).then(function(resp) {
|
||||||
|
$scope.handleEmailCheck(resp.confirmed);
|
||||||
|
}, function(resp) {
|
||||||
|
$scope.handleEmailCheck(false);
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
$scope.performCreateNotification = function() {
|
||||||
|
$scope.status = 'creating';
|
||||||
|
|
||||||
|
var params = {
|
||||||
|
'repository': $scope.repository.namespace + '/' + $scope.repository.name
|
||||||
|
};
|
||||||
|
|
||||||
|
var data = {
|
||||||
|
'event': $scope.currentEvent.id,
|
||||||
|
'method': $scope.currentMethod.id,
|
||||||
|
'config': $scope.currentConfig
|
||||||
|
};
|
||||||
|
|
||||||
|
ApiService.createRepoNotification(data, params).then(function(resp) {
|
||||||
|
$scope.status = '';
|
||||||
|
$scope.notificationCreated({'notification': resp});
|
||||||
|
$('#createNotificationModal').modal('hide');
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
$scope.handleEmailCheck = function(isAuthorized) {
|
||||||
|
if (isAuthorized) {
|
||||||
|
$scope.performCreateNotification();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($scope.status == 'authorizing-email-sent') {
|
||||||
|
$scope.watchEmail();
|
||||||
|
} else {
|
||||||
|
$scope.status = 'unauthorized-email';
|
||||||
|
}
|
||||||
|
|
||||||
|
$scope.unauthorizedEmail = true;
|
||||||
|
};
|
||||||
|
|
||||||
|
$scope.sendAuthEmail = function() {
|
||||||
|
$scope.status = 'authorizing-email';
|
||||||
|
|
||||||
|
var params = {
|
||||||
|
'repository': $scope.repository.namespace + '/' + $scope.repository.name,
|
||||||
|
'email': $scope.currentConfig.email
|
||||||
|
};
|
||||||
|
|
||||||
|
ApiService.sendAuthorizeRepoEmail(null, params).then(function(resp) {
|
||||||
|
$scope.status = 'authorizing-email-sent';
|
||||||
|
$scope.watchEmail();
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
$scope.watchEmail = function() {
|
||||||
|
// TODO: change this to SSE?
|
||||||
|
$timeout(function() {
|
||||||
|
$scope.checkEmailAuthorization();
|
||||||
|
}, 1000);
|
||||||
|
};
|
||||||
|
|
||||||
|
$scope.$watch('counter', function(counter) {
|
||||||
|
if (counter) {
|
||||||
|
$scope.clearCounter++;
|
||||||
|
$scope.status = '';
|
||||||
|
$scope.currentEvent = null;
|
||||||
|
$scope.currentMethod = null;
|
||||||
|
$scope.unauthorizedEmail = false;
|
||||||
|
$('#createNotificationModal').modal({});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
};
|
||||||
|
return directiveDefinitionObject;
|
||||||
|
});
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
quayApp.directive('twitterView', function () {
|
quayApp.directive('twitterView', function () {
|
||||||
var directiveDefinitionObject = {
|
var directiveDefinitionObject = {
|
||||||
priority: 0,
|
priority: 0,
|
||||||
|
@ -4494,7 +4930,7 @@ quayApp.directive('notificationView', function () {
|
||||||
'notification': '=notification',
|
'notification': '=notification',
|
||||||
'parent': '=parent'
|
'parent': '=parent'
|
||||||
},
|
},
|
||||||
controller: function($scope, $element, $window, $location, UserService, NotificationService) {
|
controller: function($scope, $element, $window, $location, UserService, NotificationService, ApiService) {
|
||||||
var stringStartsWith = function (str, prefix) {
|
var stringStartsWith = function (str, prefix) {
|
||||||
return str.slice(0, prefix.length) == prefix;
|
return str.slice(0, prefix.length) == prefix;
|
||||||
};
|
};
|
||||||
|
@ -4530,6 +4966,14 @@ quayApp.directive('notificationView', function () {
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
$scope.dismissNotification = function(notification) {
|
||||||
|
NotificationService.dismissNotification(notification);
|
||||||
|
};
|
||||||
|
|
||||||
|
$scope.canDismiss = function(notification) {
|
||||||
|
return NotificationService.canDismiss(notification);
|
||||||
|
};
|
||||||
|
|
||||||
$scope.getClass = function(notification) {
|
$scope.getClass = function(notification) {
|
||||||
return NotificationService.getClass(notification);
|
return NotificationService.getClass(notification);
|
||||||
};
|
};
|
||||||
|
|
|
@ -181,7 +181,7 @@ function TutorialCtrl($scope, AngularTour, AngularTourSignals, UserService, Conf
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
'title': 'Repository Admin',
|
'title': 'Repository Admin',
|
||||||
'content': "The repository admin panel allows for modification of a repository's permissions, webhooks, visibility and other settings",
|
'content': "The repository admin panel allows for modification of a repository's permissions, notifications, visibility and other settings",
|
||||||
'overlayable': true,
|
'overlayable': true,
|
||||||
'mixpanelEvent': 'tutorial_view_admin'
|
'mixpanelEvent': 'tutorial_view_admin'
|
||||||
},
|
},
|
||||||
|
@ -1258,7 +1258,7 @@ function RepoBuildCtrl($scope, Restangular, ApiService, $routeParams, $rootScope
|
||||||
fetchRepository();
|
fetchRepository();
|
||||||
}
|
}
|
||||||
|
|
||||||
function RepoAdminCtrl($scope, Restangular, ApiService, KeyService, $routeParams, $rootScope, $location, UserService, Config, Features) {
|
function RepoAdminCtrl($scope, Restangular, ApiService, KeyService, $routeParams, $rootScope, $location, UserService, Config, Features, ExternalNotificationData) {
|
||||||
var namespace = $routeParams.namespace;
|
var namespace = $routeParams.namespace;
|
||||||
var name = $routeParams.name;
|
var name = $routeParams.name;
|
||||||
|
|
||||||
|
@ -1481,42 +1481,31 @@ function RepoAdminCtrl($scope, Restangular, ApiService, KeyService, $routeParams
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
$scope.loadWebhooks = function() {
|
$scope.showNewNotificationCounter = 0;
|
||||||
|
|
||||||
|
$scope.showNewNotificationDialog = function() {
|
||||||
|
$scope.showNewNotificationCounter++;
|
||||||
|
};
|
||||||
|
|
||||||
|
$scope.handleNotificationCreated = function(notification) {
|
||||||
|
$scope.notifications.push(notification);
|
||||||
|
};
|
||||||
|
|
||||||
|
$scope.handleNotificationDeleted = function(notification) {
|
||||||
|
var index = $.inArray(notification, $scope.notifications);
|
||||||
|
if (index < 0) { return; }
|
||||||
|
$scope.notifications.splice(index, 1);
|
||||||
|
};
|
||||||
|
|
||||||
|
$scope.loadNotifications = function() {
|
||||||
var params = {
|
var params = {
|
||||||
'repository': namespace + '/' + name
|
'repository': namespace + '/' + name
|
||||||
};
|
};
|
||||||
|
|
||||||
$scope.newWebhook = {};
|
$scope.notificationsResource = ApiService.listRepoNotificationsAsResource(params).get(
|
||||||
$scope.webhooksResource = ApiService.listWebhooksAsResource(params).get(function(resp) {
|
function(resp) {
|
||||||
$scope.webhooks = resp.webhooks;
|
$scope.notifications = resp.notifications;
|
||||||
return $scope.webhooks;
|
return $scope.notifications;
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
$scope.createWebhook = function() {
|
|
||||||
if (!$scope.newWebhook.url) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
var params = {
|
|
||||||
'repository': namespace + '/' + name
|
|
||||||
};
|
|
||||||
|
|
||||||
ApiService.createWebhook($scope.newWebhook, params).then(function(resp) {
|
|
||||||
$scope.webhooks.push(resp);
|
|
||||||
$scope.newWebhook.url = '';
|
|
||||||
$scope.createWebhookForm.$setPristine();
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
$scope.deleteWebhook = function(webhook) {
|
|
||||||
var params = {
|
|
||||||
'repository': namespace + '/' + name,
|
|
||||||
'public_id': webhook.public_id
|
|
||||||
};
|
|
||||||
|
|
||||||
ApiService.deleteWebhook(null, params).then(function(resp) {
|
|
||||||
$scope.webhooks.splice($scope.webhooks.indexOf(webhook), 1);
|
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@ -1649,7 +1638,7 @@ function RepoAdminCtrl($scope, Restangular, ApiService, KeyService, $routeParams
|
||||||
$scope.repo = repo;
|
$scope.repo = repo;
|
||||||
$rootScope.title = 'Settings - ' + namespace + '/' + name;
|
$rootScope.title = 'Settings - ' + namespace + '/' + name;
|
||||||
$rootScope.description = 'Administrator settings for ' + namespace + '/' + name +
|
$rootScope.description = 'Administrator settings for ' + namespace + '/' + name +
|
||||||
': Permissions, webhooks and other settings';
|
': Permissions, notifications and other settings';
|
||||||
|
|
||||||
// Fetch all the permissions and token info for the repository.
|
// Fetch all the permissions and token info for the repository.
|
||||||
fetchPermissions('user');
|
fetchPermissions('user');
|
||||||
|
|
|
@ -1,5 +1,5 @@
|
||||||
<div class="container">
|
<div class="container">
|
||||||
<h3>Redirecting...</h3>
|
<h3>Redirecting...</h3>
|
||||||
<META http-equiv="refresh" content="0;URL=http://docs.quay.io/getting-started.html">
|
<META http-equiv="refresh" content="0;URL=http://docs.quay.io/solution/getting-started.html">
|
||||||
If this page does not redirect, please <a href="http://docs.quay.io/getting-started.html"> click here</a>.
|
If this page does not redirect, please <a href="http://docs.quay.io/solution/getting-started.html"> click here</a>.
|
||||||
</div>
|
</div>
|
||||||
|
|
|
@ -20,7 +20,7 @@
|
||||||
<li class="active"><a href="javascript:void(0)" data-toggle="tab" data-target="#permissions">Permissions</a></li>
|
<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()">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="#badge">Status Badge</a></li>
|
||||||
<li><a href="javascript:void(0)" data-toggle="tab" data-target="#webhook" ng-click="loadWebhooks()">Webhooks</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>
|
<li><a href="javascript:void(0)" data-toggle="tab" data-target="#publicprivate">Public/Private</a></li>
|
||||||
<li><a href="javascript:void(0)" data-toggle="tab" data-target="#delete">Delete</a></li>
|
<li><a href="javascript:void(0)" data-toggle="tab" data-target="#delete">Delete</a></li>
|
||||||
<li><a href="javascript:void(0)" data-toggle="tab" data-target="#logs" ng-click="loadLogs()">Usage Logs</a></li>
|
<li><a href="javascript:void(0)" data-toggle="tab" data-target="#logs" ng-click="loadLogs()">Usage Logs</a></li>
|
||||||
|
@ -135,10 +135,11 @@
|
||||||
|
|
||||||
<tr>
|
<tr>
|
||||||
<td id="add-entity-permission" colspan="2" class="admin-search">
|
<td id="add-entity-permission" colspan="2" class="admin-search">
|
||||||
<span class="entity-search" namespace="repo.namespace" include-teams="true"
|
<span class="entity-search" namespace="repo.namespace"
|
||||||
input-title="'Add a ' + (repo.is_organization ? 'team or ' : '') + 'user...'"
|
placeholder="'Add a ' + (repo.is_organization ? 'team or ' : '') + 'user...'"
|
||||||
entity-selected="addNewPermission" is-organization="repo.is_organization"
|
entity-selected="addNewPermission(entity)"
|
||||||
current-entity="selectedEntity"></span>
|
current-entity="selectedEntity"
|
||||||
|
auto-clear="true"></span>
|
||||||
</td>
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
</table>
|
</table>
|
||||||
|
@ -192,51 +193,32 @@
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Webhook tab -->
|
<!-- Notification tab -->
|
||||||
<div id="webhook" class="tab-pane">
|
<div id="notification" class="tab-pane">
|
||||||
<div class="panel panel-default">
|
<div class="panel panel-default">
|
||||||
<div class="panel-heading">Push Webhooks
|
<div class="panel-heading">Repository Notifications
|
||||||
<i class="info-icon fa fa-info-circle" data-placement="left" data-content="URLs which will be invoked with an HTTP POST and JSON payload when a successful push to the repository occurs."></i>
|
<i class="info-icon fa fa-info-circle" data-placement="left" data-content="Notifications to call to external services (web, email, etc) on repository events"></i>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="panel-body">
|
<div class="panel-body">
|
||||||
<div class="resource-view" resource="webhooksResource" error-message="'Could not load webhooks'">
|
<!-- Notifications list -->
|
||||||
<table class="permissions">
|
<div class="resource-view" resource="notificationsResource" error-message="'Could not load notifications'">
|
||||||
<thead>
|
<div class="empty" ng-if="!notifications.length">
|
||||||
<tr>
|
There are no notifications defined for this repository
|
||||||
<td style="width: 500px;">Webhook URL</td>
|
</div>
|
||||||
<td></td>
|
<div class="nonempty" ng-show="notifications.length">
|
||||||
</tr>
|
<div class="external-notification-view" notification="notification" repository="repo"
|
||||||
</thead>
|
notification-deleted="handleNotificationDeleted(notification)"
|
||||||
|
ng-repeat="notification in notifications"></div>
|
||||||
<tbody>
|
</div>
|
||||||
<tr ng-repeat="webhook in webhooks">
|
|
||||||
<td>{{ webhook.parameters.url }}</td>
|
|
||||||
<td>
|
|
||||||
<span class="delete-ui" delete-title="'Delete Webhook'" perform-delete="deleteWebhook(webhook)"></span>
|
|
||||||
</td>
|
|
||||||
</tr>
|
|
||||||
</tbody>
|
|
||||||
</table>
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<form name="createWebhookForm" ng-submit="createWebhook()">
|
<!-- Right controls -->
|
||||||
<table class="permissions">
|
<div class="right-controls">
|
||||||
<tbody>
|
<button class="btn btn-success" ng-click="showNewNotificationDialog()">
|
||||||
<tr>
|
<i class="fa fa-paper-plane"></i>
|
||||||
<td style="width: 500px;">
|
New Notification
|
||||||
<input type="url" class="form-control" placeholder="New webhook url..." ng-model="newWebhook.url" required>
|
</button>
|
||||||
</td>
|
|
||||||
<td>
|
|
||||||
<button class="btn btn-primary" type="submit" ng-disabled="createWebhookForm.$invalid">Create</button>
|
|
||||||
</td>
|
|
||||||
</tr>
|
|
||||||
</tbody>
|
|
||||||
</table>
|
|
||||||
</form>
|
|
||||||
|
|
||||||
<div class="right-info">
|
|
||||||
Quay will <b>POST</b> to these webhooks whenever a push occurs. See the <a href="/guide">User Guide</a> for more information.
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
@ -390,6 +372,11 @@
|
||||||
canceled="cancelSetupTrigger(trigger)"
|
canceled="cancelSetupTrigger(trigger)"
|
||||||
counter="showTriggerSetupCounter"></div>
|
counter="showTriggerSetupCounter"></div>
|
||||||
|
|
||||||
|
<!-- New notification dialog-->
|
||||||
|
<div class="create-external-notification-dialog" repository="repo"
|
||||||
|
counter="showNewNotificationCounter"
|
||||||
|
notification-created="handleNotificationCreated(notification)"></div>
|
||||||
|
|
||||||
<!-- Modal message dialog -->
|
<!-- Modal message dialog -->
|
||||||
<div class="modal fade" id="cannotchangeModal">
|
<div class="modal fade" id="cannotchangeModal">
|
||||||
<div class="modal-dialog">
|
<div class="modal-dialog">
|
||||||
|
|
|
@ -41,7 +41,7 @@
|
||||||
<div class="alert alert-info">
|
<div class="alert alert-info">
|
||||||
<h4 ng-show="namespace == user.username">You don't have any repositories yet!</h4>
|
<h4 ng-show="namespace == user.username">You don't have any repositories yet!</h4>
|
||||||
<h4 ng-show="namespace != user.username">This organization doesn't have any repositories, or you have not been provided access.</h4>
|
<h4 ng-show="namespace != user.username">This organization doesn't have any repositories, or you have not been provided access.</h4>
|
||||||
<a href="http://docs.quay.io/getting-started.html"><b>Click here</b> to learn how to create a repository</a>
|
<a href="http://docs.quay.io/solution/getting-started.html"><b>Click here</b> to learn how to create a repository</a>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
@ -23,10 +23,13 @@
|
||||||
</tr>
|
</tr>
|
||||||
|
|
||||||
<tr ng-show="canEditMembers">
|
<tr ng-show="canEditMembers">
|
||||||
<td colspan="2">
|
<td colspan="3">
|
||||||
<span class="entity-search" namespace="orgname" include-teams="false" input-title="'Add a Quay.io user...'"
|
<div class="entity-search" style="width: 100%"
|
||||||
entity-selected="addNewMember" is-organization="true"
|
namespace="orgname" placeholder="'Add a Quay.io user or robot...'"
|
||||||
current-entity="selectedMember"></span>
|
entity-selected="addNewMember(entity)"
|
||||||
|
current-entity="selectedMember"
|
||||||
|
auto-clear="true"
|
||||||
|
allowed-entities="['user', 'robot']"></div>
|
||||||
</td>
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
</table>
|
</table>
|
||||||
|
|
14
templates/message.html
Normal file
14
templates/message.html
Normal file
|
@ -0,0 +1,14 @@
|
||||||
|
<html>
|
||||||
|
<title>Quay.io</title>
|
||||||
|
<head>
|
||||||
|
<link rel="stylesheet" href="//netdna.bootstrapcdn.com/font-awesome/4.0.3/css/font-awesome.css">
|
||||||
|
<link rel="stylesheet" href="//netdna.bootstrapcdn.com/bootstrap/3.0.0/css/bootstrap.no-icons.min.css">
|
||||||
|
<link href='//fonts.googleapis.com/css?family=Droid+Sans:400,700' rel='stylesheet' type='text/css'>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<div class="container" style="margin-top: 20px">
|
||||||
|
<img src="/static/img/quay-logo.png">
|
||||||
|
<h5>{{ message | safe }}</h5>
|
||||||
|
</div>
|
||||||
|
</body>
|
||||||
|
</html>
|
Binary file not shown.
|
@ -18,9 +18,10 @@ from endpoints.api.robot import UserRobotList, OrgRobot, OrgRobotList, UserRobot
|
||||||
from endpoints.api.trigger import (BuildTriggerActivate, BuildTriggerSources, BuildTriggerSubdirs,
|
from endpoints.api.trigger import (BuildTriggerActivate, BuildTriggerSources, BuildTriggerSubdirs,
|
||||||
TriggerBuildList, ActivateBuildTrigger, BuildTrigger,
|
TriggerBuildList, ActivateBuildTrigger, BuildTrigger,
|
||||||
BuildTriggerList, BuildTriggerAnalyze)
|
BuildTriggerList, BuildTriggerAnalyze)
|
||||||
from endpoints.api.webhook import Webhook, WebhookList
|
from endpoints.api.repoemail import RepositoryAuthorizedEmail
|
||||||
|
from endpoints.api.repositorynotification import RepositoryNotification, RepositoryNotificationList
|
||||||
from endpoints.api.user import (PrivateRepositories, ConvertToOrganization, Recovery, Signout,
|
from endpoints.api.user import (PrivateRepositories, ConvertToOrganization, Recovery, Signout,
|
||||||
Signin, User, UserAuthorizationList, UserAuthorization)
|
Signin, User, UserAuthorizationList, UserAuthorization, UserNotification)
|
||||||
from endpoints.api.repotoken import RepositoryToken, RepositoryTokenList
|
from endpoints.api.repotoken import RepositoryToken, RepositoryTokenList
|
||||||
from endpoints.api.prototype import PermissionPrototype, PermissionPrototypeList
|
from endpoints.api.prototype import PermissionPrototype, PermissionPrototypeList
|
||||||
from endpoints.api.logs import UserLogs, OrgLogs, RepositoryLogs
|
from endpoints.api.logs import UserLogs, OrgLogs, RepositoryLogs
|
||||||
|
@ -122,6 +123,37 @@ class TestFindRepositories(ApiTestCase):
|
||||||
self._run_test('GET', 200, 'devtable', None)
|
self._run_test('GET', 200, 'devtable', None)
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
class TestUserNotification(ApiTestCase):
|
||||||
|
def setUp(self):
|
||||||
|
ApiTestCase.setUp(self)
|
||||||
|
self._set_url(UserNotification, uuid='someuuid')
|
||||||
|
|
||||||
|
def test_get_anonymous(self):
|
||||||
|
self._run_test('GET', 401, None, None)
|
||||||
|
|
||||||
|
def test_get_freshuser(self):
|
||||||
|
self._run_test('GET', 404, 'freshuser', None)
|
||||||
|
|
||||||
|
def test_get_reader(self):
|
||||||
|
self._run_test('GET', 404, 'reader', None)
|
||||||
|
|
||||||
|
def test_get_devtable(self):
|
||||||
|
self._run_test('GET', 404, 'devtable', None)
|
||||||
|
|
||||||
|
def test_put_anonymous(self):
|
||||||
|
self._run_test('PUT', 401, None, {})
|
||||||
|
|
||||||
|
def test_put_freshuser(self):
|
||||||
|
self._run_test('PUT', 404, 'freshuser', {})
|
||||||
|
|
||||||
|
def test_put_reader(self):
|
||||||
|
self._run_test('PUT', 404, 'reader', {})
|
||||||
|
|
||||||
|
def test_put_devtable(self):
|
||||||
|
self._run_test('PUT', 404, 'devtable', {})
|
||||||
|
|
||||||
|
|
||||||
class TestUserInvoiceList(ApiTestCase):
|
class TestUserInvoiceList(ApiTestCase):
|
||||||
def setUp(self):
|
def setUp(self):
|
||||||
ApiTestCase.setUp(self)
|
ApiTestCase.setUp(self)
|
||||||
|
@ -1883,10 +1915,10 @@ class TestBuildTriggerD6tiBuynlargeOrgrepo(ApiTestCase):
|
||||||
self._run_test('DELETE', 404, 'devtable', None)
|
self._run_test('DELETE', 404, 'devtable', None)
|
||||||
|
|
||||||
|
|
||||||
class TestWebhookQfatPublicPublicrepo(ApiTestCase):
|
class TestRepositoryNotificationQfatPublicPublicrepo(ApiTestCase):
|
||||||
def setUp(self):
|
def setUp(self):
|
||||||
ApiTestCase.setUp(self)
|
ApiTestCase.setUp(self)
|
||||||
self._set_url(Webhook, public_id="QFAT", repository="public/publicrepo")
|
self._set_url(RepositoryNotification, uuid="QFAT", repository="public/publicrepo")
|
||||||
|
|
||||||
def test_get_anonymous(self):
|
def test_get_anonymous(self):
|
||||||
self._run_test('GET', 401, None, None)
|
self._run_test('GET', 401, None, None)
|
||||||
|
@ -1913,10 +1945,10 @@ class TestWebhookQfatPublicPublicrepo(ApiTestCase):
|
||||||
self._run_test('DELETE', 403, 'devtable', None)
|
self._run_test('DELETE', 403, 'devtable', None)
|
||||||
|
|
||||||
|
|
||||||
class TestWebhookQfatDevtableShared(ApiTestCase):
|
class TestRepositoryNotificationQfatDevtableShared(ApiTestCase):
|
||||||
def setUp(self):
|
def setUp(self):
|
||||||
ApiTestCase.setUp(self)
|
ApiTestCase.setUp(self)
|
||||||
self._set_url(Webhook, public_id="QFAT", repository="devtable/shared")
|
self._set_url(RepositoryNotification, uuid="QFAT", repository="devtable/shared")
|
||||||
|
|
||||||
def test_get_anonymous(self):
|
def test_get_anonymous(self):
|
||||||
self._run_test('GET', 401, None, None)
|
self._run_test('GET', 401, None, None)
|
||||||
|
@ -1943,10 +1975,10 @@ class TestWebhookQfatDevtableShared(ApiTestCase):
|
||||||
self._run_test('DELETE', 400, 'devtable', None)
|
self._run_test('DELETE', 400, 'devtable', None)
|
||||||
|
|
||||||
|
|
||||||
class TestWebhookQfatBuynlargeOrgrepo(ApiTestCase):
|
class TestRepositoryNotificationQfatBuynlargeOrgrepo(ApiTestCase):
|
||||||
def setUp(self):
|
def setUp(self):
|
||||||
ApiTestCase.setUp(self)
|
ApiTestCase.setUp(self)
|
||||||
self._set_url(Webhook, public_id="QFAT", repository="buynlarge/orgrepo")
|
self._set_url(RepositoryNotification, uuid="QFAT", repository="buynlarge/orgrepo")
|
||||||
|
|
||||||
def test_get_anonymous(self):
|
def test_get_anonymous(self):
|
||||||
self._run_test('GET', 401, None, None)
|
self._run_test('GET', 401, None, None)
|
||||||
|
@ -2529,10 +2561,10 @@ class TestBuildTriggerListBuynlargeOrgrepo(ApiTestCase):
|
||||||
self._run_test('GET', 200, 'devtable', None)
|
self._run_test('GET', 200, 'devtable', None)
|
||||||
|
|
||||||
|
|
||||||
class TestWebhookListPublicPublicrepo(ApiTestCase):
|
class TestRepositoryNotificationListPublicPublicrepo(ApiTestCase):
|
||||||
def setUp(self):
|
def setUp(self):
|
||||||
ApiTestCase.setUp(self)
|
ApiTestCase.setUp(self)
|
||||||
self._set_url(WebhookList, repository="public/publicrepo")
|
self._set_url(RepositoryNotificationList, repository="public/publicrepo")
|
||||||
|
|
||||||
def test_get_anonymous(self):
|
def test_get_anonymous(self):
|
||||||
self._run_test('GET', 401, None, None)
|
self._run_test('GET', 401, None, None)
|
||||||
|
@ -2559,10 +2591,10 @@ class TestWebhookListPublicPublicrepo(ApiTestCase):
|
||||||
self._run_test('POST', 403, 'devtable', {})
|
self._run_test('POST', 403, 'devtable', {})
|
||||||
|
|
||||||
|
|
||||||
class TestWebhookListDevtableShared(ApiTestCase):
|
class TestRepositoryNotificationListDevtableShared(ApiTestCase):
|
||||||
def setUp(self):
|
def setUp(self):
|
||||||
ApiTestCase.setUp(self)
|
ApiTestCase.setUp(self)
|
||||||
self._set_url(WebhookList, repository="devtable/shared")
|
self._set_url(RepositoryNotificationList, repository="devtable/shared")
|
||||||
|
|
||||||
def test_get_anonymous(self):
|
def test_get_anonymous(self):
|
||||||
self._run_test('GET', 401, None, None)
|
self._run_test('GET', 401, None, None)
|
||||||
|
@ -2586,13 +2618,14 @@ class TestWebhookListDevtableShared(ApiTestCase):
|
||||||
self._run_test('POST', 403, 'reader', {})
|
self._run_test('POST', 403, 'reader', {})
|
||||||
|
|
||||||
def test_post_devtable(self):
|
def test_post_devtable(self):
|
||||||
self._run_test('POST', 201, 'devtable', {})
|
self._run_test('POST', 400, 'devtable', {'event': 'repo_push', 'method': 'email',
|
||||||
|
'config': {'email': 'a@b.com'}})
|
||||||
|
|
||||||
|
|
||||||
class TestWebhookListBuynlargeOrgrepo(ApiTestCase):
|
class TestRepositoryNotificationListBuynlargeOrgrepo(ApiTestCase):
|
||||||
def setUp(self):
|
def setUp(self):
|
||||||
ApiTestCase.setUp(self)
|
ApiTestCase.setUp(self)
|
||||||
self._set_url(WebhookList, repository="buynlarge/orgrepo")
|
self._set_url(RepositoryNotificationList, repository="buynlarge/orgrepo")
|
||||||
|
|
||||||
def test_get_anonymous(self):
|
def test_get_anonymous(self):
|
||||||
self._run_test('GET', 401, None, None)
|
self._run_test('GET', 401, None, None)
|
||||||
|
@ -2616,7 +2649,102 @@ class TestWebhookListBuynlargeOrgrepo(ApiTestCase):
|
||||||
self._run_test('POST', 403, 'reader', {})
|
self._run_test('POST', 403, 'reader', {})
|
||||||
|
|
||||||
def test_post_devtable(self):
|
def test_post_devtable(self):
|
||||||
self._run_test('POST', 201, 'devtable', {})
|
self._run_test('POST', 400, 'devtable', {'event': 'repo_push', 'method': 'email',
|
||||||
|
'config': {'email': 'a@b.com'}})
|
||||||
|
|
||||||
|
|
||||||
|
class TestRepositoryAuthorizedEmailPublicPublicrepo(ApiTestCase):
|
||||||
|
def setUp(self):
|
||||||
|
ApiTestCase.setUp(self)
|
||||||
|
self._set_url(RepositoryAuthorizedEmail, repository="public/publicrepo",
|
||||||
|
email="jschorr@devtable.com")
|
||||||
|
|
||||||
|
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', 403, 'devtable', None)
|
||||||
|
|
||||||
|
def test_post_anonymous(self):
|
||||||
|
self._run_test('POST', 401, None, {})
|
||||||
|
|
||||||
|
def test_post_freshuser(self):
|
||||||
|
self._run_test('POST', 403, 'freshuser', {})
|
||||||
|
|
||||||
|
def test_post_reader(self):
|
||||||
|
self._run_test('POST', 403, 'reader', {})
|
||||||
|
|
||||||
|
def test_post_devtable(self):
|
||||||
|
self._run_test('POST', 403, 'devtable', {})
|
||||||
|
|
||||||
|
|
||||||
|
class TestRepositoryAuthorizedEmailDevtableSharedrepo(ApiTestCase):
|
||||||
|
def setUp(self):
|
||||||
|
ApiTestCase.setUp(self)
|
||||||
|
self._set_url(RepositoryAuthorizedEmail, repository="devtable/shared",
|
||||||
|
email="jschorr@devtable.com")
|
||||||
|
|
||||||
|
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', 404, 'devtable', None)
|
||||||
|
|
||||||
|
def test_post_anonymous(self):
|
||||||
|
self._run_test('POST', 401, None, {})
|
||||||
|
|
||||||
|
def test_post_freshuser(self):
|
||||||
|
self._run_test('POST', 403, 'freshuser', {})
|
||||||
|
|
||||||
|
def test_post_reader(self):
|
||||||
|
self._run_test('POST', 403, 'reader', {})
|
||||||
|
|
||||||
|
def test_post_devtable(self):
|
||||||
|
self._run_test('POST', 200, 'devtable', {})
|
||||||
|
|
||||||
|
|
||||||
|
class TestRepositoryAuthorizedEmailBuynlargeOrgrepo(ApiTestCase):
|
||||||
|
def setUp(self):
|
||||||
|
ApiTestCase.setUp(self)
|
||||||
|
self._set_url(RepositoryAuthorizedEmail, repository="buynlarge/orgrepo",
|
||||||
|
email="jschorr@devtable.com")
|
||||||
|
|
||||||
|
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', 404, 'devtable', None)
|
||||||
|
|
||||||
|
def test_post_anonymous(self):
|
||||||
|
self._run_test('POST', 401, None, {})
|
||||||
|
|
||||||
|
def test_post_freshuser(self):
|
||||||
|
self._run_test('POST', 403, 'freshuser', {})
|
||||||
|
|
||||||
|
def test_post_reader(self):
|
||||||
|
self._run_test('POST', 403, 'reader', {})
|
||||||
|
|
||||||
|
def test_post_devtable(self):
|
||||||
|
self._run_test('POST', 200, 'devtable', {})
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
class TestRepositoryTokenListPublicPublicrepo(ApiTestCase):
|
class TestRepositoryTokenListPublicPublicrepo(ApiTestCase):
|
||||||
|
|
|
@ -20,9 +20,11 @@ from endpoints.api.robot import UserRobotList, OrgRobot, OrgRobotList, UserRobot
|
||||||
from endpoints.api.trigger import (BuildTriggerActivate, BuildTriggerSources, BuildTriggerSubdirs,
|
from endpoints.api.trigger import (BuildTriggerActivate, BuildTriggerSources, BuildTriggerSubdirs,
|
||||||
TriggerBuildList, ActivateBuildTrigger, BuildTrigger,
|
TriggerBuildList, ActivateBuildTrigger, BuildTrigger,
|
||||||
BuildTriggerList, BuildTriggerAnalyze)
|
BuildTriggerList, BuildTriggerAnalyze)
|
||||||
from endpoints.api.webhook import Webhook, WebhookList
|
from endpoints.api.repoemail import RepositoryAuthorizedEmail
|
||||||
|
from endpoints.api.repositorynotification import RepositoryNotification, RepositoryNotificationList
|
||||||
from endpoints.api.user import (PrivateRepositories, ConvertToOrganization, Signout, Signin, User,
|
from endpoints.api.user import (PrivateRepositories, ConvertToOrganization, Signout, Signin, User,
|
||||||
UserAuthorizationList, UserAuthorization)
|
UserAuthorizationList, UserAuthorization, UserNotification,
|
||||||
|
UserNotificationList)
|
||||||
|
|
||||||
from endpoints.api.repotoken import RepositoryToken, RepositoryTokenList
|
from endpoints.api.repotoken import RepositoryToken, RepositoryTokenList
|
||||||
from endpoints.api.prototype import PermissionPrototype, PermissionPrototypeList
|
from endpoints.api.prototype import PermissionPrototype, PermissionPrototypeList
|
||||||
|
@ -207,6 +209,26 @@ class TestLoggedInUser(ApiTestCase):
|
||||||
assert json['username'] == READ_ACCESS_USER
|
assert json['username'] == READ_ACCESS_USER
|
||||||
|
|
||||||
|
|
||||||
|
class TestUserNotification(ApiTestCase):
|
||||||
|
def test_get(self):
|
||||||
|
self.login(ADMIN_ACCESS_USER)
|
||||||
|
json = self.getJsonResponse(UserNotificationList)
|
||||||
|
|
||||||
|
# Make sure each notification can be retrieved.
|
||||||
|
for notification in json['notifications']:
|
||||||
|
njson = self.getJsonResponse(UserNotification, params=dict(uuid=notification['id']))
|
||||||
|
self.assertEquals(notification['id'], njson['id'])
|
||||||
|
|
||||||
|
# Update a notification.
|
||||||
|
assert json['notifications']
|
||||||
|
assert not json['notifications'][0]['dismissed']
|
||||||
|
|
||||||
|
pjson = self.putJsonResponse(UserNotification, params=dict(uuid=notification['id']),
|
||||||
|
data=dict(dismissed=True))
|
||||||
|
|
||||||
|
self.assertEquals(True, pjson['dismissed'])
|
||||||
|
|
||||||
|
|
||||||
class TestGetUserPrivateAllowed(ApiTestCase):
|
class TestGetUserPrivateAllowed(ApiTestCase):
|
||||||
def test_nonallowed(self):
|
def test_nonallowed(self):
|
||||||
self.login(READ_ACCESS_USER)
|
self.login(READ_ACCESS_USER)
|
||||||
|
@ -388,6 +410,16 @@ class TestGetMatchingEntities(ApiTestCase):
|
||||||
assert 'outsideorg' in names
|
assert 'outsideorg' in names
|
||||||
assert 'owners' in names
|
assert 'owners' in names
|
||||||
|
|
||||||
|
def test_inorg_withorgs(self):
|
||||||
|
self.login(ADMIN_ACCESS_USER)
|
||||||
|
|
||||||
|
json = self.getJsonResponse(EntitySearch,
|
||||||
|
params=dict(prefix=ORGANIZATION[0], namespace=ORGANIZATION,
|
||||||
|
includeOrgs='true'))
|
||||||
|
|
||||||
|
names = set([r['name'] for r in json['results']])
|
||||||
|
assert ORGANIZATION in names
|
||||||
|
|
||||||
|
|
||||||
class TestCreateOrganization(ApiTestCase):
|
class TestCreateOrganization(ApiTestCase):
|
||||||
def test_existinguser(self):
|
def test_existinguser(self):
|
||||||
|
@ -1072,42 +1104,89 @@ class TestRequestRepoBuild(ApiTestCase):
|
||||||
expected_code=403)
|
expected_code=403)
|
||||||
|
|
||||||
|
|
||||||
|
class TestRepositoryEmail(ApiTestCase):
|
||||||
|
def test_emailnotauthorized(self):
|
||||||
|
self.login(ADMIN_ACCESS_USER)
|
||||||
|
|
||||||
class TestWebhooks(ApiTestCase):
|
# Verify the e-mail address is not authorized.
|
||||||
|
json = self.getResponse(RepositoryAuthorizedEmail,
|
||||||
|
params=dict(repository=ADMIN_ACCESS_USER + '/simple', email='test@example.com'),
|
||||||
|
expected_code=404)
|
||||||
|
|
||||||
|
def test_emailnotauthorized_butsent(self):
|
||||||
|
self.login(ADMIN_ACCESS_USER)
|
||||||
|
|
||||||
|
# Verify the e-mail address is not authorized.
|
||||||
|
json = self.getJsonResponse(RepositoryAuthorizedEmail,
|
||||||
|
params=dict(repository=ADMIN_ACCESS_USER + '/simple', email='jschorr+other@devtable.com'))
|
||||||
|
|
||||||
|
self.assertEquals(False, json['confirmed'])
|
||||||
|
self.assertEquals(ADMIN_ACCESS_USER, json['namespace'])
|
||||||
|
self.assertEquals('simple', json['repository'])
|
||||||
|
|
||||||
|
|
||||||
|
def test_emailauthorized(self):
|
||||||
|
self.login(ADMIN_ACCESS_USER)
|
||||||
|
|
||||||
|
# Verify the e-mail address is authorized.
|
||||||
|
json = self.getJsonResponse(RepositoryAuthorizedEmail,
|
||||||
|
params=dict(repository=ADMIN_ACCESS_USER + '/simple', email='jschorr@devtable.com'))
|
||||||
|
|
||||||
|
self.assertEquals(True, json['confirmed'])
|
||||||
|
self.assertEquals(ADMIN_ACCESS_USER, json['namespace'])
|
||||||
|
self.assertEquals('simple', json['repository'])
|
||||||
|
|
||||||
|
|
||||||
|
def test_send_email_authorization(self):
|
||||||
|
self.login(ADMIN_ACCESS_USER)
|
||||||
|
|
||||||
|
# Send the email.
|
||||||
|
json = self.postJsonResponse(RepositoryAuthorizedEmail,
|
||||||
|
params=dict(repository=ADMIN_ACCESS_USER + '/simple', email='jschorr+foo@devtable.com'))
|
||||||
|
|
||||||
|
self.assertEquals(False, json['confirmed'])
|
||||||
|
self.assertEquals(ADMIN_ACCESS_USER, json['namespace'])
|
||||||
|
self.assertEquals('simple', json['repository'])
|
||||||
|
|
||||||
|
|
||||||
|
class TestRepositoryNotifications(ApiTestCase):
|
||||||
def test_webhooks(self):
|
def test_webhooks(self):
|
||||||
self.login(ADMIN_ACCESS_USER)
|
self.login(ADMIN_ACCESS_USER)
|
||||||
|
|
||||||
# Add a webhook.
|
# Add a notification.
|
||||||
json = self.postJsonResponse(WebhookList,
|
json = self.postJsonResponse(RepositoryNotificationList,
|
||||||
params=dict(repository=ADMIN_ACCESS_USER + '/simple'),
|
params=dict(repository=ADMIN_ACCESS_USER + '/simple'),
|
||||||
data=dict(url='http://example.com'),
|
data=dict(config={'url': 'http://example.com'}, event='repo_push', method='webhook'),
|
||||||
expected_code=201)
|
expected_code=201)
|
||||||
|
|
||||||
self.assertEquals('http://example.com', json['parameters']['url'])
|
self.assertEquals('repo_push', json['event'])
|
||||||
wid = json['public_id']
|
self.assertEquals('webhook', json['method'])
|
||||||
|
self.assertEquals('http://example.com', json['config']['url'])
|
||||||
|
wid = json['uuid']
|
||||||
|
|
||||||
# Get the webhook.
|
# Get the notification.
|
||||||
json = self.getJsonResponse(Webhook,
|
json = self.getJsonResponse(RepositoryNotification,
|
||||||
params=dict(repository=ADMIN_ACCESS_USER + '/simple', public_id=wid))
|
params=dict(repository=ADMIN_ACCESS_USER + '/simple', uuid=wid))
|
||||||
|
|
||||||
self.assertEquals(wid, json['public_id'])
|
self.assertEquals(wid, json['uuid'])
|
||||||
self.assertEquals('http://example.com', json['parameters']['url'])
|
self.assertEquals('repo_push', json['event'])
|
||||||
|
self.assertEquals('webhook', json['method'])
|
||||||
|
|
||||||
# Verify the webhook is listed.
|
# Verify the notification is listed.
|
||||||
json = self.getJsonResponse(WebhookList,
|
json = self.getJsonResponse(RepositoryNotificationList,
|
||||||
params=dict(repository=ADMIN_ACCESS_USER + '/simple'))
|
params=dict(repository=ADMIN_ACCESS_USER + '/simple'))
|
||||||
|
|
||||||
ids = [w['public_id'] for w in json['webhooks']]
|
ids = [w['uuid'] for w in json['notifications']]
|
||||||
assert wid in ids
|
assert wid in ids
|
||||||
|
|
||||||
# Delete the webhook.
|
# Delete the notification.
|
||||||
self.deleteResponse(Webhook,
|
self.deleteResponse(RepositoryNotification,
|
||||||
params=dict(repository=ADMIN_ACCESS_USER + '/simple', public_id=wid),
|
params=dict(repository=ADMIN_ACCESS_USER + '/simple', uuid=wid),
|
||||||
expected_code=204)
|
expected_code=204)
|
||||||
|
|
||||||
# Verify the webhook is gone.
|
# Verify the notification is gone.
|
||||||
self.getResponse(Webhook,
|
self.getResponse(RepositoryNotification,
|
||||||
params=dict(repository=ADMIN_ACCESS_USER + '/simple', public_id=wid),
|
params=dict(repository=ADMIN_ACCESS_USER + '/simple', uuid=wid),
|
||||||
expected_code=404)
|
expected_code=404)
|
||||||
|
|
||||||
|
|
||||||
|
|
|
@ -1,33 +1,33 @@
|
||||||
from flask.ext.mail import Message
|
from flask.ext.mail import Message
|
||||||
|
|
||||||
from app import mail, app
|
from app import mail, app, get_app_url
|
||||||
|
|
||||||
|
|
||||||
CONFIRM_MESSAGE = """
|
CONFIRM_MESSAGE = """
|
||||||
This email address was recently used to register the username '%s'
|
This email address was recently used to register the username '%s'
|
||||||
at <a href="https://quay.io">Quay.io</a>.<br>
|
at <a href="%s">Quay.io</a>.<br>
|
||||||
<br>
|
<br>
|
||||||
To confirm this email address, please click the following link:<br>
|
To confirm this email address, please click the following link:<br>
|
||||||
<a href="https://quay.io/confirm?code=%s">https://quay.io/confirm?code=%s</a>
|
<a href="%s/confirm?code=%s">%s/confirm?code=%s</a>
|
||||||
"""
|
"""
|
||||||
|
|
||||||
|
|
||||||
CHANGE_MESSAGE = """
|
CHANGE_MESSAGE = """
|
||||||
This email address was recently asked to become the new e-mail address for username '%s'
|
This email address was recently asked to become the new e-mail address for username '%s'
|
||||||
at <a href="https://quay.io">Quay.io</a>.<br>
|
at <a href="%s">Quay.io</a>.<br>
|
||||||
<br>
|
<br>
|
||||||
To confirm this email address, please click the following link:<br>
|
To confirm this email address, please click the following link:<br>
|
||||||
<a href="https://quay.io/confirm?code=%s">https://quay.io/confirm?code=%s</a>
|
<a href="%s/confirm?code=%s">%s/confirm?code=%s</a>
|
||||||
"""
|
"""
|
||||||
|
|
||||||
|
|
||||||
RECOVERY_MESSAGE = """
|
RECOVERY_MESSAGE = """
|
||||||
A user at <a href="https://quay.io">Quay.io</a> has attempted to recover their account
|
A user at <a href="%s">Quay.io</a> has attempted to recover their account
|
||||||
using this email.<br>
|
using this email address.<br>
|
||||||
<br>
|
<br>
|
||||||
If you made this request, please click the following link to recover your account and
|
If you made this request, please click the following link to recover your account and
|
||||||
change your password:
|
change your password:
|
||||||
<a href="https://quay.io/recovery?code=%s">https://quay.io/recovery?code=%s</a><br>
|
<a href="%s/recovery?code=%s">%s/recovery?code=%s</a><br>
|
||||||
<br>
|
<br>
|
||||||
If you did not make this request, your account has not been compromised and the user was
|
If you did not make this request, your account has not been compromised and the user was
|
||||||
not given access. Please disregard this email.<br>
|
not given access. Please disregard this email.<br>
|
||||||
|
@ -57,6 +57,15 @@ Thanks and have a great day!<br>
|
||||||
"""
|
"""
|
||||||
|
|
||||||
|
|
||||||
|
AUTH_FORREPO_MESSAGE = """
|
||||||
|
A request has been made to send notifications to this email address for the
|
||||||
|
<a href="%s">Quay.io</a> repository <a href="%s/repository/%s/%s">%s/%s</a>.
|
||||||
|
<br>
|
||||||
|
To confirm this email address, please click the following link:<br>
|
||||||
|
<a href="%s/authrepoemail?code=%s">%s/authrepoemail?code=%s</a>
|
||||||
|
"""
|
||||||
|
|
||||||
|
|
||||||
SUBSCRIPTION_CHANGE_TITLE = 'Subscription Change - {0} {1}'
|
SUBSCRIPTION_CHANGE_TITLE = 'Subscription Change - {0} {1}'
|
||||||
|
|
||||||
|
|
||||||
|
@ -64,7 +73,7 @@ def send_change_email(username, email, token):
|
||||||
msg = Message('Quay.io email change. Please confirm your email.',
|
msg = Message('Quay.io email change. Please confirm your email.',
|
||||||
sender='support@quay.io', # Why do I need this?
|
sender='support@quay.io', # Why do I need this?
|
||||||
recipients=[email])
|
recipients=[email])
|
||||||
msg.html = CHANGE_MESSAGE % (username, token, token)
|
msg.html = CHANGE_MESSAGE % (username, get_app_url(), get_app_url(), token, get_app_url(), token)
|
||||||
mail.send(msg)
|
mail.send(msg)
|
||||||
|
|
||||||
|
|
||||||
|
@ -72,7 +81,16 @@ def send_confirmation_email(username, email, token):
|
||||||
msg = Message('Welcome to Quay.io! Please confirm your email.',
|
msg = Message('Welcome to Quay.io! Please confirm your email.',
|
||||||
sender='support@quay.io', # Why do I need this?
|
sender='support@quay.io', # Why do I need this?
|
||||||
recipients=[email])
|
recipients=[email])
|
||||||
msg.html = CONFIRM_MESSAGE % (username, token, token)
|
msg.html = CONFIRM_MESSAGE % (username, get_app_url(), get_app_url(), token, get_app_url(), token)
|
||||||
|
mail.send(msg)
|
||||||
|
|
||||||
|
|
||||||
|
def send_repo_authorization_email(namespace, repository, email, token):
|
||||||
|
msg = Message('Quay.io Notification: Please confirm your email.',
|
||||||
|
sender='support@quay.io', # Why do I need this?
|
||||||
|
recipients=[email])
|
||||||
|
msg.html = AUTH_FORREPO_MESSAGE % (get_app_url(), get_app_url(), namespace, repository, namespace,
|
||||||
|
repository, get_app_url(), token, get_app_url(), token)
|
||||||
mail.send(msg)
|
mail.send(msg)
|
||||||
|
|
||||||
|
|
||||||
|
@ -80,7 +98,7 @@ def send_recovery_email(email, token):
|
||||||
msg = Message('Quay.io account recovery.',
|
msg = Message('Quay.io account recovery.',
|
||||||
sender='support@quay.io', # Why do I need this?
|
sender='support@quay.io', # Why do I need this?
|
||||||
recipients=[email])
|
recipients=[email])
|
||||||
msg.html = RECOVERY_MESSAGE % (token, token)
|
msg.html = RECOVERY_MESSAGE % (get_app_url(), get_app_url(), token, get_app_url(), token)
|
||||||
mail.send(msg)
|
mail.send(msg)
|
||||||
|
|
||||||
|
|
||||||
|
|
|
@ -25,6 +25,7 @@ from requests.exceptions import ConnectionError
|
||||||
from data import model
|
from data import model
|
||||||
from workers.worker import Worker, WorkerUnhealthyException, JobException
|
from workers.worker import Worker, WorkerUnhealthyException, JobException
|
||||||
from app import userfiles as user_files, build_logs, sentry, dockerfile_build_queue
|
from app import userfiles as user_files, build_logs, sentry, dockerfile_build_queue
|
||||||
|
from endpoints.notificationhelper import spawn_notification
|
||||||
from util.safetar import safe_extractall
|
from util.safetar import safe_extractall
|
||||||
from util.dockerfileparse import parse_dockerfile, ParsedDockerfile, serialize_dockerfile
|
from util.dockerfileparse import parse_dockerfile, ParsedDockerfile, serialize_dockerfile
|
||||||
|
|
||||||
|
@ -529,6 +530,27 @@ class DockerfileBuildWorker(Worker):
|
||||||
|
|
||||||
build_dir = self._mime_processors[c_type](docker_resource)
|
build_dir = self._mime_processors[c_type](docker_resource)
|
||||||
|
|
||||||
|
# Spawn a notification that the build has started.
|
||||||
|
event_data = {
|
||||||
|
'build_id': repository_build.uuid,
|
||||||
|
'build_name': repository_build.display_name,
|
||||||
|
'docker_tags': tag_names,
|
||||||
|
'trigger_id': repository_build.trigger.uuid,
|
||||||
|
'trigger_kind': repository_build.trigger.service.name
|
||||||
|
}
|
||||||
|
|
||||||
|
spawn_notification(repository_build.repository, 'build_start', event_data,
|
||||||
|
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
|
||||||
|
spawn_notification(repository_build.repository, 'build_failure', event_data,
|
||||||
|
subpage='build?current=%s' % repository_build.uuid,
|
||||||
|
pathargs=['build', repository_build.uuid])
|
||||||
|
|
||||||
|
# Start the build process.
|
||||||
try:
|
try:
|
||||||
with DockerfileBuildContext(build_dir, build_subdir, repo, tag_names, access_token,
|
with DockerfileBuildContext(build_dir, build_subdir, repo, tag_names, access_token,
|
||||||
repository_build.uuid, self._cache_size_gb,
|
repository_build.uuid, self._cache_size_gb,
|
||||||
|
@ -569,12 +591,23 @@ class DockerfileBuildWorker(Worker):
|
||||||
repository_build.phase = 'complete'
|
repository_build.phase = 'complete'
|
||||||
repository_build.save()
|
repository_build.save()
|
||||||
|
|
||||||
|
# Spawn a notification that the build has completed.
|
||||||
|
spawn_notification(repository_build.repository, 'build_success', event_data,
|
||||||
|
subpage='build?current=%s' % repository_build.uuid,
|
||||||
|
pathargs=['build', repository_build.uuid])
|
||||||
|
|
||||||
except WorkerUnhealthyException as exc:
|
except WorkerUnhealthyException as exc:
|
||||||
# Need a separate handler for this so it doesn't get caught by catch all below
|
# Spawn a notification that the build has failed.
|
||||||
|
spawn_failure(exc.message, event_data)
|
||||||
|
|
||||||
|
# Raise the exception to the queue.
|
||||||
raise exc
|
raise exc
|
||||||
|
|
||||||
except JobException as exc:
|
except JobException as exc:
|
||||||
# Need a separate handler for this so it doesn't get caught by catch all below
|
# Spawn a notification that the build has failed.
|
||||||
|
spawn_failure(exc.message, event_data)
|
||||||
|
|
||||||
|
# Raise the exception to the queue.
|
||||||
raise exc
|
raise exc
|
||||||
|
|
||||||
except ConnectionError as exc:
|
except ConnectionError as exc:
|
||||||
|
@ -584,12 +617,18 @@ class DockerfileBuildWorker(Worker):
|
||||||
raise WorkerUnhealthyException(exc.message)
|
raise WorkerUnhealthyException(exc.message)
|
||||||
|
|
||||||
except Exception as exc:
|
except Exception as exc:
|
||||||
|
# Spawn a notification that the build has failed.
|
||||||
|
spawn_failure(exc.message, event_data)
|
||||||
|
|
||||||
|
# Write the error to the logs.
|
||||||
sentry.client.captureException()
|
sentry.client.captureException()
|
||||||
log_appender('error', build_logs.PHASE)
|
log_appender('error', build_logs.PHASE)
|
||||||
logger.exception('Exception when processing request.')
|
logger.exception('Exception when processing request.')
|
||||||
repository_build.phase = 'error'
|
repository_build.phase = 'error'
|
||||||
repository_build.save()
|
repository_build.save()
|
||||||
log_appender(str(exc), build_logs.ERROR)
|
log_appender(str(exc), build_logs.ERROR)
|
||||||
|
|
||||||
|
# Raise the exception to the queue.
|
||||||
raise JobException(str(exc))
|
raise JobException(str(exc))
|
||||||
|
|
||||||
|
|
||||||
|
|
53
workers/notificationworker.py
Normal file
53
workers/notificationworker.py
Normal file
|
@ -0,0 +1,53 @@
|
||||||
|
import logging
|
||||||
|
import argparse
|
||||||
|
import requests
|
||||||
|
import json
|
||||||
|
|
||||||
|
from app import notification_queue
|
||||||
|
from workers.worker import Worker
|
||||||
|
|
||||||
|
from endpoints.notificationmethod import NotificationMethod, InvalidNotificationMethodException
|
||||||
|
from endpoints.notificationevent import NotificationEvent, InvalidNotificationEventException
|
||||||
|
|
||||||
|
from data import model
|
||||||
|
|
||||||
|
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 NotificationWorker(Worker):
|
||||||
|
def process_queue_item(self, job_details):
|
||||||
|
notification_uuid = job_details['notification_uuid'];
|
||||||
|
repo_namespace = job_details['repository_namespace']
|
||||||
|
repo_name = job_details['repository_name']
|
||||||
|
|
||||||
|
notification = model.get_repo_notification(repo_namespace, repo_name, notification_uuid)
|
||||||
|
if not notification:
|
||||||
|
# Probably deleted.
|
||||||
|
return True
|
||||||
|
|
||||||
|
event_name = notification.event.name
|
||||||
|
method_name = notification.method.name
|
||||||
|
|
||||||
|
try:
|
||||||
|
event_handler = NotificationEvent.get_event(event_name)
|
||||||
|
method_handler = NotificationMethod.get_method(method_name)
|
||||||
|
except InvalidNotificationMethodException as ex:
|
||||||
|
logger.exception('Cannot find notification method: %s' % ex.message)
|
||||||
|
return False
|
||||||
|
except InvalidNotificationEventException as ex:
|
||||||
|
logger.exception('Cannot find notification method: %s' % ex.message)
|
||||||
|
return False
|
||||||
|
|
||||||
|
return method_handler.perform(notification, event_handler, job_details)
|
||||||
|
|
||||||
|
logging.config.fileConfig('conf/logging.conf', disable_existing_loggers=False)
|
||||||
|
|
||||||
|
worker = NotificationWorker(notification_queue, poll_period_seconds=15,
|
||||||
|
reservation_seconds=3600)
|
||||||
|
worker.start()
|
Reference in a new issue