Merge master into laffa
This commit is contained in:
commit
f38ce51943
94 changed files with 3132 additions and 871 deletions
|
@ -47,7 +47,7 @@ RUN venv/bin/python -m external_libraries
|
||||||
# Run the tests
|
# Run the tests
|
||||||
RUN TEST=true venv/bin/python -m unittest discover
|
RUN TEST=true venv/bin/python -m unittest discover
|
||||||
|
|
||||||
VOLUME ["/conf/stack", "/var/log", "/datastorage"]
|
VOLUME ["/conf/stack", "/var/log", "/datastorage", "/tmp"]
|
||||||
|
|
||||||
EXPOSE 443 80
|
EXPOSE 443 80
|
||||||
|
|
||||||
|
|
|
@ -7,7 +7,7 @@ from peewee import Proxy
|
||||||
from app import app as application
|
from app import app as application
|
||||||
from flask import request, Request
|
from flask import request, Request
|
||||||
from util.names import urn_generator
|
from util.names import urn_generator
|
||||||
from data.model import db as model_db, read_slave
|
from data.database import db as model_db, read_slave
|
||||||
|
|
||||||
# Turn off debug logging for boto
|
# Turn off debug logging for boto
|
||||||
logging.getLogger('boto').setLevel(logging.CRITICAL)
|
logging.getLogger('boto').setLevel(logging.CRITICAL)
|
||||||
|
|
11
auth/auth.py
11
auth/auth.py
|
@ -25,7 +25,7 @@ def _load_user_from_cookie():
|
||||||
if not current_user.is_anonymous():
|
if not current_user.is_anonymous():
|
||||||
logger.debug('Loading user from cookie: %s', current_user.get_id())
|
logger.debug('Loading user from cookie: %s', current_user.get_id())
|
||||||
set_authenticated_user_deferred(current_user.get_id())
|
set_authenticated_user_deferred(current_user.get_id())
|
||||||
loaded = QuayDeferredPermissionUser(current_user.get_id(), 'username', {scopes.DIRECT_LOGIN})
|
loaded = QuayDeferredPermissionUser(current_user.get_id(), 'user_db_id', {scopes.DIRECT_LOGIN})
|
||||||
identity_changed.send(app, identity=loaded)
|
identity_changed.send(app, identity=loaded)
|
||||||
return current_user.db_user()
|
return current_user.db_user()
|
||||||
return None
|
return None
|
||||||
|
@ -58,12 +58,10 @@ def _validate_and_apply_oauth_token(token):
|
||||||
set_authenticated_user(validated.authorized_user)
|
set_authenticated_user(validated.authorized_user)
|
||||||
set_validated_oauth_token(validated)
|
set_validated_oauth_token(validated)
|
||||||
|
|
||||||
new_identity = QuayDeferredPermissionUser(validated.authorized_user.username, 'username',
|
new_identity = QuayDeferredPermissionUser(validated.authorized_user.id, 'user_db_id', scope_set)
|
||||||
scope_set)
|
|
||||||
identity_changed.send(app, identity=new_identity)
|
identity_changed.send(app, identity=new_identity)
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
def process_basic_auth(auth):
|
def process_basic_auth(auth):
|
||||||
normalized = [part.strip() for part in auth.split(' ') if part]
|
normalized = [part.strip() for part in auth.split(' ') if part]
|
||||||
if normalized[0].lower() != 'basic' or len(normalized) != 2:
|
if normalized[0].lower() != 'basic' or len(normalized) != 2:
|
||||||
|
@ -100,8 +98,7 @@ def process_basic_auth(auth):
|
||||||
logger.debug('Successfully validated robot: %s' % credentials[0])
|
logger.debug('Successfully validated robot: %s' % credentials[0])
|
||||||
set_authenticated_user(robot)
|
set_authenticated_user(robot)
|
||||||
|
|
||||||
deferred_robot = QuayDeferredPermissionUser(robot.username, 'username',
|
deferred_robot = QuayDeferredPermissionUser(robot.id, 'user_db_id', {scopes.DIRECT_LOGIN})
|
||||||
{scopes.DIRECT_LOGIN})
|
|
||||||
identity_changed.send(app, identity=deferred_robot)
|
identity_changed.send(app, identity=deferred_robot)
|
||||||
return
|
return
|
||||||
except model.InvalidRobotException:
|
except model.InvalidRobotException:
|
||||||
|
@ -114,7 +111,7 @@ def process_basic_auth(auth):
|
||||||
logger.debug('Successfully validated user: %s' % authenticated.username)
|
logger.debug('Successfully validated user: %s' % authenticated.username)
|
||||||
set_authenticated_user(authenticated)
|
set_authenticated_user(authenticated)
|
||||||
|
|
||||||
new_identity = QuayDeferredPermissionUser(authenticated.username, 'username',
|
new_identity = QuayDeferredPermissionUser(authenticated.id, 'user_db_id',
|
||||||
{scopes.DIRECT_LOGIN})
|
{scopes.DIRECT_LOGIN})
|
||||||
identity_changed.send(app, identity=new_identity)
|
identity_changed.send(app, identity=new_identity)
|
||||||
return
|
return
|
||||||
|
|
|
@ -10,13 +10,13 @@ logger = logging.getLogger(__name__)
|
||||||
def get_authenticated_user():
|
def get_authenticated_user():
|
||||||
user = getattr(_request_ctx_stack.top, 'authenticated_user', None)
|
user = getattr(_request_ctx_stack.top, 'authenticated_user', None)
|
||||||
if not user:
|
if not user:
|
||||||
username = getattr(_request_ctx_stack.top, 'authenticated_username', None)
|
db_id = getattr(_request_ctx_stack.top, 'authenticated_db_id', None)
|
||||||
if not username:
|
if not db_id:
|
||||||
logger.debug('No authenticated user or deferred username.')
|
logger.debug('No authenticated user or deferred database id.')
|
||||||
return None
|
return None
|
||||||
|
|
||||||
logger.debug('Loading deferred authenticated user.')
|
logger.debug('Loading deferred authenticated user.')
|
||||||
loaded = model.get_user(username)
|
loaded = model.get_user_by_id(db_id)
|
||||||
set_authenticated_user(loaded)
|
set_authenticated_user(loaded)
|
||||||
user = loaded
|
user = loaded
|
||||||
|
|
||||||
|
@ -30,10 +30,10 @@ def set_authenticated_user(user_or_robot):
|
||||||
ctx.authenticated_user = user_or_robot
|
ctx.authenticated_user = user_or_robot
|
||||||
|
|
||||||
|
|
||||||
def set_authenticated_user_deferred(username_or_robotname):
|
def set_authenticated_user_deferred(user_or_robot_db_id):
|
||||||
logger.debug('Deferring loading of authenticated user object: %s', username_or_robotname)
|
logger.debug('Deferring loading of authenticated user object: %s', user_or_robot_db_id)
|
||||||
ctx = _request_ctx_stack.top
|
ctx = _request_ctx_stack.top
|
||||||
ctx.authenticated_username = username_or_robotname
|
ctx.authenticated_db_id = user_or_robot_db_id
|
||||||
|
|
||||||
|
|
||||||
def get_validated_oauth_token():
|
def get_validated_oauth_token():
|
||||||
|
|
|
@ -58,8 +58,8 @@ SCOPE_MAX_USER_ROLES.update({
|
||||||
|
|
||||||
|
|
||||||
class QuayDeferredPermissionUser(Identity):
|
class QuayDeferredPermissionUser(Identity):
|
||||||
def __init__(self, id, auth_type, scopes):
|
def __init__(self, db_id, auth_type, scopes):
|
||||||
super(QuayDeferredPermissionUser, self).__init__(id, auth_type)
|
super(QuayDeferredPermissionUser, self).__init__(db_id, auth_type)
|
||||||
|
|
||||||
self._permissions_loaded = False
|
self._permissions_loaded = False
|
||||||
self._scope_set = scopes
|
self._scope_set = scopes
|
||||||
|
@ -88,7 +88,7 @@ class QuayDeferredPermissionUser(Identity):
|
||||||
def can(self, permission):
|
def can(self, permission):
|
||||||
if not self._permissions_loaded:
|
if not self._permissions_loaded:
|
||||||
logger.debug('Loading user permissions after deferring.')
|
logger.debug('Loading user permissions after deferring.')
|
||||||
user_object = model.get_user(self.id)
|
user_object = model.get_user_by_id(self.id)
|
||||||
|
|
||||||
# Add the superuser need, if applicable.
|
# Add the superuser need, if applicable.
|
||||||
if (user_object.username is not None and
|
if (user_object.username is not None and
|
||||||
|
@ -112,7 +112,7 @@ class QuayDeferredPermissionUser(Identity):
|
||||||
|
|
||||||
# Add repository permissions
|
# Add repository permissions
|
||||||
for perm in model.get_all_user_permissions(user_object):
|
for perm in model.get_all_user_permissions(user_object):
|
||||||
repo_grant = _RepositoryNeed(perm.repository.namespace, perm.repository.name,
|
repo_grant = _RepositoryNeed(perm.repository.namespace_user.username, perm.repository.name,
|
||||||
self._repo_role_for_scopes(perm.role.name))
|
self._repo_role_for_scopes(perm.role.name))
|
||||||
logger.debug('User added permission: {0}'.format(repo_grant))
|
logger.debug('User added permission: {0}'.format(repo_grant))
|
||||||
self.provides.add(repo_grant)
|
self.provides.add(repo_grant)
|
||||||
|
@ -230,16 +230,16 @@ def on_identity_loaded(sender, identity):
|
||||||
if isinstance(identity, QuayDeferredPermissionUser):
|
if isinstance(identity, QuayDeferredPermissionUser):
|
||||||
logger.debug('Deferring permissions for user: %s', identity.id)
|
logger.debug('Deferring permissions for user: %s', identity.id)
|
||||||
|
|
||||||
elif identity.auth_type == 'username':
|
elif identity.auth_type == 'user_db_id':
|
||||||
logger.debug('Switching username permission to deferred object: %s', identity.id)
|
logger.debug('Switching username permission to deferred object: %s', identity.id)
|
||||||
switch_to_deferred = QuayDeferredPermissionUser(identity.id, 'username', {scopes.DIRECT_LOGIN})
|
switch_to_deferred = QuayDeferredPermissionUser(identity.id, 'user_db_id', {scopes.DIRECT_LOGIN})
|
||||||
identity_changed.send(app, identity=switch_to_deferred)
|
identity_changed.send(app, identity=switch_to_deferred)
|
||||||
|
|
||||||
elif identity.auth_type == 'token':
|
elif identity.auth_type == 'token':
|
||||||
logger.debug('Loading permissions for token: %s', identity.id)
|
logger.debug('Loading permissions for token: %s', identity.id)
|
||||||
token_data = model.load_token_data(identity.id)
|
token_data = model.load_token_data(identity.id)
|
||||||
|
|
||||||
repo_grant = _RepositoryNeed(token_data.repository.namespace,
|
repo_grant = _RepositoryNeed(token_data.repository.namespace_user.username,
|
||||||
token_data.repository.name,
|
token_data.repository.name,
|
||||||
token_data.role.name)
|
token_data.role.name)
|
||||||
logger.debug('Delegate token added permission: {0}'.format(repo_grant))
|
logger.debug('Delegate token added permission: {0}'.format(repo_grant))
|
||||||
|
|
|
@ -13,10 +13,5 @@ http {
|
||||||
include server-base.conf;
|
include server-base.conf;
|
||||||
|
|
||||||
listen 80 default;
|
listen 80 default;
|
||||||
|
|
||||||
location /static/ {
|
|
||||||
# checks for static file, if not found proxy to app
|
|
||||||
alias /static/;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -23,10 +23,5 @@ http {
|
||||||
ssl_protocols SSLv3 TLSv1;
|
ssl_protocols SSLv3 TLSv1;
|
||||||
ssl_ciphers ALL:!ADH:!EXPORT56:RC4+RSA:+HIGH:+MEDIUM:+LOW:+SSLv3:+EXP;
|
ssl_ciphers ALL:!ADH:!EXPORT56:RC4+RSA:+HIGH:+MEDIUM:+LOW:+SSLv3:+EXP;
|
||||||
ssl_prefer_server_ciphers on;
|
ssl_prefer_server_ciphers on;
|
||||||
|
|
||||||
location /static/ {
|
|
||||||
# checks for static file, if not found proxy to app
|
|
||||||
alias /static/;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -24,4 +24,16 @@ location / {
|
||||||
proxy_pass http://app_server;
|
proxy_pass http://app_server;
|
||||||
proxy_read_timeout 2000;
|
proxy_read_timeout 2000;
|
||||||
proxy_temp_path /var/log/nginx/proxy_temp 1 2;
|
proxy_temp_path /var/log/nginx/proxy_temp 1 2;
|
||||||
|
}
|
||||||
|
|
||||||
|
location /static/ {
|
||||||
|
# checks for static file, if not found proxy to app
|
||||||
|
alias /static/;
|
||||||
|
}
|
||||||
|
|
||||||
|
location /v1/_ping {
|
||||||
|
add_header Content-Type text/plain;
|
||||||
|
add_header X-Docker-Registry-Version 0.6.0;
|
||||||
|
add_header X-Docker-Registry-Standalone 0;
|
||||||
|
return 200 'okay';
|
||||||
}
|
}
|
10
config.py
10
config.py
|
@ -80,11 +80,11 @@ class DefaultConfig(object):
|
||||||
AUTHENTICATION_TYPE = 'Database'
|
AUTHENTICATION_TYPE = 'Database'
|
||||||
|
|
||||||
# Build logs
|
# Build logs
|
||||||
BUILDLOGS_REDIS_HOSTNAME = 'logs.quay.io'
|
BUILDLOGS_REDIS = {'host': 'logs.quay.io'}
|
||||||
BUILDLOGS_OPTIONS = []
|
BUILDLOGS_OPTIONS = []
|
||||||
|
|
||||||
# Real-time user events
|
# Real-time user events
|
||||||
USER_EVENTS_REDIS_HOSTNAME = 'logs.quay.io'
|
USER_EVENTS_REDIS = {'host': 'logs.quay.io'}
|
||||||
|
|
||||||
# Stripe config
|
# Stripe config
|
||||||
BILLING_TYPE = 'FakeStripe'
|
BILLING_TYPE = 'FakeStripe'
|
||||||
|
@ -162,6 +162,12 @@ class DefaultConfig(object):
|
||||||
# Feature Flag: Dockerfile build support.
|
# Feature Flag: Dockerfile build support.
|
||||||
FEATURE_BUILD_SUPPORT = True
|
FEATURE_BUILD_SUPPORT = True
|
||||||
|
|
||||||
|
# Feature Flag: Whether emails are enabled.
|
||||||
|
FEATURE_MAILING = True
|
||||||
|
|
||||||
|
# Feature Flag: Whether users can be created (by non-super users).
|
||||||
|
FEATURE_USER_CREATION = True
|
||||||
|
|
||||||
DISTRIBUTED_STORAGE_CONFIG = {
|
DISTRIBUTED_STORAGE_CONFIG = {
|
||||||
'local_eu': ['LocalStorage', {'storage_path': 'test/data/registry/eu'}],
|
'local_eu': ['LocalStorage', {'storage_path': 'test/data/registry/eu'}],
|
||||||
'local_us': ['LocalStorage', {'storage_path': 'test/data/registry/us'}],
|
'local_us': ['LocalStorage', {'storage_path': 'test/data/registry/us'}],
|
||||||
|
|
|
@ -16,8 +16,8 @@ class RedisBuildLogs(object):
|
||||||
COMMAND = 'command'
|
COMMAND = 'command'
|
||||||
PHASE = 'phase'
|
PHASE = 'phase'
|
||||||
|
|
||||||
def __init__(self, redis_host):
|
def __init__(self, redis_config):
|
||||||
self._redis = redis.StrictRedis(host=redis_host)
|
self._redis = redis.StrictRedis(socket_connect_timeout=5, **redis_config)
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def _logs_key(build_id):
|
def _logs_key(build_id):
|
||||||
|
@ -104,7 +104,13 @@ class BuildLogs(object):
|
||||||
self.state = None
|
self.state = None
|
||||||
|
|
||||||
def init_app(self, app):
|
def init_app(self, app):
|
||||||
buildlogs_hostname = app.config.get('BUILDLOGS_REDIS_HOSTNAME')
|
buildlogs_config = app.config.get('BUILDLOGS_REDIS')
|
||||||
|
if not buildlogs_config:
|
||||||
|
# This is the old key name.
|
||||||
|
buildlogs_config = {
|
||||||
|
'host': app.config.get('BUILDLOGS_REDIS_HOSTNAME')
|
||||||
|
}
|
||||||
|
|
||||||
buildlogs_options = app.config.get('BUILDLOGS_OPTIONS', [])
|
buildlogs_options = app.config.get('BUILDLOGS_OPTIONS', [])
|
||||||
buildlogs_import = app.config.get('BUILDLOGS_MODULE_AND_CLASS', None)
|
buildlogs_import = app.config.get('BUILDLOGS_MODULE_AND_CLASS', None)
|
||||||
|
|
||||||
|
@ -113,7 +119,7 @@ class BuildLogs(object):
|
||||||
else:
|
else:
|
||||||
klass = import_class(buildlogs_import[0], buildlogs_import[1])
|
klass = import_class(buildlogs_import[0], buildlogs_import[1])
|
||||||
|
|
||||||
buildlogs = klass(buildlogs_hostname, *buildlogs_options)
|
buildlogs = klass(buildlogs_config, *buildlogs_options)
|
||||||
|
|
||||||
# register extension with app
|
# register extension with app
|
||||||
app.extensions = getattr(app, 'extensions', {})
|
app.extensions = getattr(app, 'extensions', {})
|
||||||
|
|
|
@ -8,7 +8,7 @@ from peewee import *
|
||||||
from data.read_slave import ReadSlaveModel
|
from data.read_slave import ReadSlaveModel
|
||||||
from sqlalchemy.engine.url import make_url
|
from sqlalchemy.engine.url import make_url
|
||||||
from urlparse import urlparse
|
from urlparse import urlparse
|
||||||
|
from util.names import urn_generator
|
||||||
|
|
||||||
logger = logging.getLogger(__name__)
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
@ -21,8 +21,24 @@ SCHEME_DRIVERS = {
|
||||||
'postgresql+psycopg2': PostgresqlDatabase,
|
'postgresql+psycopg2': PostgresqlDatabase,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
SCHEME_RANDOM_FUNCTION = {
|
||||||
|
'mysql': fn.Rand,
|
||||||
|
'mysql+pymysql': fn.Rand,
|
||||||
|
'sqlite': fn.Random,
|
||||||
|
'postgresql': fn.Random,
|
||||||
|
'postgresql+psycopg2': fn.Random,
|
||||||
|
}
|
||||||
|
|
||||||
|
class CallableProxy(Proxy):
|
||||||
|
def __call__(self, *args, **kwargs):
|
||||||
|
if self.obj is None:
|
||||||
|
raise AttributeError('Cannot use uninitialized Proxy.')
|
||||||
|
return self.obj(*args, **kwargs)
|
||||||
|
|
||||||
db = Proxy()
|
db = Proxy()
|
||||||
read_slave = Proxy()
|
read_slave = Proxy()
|
||||||
|
db_random_func = CallableProxy()
|
||||||
|
|
||||||
|
|
||||||
def _db_from_url(url, db_kwargs):
|
def _db_from_url(url, db_kwargs):
|
||||||
parsed_url = make_url(url)
|
parsed_url = make_url(url)
|
||||||
|
@ -38,11 +54,15 @@ def _db_from_url(url, db_kwargs):
|
||||||
|
|
||||||
return SCHEME_DRIVERS[parsed_url.drivername](parsed_url.database, **db_kwargs)
|
return SCHEME_DRIVERS[parsed_url.drivername](parsed_url.database, **db_kwargs)
|
||||||
|
|
||||||
|
|
||||||
def configure(config_object):
|
def configure(config_object):
|
||||||
db_kwargs = dict(config_object['DB_CONNECTION_ARGS'])
|
db_kwargs = dict(config_object['DB_CONNECTION_ARGS'])
|
||||||
write_db_uri = config_object['DB_URI']
|
write_db_uri = config_object['DB_URI']
|
||||||
db.initialize(_db_from_url(write_db_uri, db_kwargs))
|
db.initialize(_db_from_url(write_db_uri, db_kwargs))
|
||||||
|
|
||||||
|
parsed_write_uri = make_url(write_db_uri)
|
||||||
|
db_random_func.initialize(SCHEME_RANDOM_FUNCTION[parsed_write_uri.drivername])
|
||||||
|
|
||||||
read_slave_uri = config_object.get('DB_READ_SLAVE_URI', None)
|
read_slave_uri = config_object.get('DB_READ_SLAVE_URI', None)
|
||||||
if read_slave_uri is not None:
|
if read_slave_uri is not None:
|
||||||
read_slave.initialize(_db_from_url(read_slave_uri, db_kwargs))
|
read_slave.initialize(_db_from_url(read_slave_uri, db_kwargs))
|
||||||
|
@ -112,6 +132,15 @@ class TeamMember(BaseModel):
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
class TeamMemberInvite(BaseModel):
|
||||||
|
# Note: Either user OR email will be filled in, but not both.
|
||||||
|
user = ForeignKeyField(User, index=True, null=True)
|
||||||
|
email = CharField(null=True)
|
||||||
|
team = ForeignKeyField(Team, index=True)
|
||||||
|
inviter = ForeignKeyField(User, related_name='inviter')
|
||||||
|
invite_token = CharField(default=urn_generator(['teaminvite']))
|
||||||
|
|
||||||
|
|
||||||
class LoginService(BaseModel):
|
class LoginService(BaseModel):
|
||||||
name = CharField(unique=True, index=True)
|
name = CharField(unique=True, index=True)
|
||||||
|
|
||||||
|
@ -139,7 +168,7 @@ class Visibility(BaseModel):
|
||||||
|
|
||||||
|
|
||||||
class Repository(BaseModel):
|
class Repository(BaseModel):
|
||||||
namespace = CharField()
|
namespace_user = ForeignKeyField(User)
|
||||||
name = CharField()
|
name = CharField()
|
||||||
visibility = ForeignKeyField(Visibility)
|
visibility = ForeignKeyField(Visibility)
|
||||||
description = TextField(null=True)
|
description = TextField(null=True)
|
||||||
|
@ -150,7 +179,7 @@ class Repository(BaseModel):
|
||||||
read_slaves = (read_slave,)
|
read_slaves = (read_slave,)
|
||||||
indexes = (
|
indexes = (
|
||||||
# create a unique index on namespace and name
|
# create a unique index on namespace and name
|
||||||
(('namespace', 'name'), True),
|
(('namespace_user', 'name'), True),
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
@ -227,7 +256,7 @@ class EmailConfirmation(BaseModel):
|
||||||
|
|
||||||
|
|
||||||
class ImageStorage(BaseModel):
|
class ImageStorage(BaseModel):
|
||||||
uuid = CharField(default=uuid_generator)
|
uuid = CharField(default=uuid_generator, index=True)
|
||||||
checksum = CharField(null=True)
|
checksum = CharField(null=True)
|
||||||
created = DateTimeField(null=True)
|
created = DateTimeField(null=True)
|
||||||
comment = TextField(null=True)
|
comment = TextField(null=True)
|
||||||
|
@ -333,7 +362,7 @@ class RepositoryBuild(BaseModel):
|
||||||
class QueueItem(BaseModel):
|
class QueueItem(BaseModel):
|
||||||
queue_name = CharField(index=True, max_length=1024)
|
queue_name = CharField(index=True, max_length=1024)
|
||||||
body = TextField()
|
body = TextField()
|
||||||
available_after = DateTimeField(default=datetime.now, index=True)
|
available_after = DateTimeField(default=datetime.utcnow, index=True)
|
||||||
available = BooleanField(default=True, index=True)
|
available = BooleanField(default=True, index=True)
|
||||||
processing_expires = DateTimeField(null=True, index=True)
|
processing_expires = DateTimeField(null=True, index=True)
|
||||||
retries_remaining = IntegerField(default=5)
|
retries_remaining = IntegerField(default=5)
|
||||||
|
@ -438,4 +467,5 @@ all_models = [User, Repository, Image, AccessToken, Role, RepositoryPermission,
|
||||||
OAuthApplication, OAuthAuthorizationCode, OAuthAccessToken, NotificationKind,
|
OAuthApplication, OAuthAuthorizationCode, OAuthAccessToken, NotificationKind,
|
||||||
Notification, ImageStorageLocation, ImageStoragePlacement,
|
Notification, ImageStorageLocation, ImageStoragePlacement,
|
||||||
ExternalNotificationEvent, ExternalNotificationMethod, RepositoryNotification,
|
ExternalNotificationEvent, ExternalNotificationMethod, RepositoryNotification,
|
||||||
RepositoryAuthorizedEmail, ImageStorageTransformation, DerivedImageStorage]
|
RepositoryAuthorizedEmail, ImageStorageTransformation, DerivedImageStorage,
|
||||||
|
TeamMemberInvite]
|
||||||
|
|
|
@ -0,0 +1,24 @@
|
||||||
|
"""Migrate registry namespaces to reference a user.
|
||||||
|
|
||||||
|
Revision ID: 13da56878560
|
||||||
|
Revises: 51d04d0e7e6f
|
||||||
|
Create Date: 2014-09-18 13:56:45.130455
|
||||||
|
|
||||||
|
"""
|
||||||
|
|
||||||
|
# revision identifiers, used by Alembic.
|
||||||
|
revision = '13da56878560'
|
||||||
|
down_revision = '51d04d0e7e6f'
|
||||||
|
|
||||||
|
from alembic import op
|
||||||
|
import sqlalchemy as sa
|
||||||
|
|
||||||
|
from data.database import Repository, User
|
||||||
|
|
||||||
|
def upgrade(tables):
|
||||||
|
# Add the namespace_user column, allowing it to be nullable
|
||||||
|
op.add_column('repository', sa.Column('namespace_user_id', sa.Integer(), sa.ForeignKey('user.id')))
|
||||||
|
|
||||||
|
|
||||||
|
def downgrade(tables):
|
||||||
|
op.drop_column('repository', 'namespace_user_id')
|
|
@ -44,11 +44,11 @@ def downgrade(tables):
|
||||||
op.create_index('notificationkind_name', 'notificationkind', ['name'], unique=False)
|
op.create_index('notificationkind_name', 'notificationkind', ['name'], unique=False)
|
||||||
op.drop_index('logentrykind_name', table_name='logentrykind')
|
op.drop_index('logentrykind_name', table_name='logentrykind')
|
||||||
op.create_index('logentrykind_name', 'logentrykind', ['name'], unique=False)
|
op.create_index('logentrykind_name', 'logentrykind', ['name'], unique=False)
|
||||||
op.add_column('image', sa.Column('created', mysql.DATETIME(), nullable=True))
|
op.add_column('image', sa.Column('created', sa.DateTime(), nullable=True))
|
||||||
op.add_column('image', sa.Column('command', mysql.LONGTEXT(), nullable=True))
|
op.add_column('image', sa.Column('command', sa.Text(), nullable=True))
|
||||||
op.add_column('image', sa.Column('image_size', mysql.BIGINT(display_width=20), nullable=True))
|
op.add_column('image', sa.Column('image_size', sa.BigInteger(), nullable=True))
|
||||||
op.add_column('image', sa.Column('checksum', mysql.VARCHAR(length=255), nullable=True))
|
op.add_column('image', sa.Column('checksum', sa.String(length=255), nullable=True))
|
||||||
op.add_column('image', sa.Column('comment', mysql.LONGTEXT(), nullable=True))
|
op.add_column('image', sa.Column('comment', sa.Text(), nullable=True))
|
||||||
op.drop_index('buildtriggerservice_name', table_name='buildtriggerservice')
|
op.drop_index('buildtriggerservice_name', table_name='buildtriggerservice')
|
||||||
op.create_index('buildtriggerservice_name', 'buildtriggerservice', ['name'], unique=False)
|
op.create_index('buildtriggerservice_name', 'buildtriggerservice', ['name'], unique=False)
|
||||||
### end Alembic commands ###
|
### end Alembic commands ###
|
||||||
|
|
|
@ -0,0 +1,26 @@
|
||||||
|
"""Backfill the namespace_user fields.
|
||||||
|
|
||||||
|
Revision ID: 3f4fe1194671
|
||||||
|
Revises: 6f2ecf5afcf
|
||||||
|
Create Date: 2014-09-24 14:29:45.192179
|
||||||
|
|
||||||
|
"""
|
||||||
|
|
||||||
|
# revision identifiers, used by Alembic.
|
||||||
|
revision = '3f4fe1194671'
|
||||||
|
down_revision = '6f2ecf5afcf'
|
||||||
|
|
||||||
|
from alembic import op
|
||||||
|
import sqlalchemy as sa
|
||||||
|
|
||||||
|
|
||||||
|
def upgrade(tables):
|
||||||
|
conn = op.get_bind()
|
||||||
|
user_table_name_escaped = conn.dialect.identifier_preparer.format_table(tables['user'])
|
||||||
|
conn.execute('update repository set namespace_user_id = (select id from {0} where {0}.username = repository.namespace) where namespace_user_id is NULL'.format(user_table_name_escaped))
|
||||||
|
op.create_index('repository_namespace_user_id_name', 'repository', ['namespace_user_id', 'name'], unique=True)
|
||||||
|
|
||||||
|
|
||||||
|
def downgrade(tables):
|
||||||
|
op.drop_constraint('fk_repository_namespace_user_id_user', table_name='repository', type_='foreignkey')
|
||||||
|
op.drop_index('repository_namespace_user_id_name', table_name='repository')
|
|
@ -0,0 +1,78 @@
|
||||||
|
"""Email invites for joining a team.
|
||||||
|
|
||||||
|
Revision ID: 51d04d0e7e6f
|
||||||
|
Revises: 34fd69f63809
|
||||||
|
Create Date: 2014-09-15 23:51:35.478232
|
||||||
|
|
||||||
|
"""
|
||||||
|
|
||||||
|
# revision identifiers, used by Alembic.
|
||||||
|
revision = '51d04d0e7e6f'
|
||||||
|
down_revision = '34fd69f63809'
|
||||||
|
|
||||||
|
from alembic import op
|
||||||
|
import sqlalchemy as sa
|
||||||
|
|
||||||
|
|
||||||
|
def upgrade(tables):
|
||||||
|
### commands auto generated by Alembic - please adjust! ###
|
||||||
|
op.create_table('teammemberinvite',
|
||||||
|
sa.Column('id', sa.Integer(), nullable=False),
|
||||||
|
sa.Column('user_id', sa.Integer(), nullable=True),
|
||||||
|
sa.Column('email', sa.String(length=255), nullable=True),
|
||||||
|
sa.Column('team_id', sa.Integer(), nullable=False),
|
||||||
|
sa.Column('inviter_id', sa.Integer(), nullable=False),
|
||||||
|
sa.Column('invite_token', sa.String(length=255), nullable=False),
|
||||||
|
sa.ForeignKeyConstraint(['inviter_id'], ['user.id'], ),
|
||||||
|
sa.ForeignKeyConstraint(['team_id'], ['team.id'], ),
|
||||||
|
sa.ForeignKeyConstraint(['user_id'], ['user.id'], ),
|
||||||
|
sa.PrimaryKeyConstraint('id')
|
||||||
|
)
|
||||||
|
op.create_index('teammemberinvite_inviter_id', 'teammemberinvite', ['inviter_id'], unique=False)
|
||||||
|
op.create_index('teammemberinvite_team_id', 'teammemberinvite', ['team_id'], unique=False)
|
||||||
|
op.create_index('teammemberinvite_user_id', 'teammemberinvite', ['user_id'], unique=False)
|
||||||
|
### end Alembic commands ###
|
||||||
|
|
||||||
|
# Manually add the new logentrykind types
|
||||||
|
op.bulk_insert(tables.logentrykind,
|
||||||
|
[
|
||||||
|
{'id':42, 'name':'org_invite_team_member'},
|
||||||
|
{'id':43, 'name':'org_team_member_invite_accepted'},
|
||||||
|
{'id':44, 'name':'org_team_member_invite_declined'},
|
||||||
|
{'id':45, 'name':'org_delete_team_member_invite'},
|
||||||
|
])
|
||||||
|
|
||||||
|
op.bulk_insert(tables.notificationkind,
|
||||||
|
[
|
||||||
|
{'id':10, 'name':'org_team_invite'},
|
||||||
|
])
|
||||||
|
|
||||||
|
def downgrade(tables):
|
||||||
|
### commands auto generated by Alembic - please adjust! ###
|
||||||
|
op.execute(
|
||||||
|
(tables.logentrykind.delete()
|
||||||
|
.where(tables.logentrykind.c.name == op.inline_literal('org_invite_team_member')))
|
||||||
|
)
|
||||||
|
|
||||||
|
op.execute(
|
||||||
|
(tables.logentrykind.delete()
|
||||||
|
.where(tables.logentrykind.c.name == op.inline_literal('org_team_member_invite_accepted')))
|
||||||
|
)
|
||||||
|
|
||||||
|
op.execute(
|
||||||
|
(tables.logentrykind.delete()
|
||||||
|
.where(tables.logentrykind.c.name == op.inline_literal('org_team_member_invite_declined')))
|
||||||
|
)
|
||||||
|
|
||||||
|
op.execute(
|
||||||
|
(tables.logentrykind.delete()
|
||||||
|
.where(tables.logentrykind.c.name == op.inline_literal('org_delete_team_member_invite')))
|
||||||
|
)
|
||||||
|
|
||||||
|
op.execute(
|
||||||
|
(tables.notificationkind.delete()
|
||||||
|
.where(tables.notificationkind.c.name == op.inline_literal('org_team_invite')))
|
||||||
|
)
|
||||||
|
|
||||||
|
op.drop_table('teammemberinvite')
|
||||||
|
### end Alembic commands ###
|
|
@ -1,17 +1,16 @@
|
||||||
"""add the uncompressed size to image storage
|
"""add the uncompressed size to image storage
|
||||||
|
|
||||||
Revision ID: 6f2ecf5afcf
|
Revision ID: 6f2ecf5afcf
|
||||||
Revises: 3f6d26399bd2
|
Revises: 13da56878560
|
||||||
Create Date: 2014-09-22 14:39:13.470566
|
Create Date: 2014-09-22 14:39:13.470566
|
||||||
|
|
||||||
"""
|
"""
|
||||||
|
|
||||||
# revision identifiers, used by Alembic.
|
# revision identifiers, used by Alembic.
|
||||||
revision = '6f2ecf5afcf'
|
revision = '6f2ecf5afcf'
|
||||||
down_revision = '3f6d26399bd2'
|
down_revision = '13da56878560'
|
||||||
|
|
||||||
from alembic import op
|
from alembic import op
|
||||||
from tools.uncompressedsize import backfill_sizes
|
|
||||||
import sqlalchemy as sa
|
import sqlalchemy as sa
|
||||||
|
|
||||||
|
|
||||||
|
@ -20,9 +19,6 @@ def upgrade(tables):
|
||||||
op.add_column('imagestorage', sa.Column('uncompressed_size', sa.BigInteger(), nullable=True))
|
op.add_column('imagestorage', sa.Column('uncompressed_size', sa.BigInteger(), nullable=True))
|
||||||
### end Alembic commands ###
|
### end Alembic commands ###
|
||||||
|
|
||||||
# Backfill the uncompressed size to the image storage table.
|
|
||||||
backfill_sizes()
|
|
||||||
|
|
||||||
def downgrade(tables):
|
def downgrade(tables):
|
||||||
### commands auto generated by Alembic - please adjust! ###
|
### commands auto generated by Alembic - please adjust! ###
|
||||||
op.drop_column('imagestorage', 'uncompressed_size')
|
op.drop_column('imagestorage', 'uncompressed_size')
|
||||||
|
|
|
@ -0,0 +1,29 @@
|
||||||
|
"""Allow the namespace column to be nullable.
|
||||||
|
|
||||||
|
Revision ID: 9a1087b007d
|
||||||
|
Revises: 3f4fe1194671
|
||||||
|
Create Date: 2014-10-01 16:11:21.277226
|
||||||
|
|
||||||
|
"""
|
||||||
|
|
||||||
|
# revision identifiers, used by Alembic.
|
||||||
|
revision = '9a1087b007d'
|
||||||
|
down_revision = '3f4fe1194671'
|
||||||
|
|
||||||
|
from alembic import op
|
||||||
|
import sqlalchemy as sa
|
||||||
|
|
||||||
|
|
||||||
|
def upgrade(tables):
|
||||||
|
op.drop_index('repository_namespace_name', table_name='repository')
|
||||||
|
op.alter_column('repository', 'namespace', nullable=True, existing_type=sa.String(length=255),
|
||||||
|
server_default=sa.text('NULL'))
|
||||||
|
|
||||||
|
|
||||||
|
def downgrade(tables):
|
||||||
|
conn = op.get_bind()
|
||||||
|
user_table_name_escaped = conn.dialect.identifier_preparer.format_table(tables['user'])
|
||||||
|
conn.execute('update repository set namespace = (select username from {0} where {0}.id = repository.namespace_user_id) where namespace is NULL'.format(user_table_name_escaped))
|
||||||
|
|
||||||
|
op.create_index('repository_namespace_name', 'repository', ['namespace', 'name'], unique=True)
|
||||||
|
op.alter_column('repository', 'namespace', nullable=False, existing_type=sa.String(length=255))
|
|
@ -0,0 +1,22 @@
|
||||||
|
"""Add an index to the uuid in the image storage table.
|
||||||
|
|
||||||
|
Revision ID: b1d41e2071b
|
||||||
|
Revises: 9a1087b007d
|
||||||
|
Create Date: 2014-10-06 18:42:10.021235
|
||||||
|
|
||||||
|
"""
|
||||||
|
|
||||||
|
# revision identifiers, used by Alembic.
|
||||||
|
revision = 'b1d41e2071b'
|
||||||
|
down_revision = '9a1087b007d'
|
||||||
|
|
||||||
|
from alembic import op
|
||||||
|
import sqlalchemy as sa
|
||||||
|
|
||||||
|
|
||||||
|
def upgrade(tables):
|
||||||
|
op.create_index('imagestorage_uuid', 'imagestorage', ['uuid'], unique=True)
|
||||||
|
|
||||||
|
|
||||||
|
def downgrade(tables):
|
||||||
|
op.drop_index('imagestorage_uuid', table_name='imagestorage')
|
|
@ -23,13 +23,11 @@ def upgrade(tables):
|
||||||
def downgrade(tables):
|
def downgrade(tables):
|
||||||
### commands auto generated by Alembic - please adjust! ###
|
### commands auto generated by Alembic - please adjust! ###
|
||||||
op.create_table('webhook',
|
op.create_table('webhook',
|
||||||
sa.Column('id', mysql.INTEGER(display_width=11), nullable=False),
|
sa.Column('id', sa.Integer(), nullable=False),
|
||||||
sa.Column('public_id', mysql.VARCHAR(length=255), nullable=False),
|
sa.Column('public_id', sa.String(length=255), nullable=False),
|
||||||
sa.Column('repository_id', mysql.INTEGER(display_width=11), autoincrement=False, nullable=False),
|
sa.Column('repository_id', sa.Integer(), nullable=False),
|
||||||
sa.Column('parameters', mysql.LONGTEXT(), nullable=False),
|
sa.Column('parameters', sa.Text(), nullable=False),
|
||||||
sa.ForeignKeyConstraint(['repository_id'], [u'repository.id'], name=u'fk_webhook_repository_repository_id'),
|
sa.ForeignKeyConstraint(['repository_id'], ['repository.id'], ),
|
||||||
sa.PrimaryKeyConstraint('id'),
|
sa.PrimaryKeyConstraint('id')
|
||||||
mysql_default_charset=u'latin1',
|
|
||||||
mysql_engine=u'InnoDB'
|
|
||||||
)
|
)
|
||||||
### end Alembic commands ###
|
### end Alembic commands ###
|
||||||
|
|
File diff suppressed because it is too large
Load diff
|
@ -17,7 +17,12 @@ OPTION_TRANSLATIONS = {
|
||||||
|
|
||||||
|
|
||||||
def gen_sqlalchemy_metadata(peewee_model_list):
|
def gen_sqlalchemy_metadata(peewee_model_list):
|
||||||
metadata = MetaData()
|
metadata = MetaData(naming_convention={
|
||||||
|
"ix": 'ix_%(column_0_label)s',
|
||||||
|
"uq": "uq_%(table_name)s_%(column_0_name)s",
|
||||||
|
"fk": "fk_%(table_name)s_%(column_0_name)s_%(referred_table_name)s",
|
||||||
|
"pk": "pk_%(table_name)s"
|
||||||
|
})
|
||||||
|
|
||||||
for model in peewee_model_list:
|
for model in peewee_model_list:
|
||||||
meta = model._meta
|
meta = model._meta
|
||||||
|
|
|
@ -68,9 +68,8 @@ class WorkQueue(object):
|
||||||
'retries_remaining': retries_remaining,
|
'retries_remaining': retries_remaining,
|
||||||
}
|
}
|
||||||
|
|
||||||
if available_after:
|
available_date = datetime.utcnow() + timedelta(seconds=available_after or 0)
|
||||||
available_date = datetime.utcnow() + timedelta(seconds=available_after)
|
params['available_after'] = available_date
|
||||||
params['available_after'] = available_date
|
|
||||||
|
|
||||||
with self._transaction_factory(db):
|
with self._transaction_factory(db):
|
||||||
QueueItem.create(**params)
|
QueueItem.create(**params)
|
||||||
|
|
|
@ -7,14 +7,14 @@ class UserEventBuilder(object):
|
||||||
Defines a helper class for constructing UserEvent and UserEventListener
|
Defines a helper class for constructing UserEvent and UserEventListener
|
||||||
instances.
|
instances.
|
||||||
"""
|
"""
|
||||||
def __init__(self, redis_host):
|
def __init__(self, redis_config):
|
||||||
self._redis_host = redis_host
|
self._redis_config = redis_config
|
||||||
|
|
||||||
def get_event(self, username):
|
def get_event(self, username):
|
||||||
return UserEvent(self._redis_host, username)
|
return UserEvent(self._redis_config, username)
|
||||||
|
|
||||||
def get_listener(self, username, events):
|
def get_listener(self, username, events):
|
||||||
return UserEventListener(self._redis_host, username, events)
|
return UserEventListener(self._redis_config, username, events)
|
||||||
|
|
||||||
|
|
||||||
class UserEventsBuilderModule(object):
|
class UserEventsBuilderModule(object):
|
||||||
|
@ -26,8 +26,14 @@ class UserEventsBuilderModule(object):
|
||||||
self.state = None
|
self.state = None
|
||||||
|
|
||||||
def init_app(self, app):
|
def init_app(self, app):
|
||||||
redis_hostname = app.config.get('USER_EVENTS_REDIS_HOSTNAME')
|
redis_config = app.config.get('USER_EVENTS_REDIS')
|
||||||
user_events = UserEventBuilder(redis_hostname)
|
if not redis_config:
|
||||||
|
# This is the old key name.
|
||||||
|
redis_config = {
|
||||||
|
'host': app.config.get('USER_EVENTS_REDIS_HOSTNAME')
|
||||||
|
}
|
||||||
|
|
||||||
|
user_events = UserEventBuilder(redis_config)
|
||||||
|
|
||||||
# register extension with app
|
# register extension with app
|
||||||
app.extensions = getattr(app, 'extensions', {})
|
app.extensions = getattr(app, 'extensions', {})
|
||||||
|
@ -43,8 +49,8 @@ class UserEvent(object):
|
||||||
Defines a helper class for publishing to realtime user events
|
Defines a helper class for publishing to realtime user events
|
||||||
as backed by Redis.
|
as backed by Redis.
|
||||||
"""
|
"""
|
||||||
def __init__(self, redis_host, username):
|
def __init__(self, redis_config, username):
|
||||||
self._redis = redis.StrictRedis(host=redis_host)
|
self._redis = redis.StrictRedis(socket_connect_timeout=5, **redis_config)
|
||||||
self._username = username
|
self._username = username
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
|
@ -74,10 +80,10 @@ class UserEventListener(object):
|
||||||
Defines a helper class for subscribing to realtime user events as
|
Defines a helper class for subscribing to realtime user events as
|
||||||
backed by Redis.
|
backed by Redis.
|
||||||
"""
|
"""
|
||||||
def __init__(self, redis_host, username, events=set([])):
|
def __init__(self, redis_config, username, events=set([])):
|
||||||
channels = [self._user_event_key(username, e) for e in events]
|
channels = [self._user_event_key(username, e) for e in events]
|
||||||
|
|
||||||
self._redis = redis.StrictRedis(host=redis_host)
|
self._redis = redis.StrictRedis(socket_connect_timeout=5, **redis_config)
|
||||||
self._pubsub = self._redis.pubsub()
|
self._pubsub = self._redis.pubsub()
|
||||||
self._pubsub.subscribe(channels)
|
self._pubsub.subscribe(channels)
|
||||||
|
|
||||||
|
|
45
emails/base.html
Normal file
45
emails/base.html
Normal file
|
@ -0,0 +1,45 @@
|
||||||
|
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
|
||||||
|
<html xmlns="http://www.w3.org/1999/xhtml" xmlns="http://www.w3.org/1999/xhtml" style="font-family: 'Helvetica Neue', 'Helvetica', Helvetica, Arial, sans-serif; margin: 0; padding: 0;">
|
||||||
|
<head>
|
||||||
|
<meta name="viewport" content="width=device-width" />
|
||||||
|
<meta http-equiv="Content-Type" content="text/html; charset=UTF-8" />
|
||||||
|
<title>{{ subject }}</title>
|
||||||
|
</head>
|
||||||
|
<body bgcolor="#FFFFFF" style="font-family: 'Helvetica Neue', 'Helvetica', Helvetica, Arial, sans-serif; -webkit-font-smoothing: antialiased; -webkit-text-size-adjust: none; width: 100% !important; height: 100%; margin: 0; padding: 0;"><style type="text/css">
|
||||||
|
@media only screen and (max-width: 600px) {
|
||||||
|
a[class="btn"] {
|
||||||
|
display: block !important; margin-bottom: 10px !important; background-image: none !important; margin-right: 0 !important;
|
||||||
|
}
|
||||||
|
div[class="column"] {
|
||||||
|
width: auto !important; float: none !important;
|
||||||
|
}
|
||||||
|
table.social div[class="column"] {
|
||||||
|
width: auto !important;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
|
||||||
|
<!-- HEADER -->
|
||||||
|
<table class="head-wrap" bgcolor="#FFFFFF" style="font-family: 'Helvetica Neue', 'Helvetica', Helvetica, Arial, sans-serif; width: 100%; margin: 0; padding: 0;"><tr style="font-family: 'Helvetica Neue', 'Helvetica', Helvetica, Arial, sans-serif; margin: 0; padding: 0;"><td style="font-family: 'Helvetica Neue', 'Helvetica', Helvetica, Arial, sans-serif; margin: 0; padding: 0;"></td>
|
||||||
|
<td class="header container" style="font-family: 'Helvetica Neue', 'Helvetica', Helvetica, Arial, sans-serif; display: block !important; max-width: 100% !important; clear: both !important; margin: 0; padding: 0;">
|
||||||
|
|
||||||
|
<div class="content" style="font-family: 'Helvetica Neue', 'Helvetica', Helvetica, Arial, sans-serif; max-width: 100%; display: block; margin: 0; padding: 15px;">
|
||||||
|
<table bgcolor="#FFFFFF" style="font-family: 'Helvetica Neue', 'Helvetica', Helvetica, Arial, sans-serif; width: 100%; margin: 0; padding: 0;"><tr style="font-family: 'Helvetica Neue', 'Helvetica', Helvetica, Arial, sans-serif; margin: 0; padding: 0;"><td style="font-family: 'Helvetica Neue', 'Helvetica', Helvetica, Arial, sans-serif; margin: 0; padding: 0;"><img src="{{ app_logo }}" style="font-family: 'Helvetica Neue', 'Helvetica', Helvetica, Arial, sans-serif; max-width: 100%; margin: 0; padding: 0;" alt="{{ app_title }}" title="{{ app_title }}"/></td>
|
||||||
|
</tr></table></div>
|
||||||
|
|
||||||
|
</td>
|
||||||
|
<td style="font-family: 'Helvetica Neue', 'Helvetica', Helvetica, Arial, sans-serif; margin: 0; padding: 0;"></td>
|
||||||
|
</tr></table><!-- /HEADER --><!-- BODY --><table class="body-wrap" style="font-family: 'Helvetica Neue', 'Helvetica', Helvetica, Arial, sans-serif; width: 100%; margin: 0; padding: 0;"><tr style="font-family: 'Helvetica Neue', 'Helvetica', Helvetica, Arial, sans-serif; margin: 0; padding: 0;"><td style="font-family: 'Helvetica Neue', 'Helvetica', Helvetica, Arial, sans-serif; margin: 0; padding: 0;"></td>
|
||||||
|
<td class="container" bgcolor="#FFFFFF" style="font-family: 'Helvetica Neue', 'Helvetica', Helvetica, Arial, sans-serif; display: block !important; max-width: 100% !important; clear: both !important; margin: 0; padding: 0;">
|
||||||
|
|
||||||
|
<div class="content" style="font-family: 'Helvetica Neue', 'Helvetica', Helvetica, Arial, sans-serif; max-width: 100%; display: block; margin: 0; padding: 15px;">
|
||||||
|
<table style="font-family: 'Helvetica Neue', 'Helvetica', Helvetica, Arial, sans-serif; width: 100%; margin: 0; padding: 0;"><tr style="font-family: 'Helvetica Neue', 'Helvetica', Helvetica, Arial, sans-serif; margin: 0; padding: 0;"><td style="font-family: 'Helvetica Neue', 'Helvetica', Helvetica, Arial, sans-serif; margin: 0; padding: 0;">
|
||||||
|
{% block content %}{% endblock %}
|
||||||
|
</td>
|
||||||
|
</tr></table></div><!-- /content -->
|
||||||
|
|
||||||
|
</td>
|
||||||
|
<td style="font-family: 'Helvetica Neue', 'Helvetica', Helvetica, Arial, sans-serif; margin: 0; padding: 0;"></td>
|
||||||
|
</tr></table><!-- /BODY -->
|
||||||
|
</body>
|
||||||
|
</html>
|
13
emails/changeemail.html
Normal file
13
emails/changeemail.html
Normal file
|
@ -0,0 +1,13 @@
|
||||||
|
{% extends "base.html" %}
|
||||||
|
|
||||||
|
{% block content %}
|
||||||
|
|
||||||
|
<h3>E-mail Address Change Requested</h3>
|
||||||
|
|
||||||
|
This email address was recently asked to become the new e-mail address for user {{ username | user_reference }}.
|
||||||
|
<br>
|
||||||
|
<br>
|
||||||
|
To confirm this change, please click the following link:<br>
|
||||||
|
{{ app_link('confirm?code=' + token) }}
|
||||||
|
|
||||||
|
{% endblock %}
|
13
emails/confirmemail.html
Normal file
13
emails/confirmemail.html
Normal file
|
@ -0,0 +1,13 @@
|
||||||
|
{% extends "base.html" %}
|
||||||
|
|
||||||
|
{% block content %}
|
||||||
|
|
||||||
|
<h3>Please Confirm E-mail Address</h3>
|
||||||
|
|
||||||
|
This email address was recently used to register user {{ username | user_reference }}.
|
||||||
|
<br>
|
||||||
|
<br>
|
||||||
|
To confirm this email address, please click the following link:<br>
|
||||||
|
{{ app_link('confirm?code=' + token) }}
|
||||||
|
|
||||||
|
{% endblock %}
|
12
emails/emailchanged.html
Normal file
12
emails/emailchanged.html
Normal file
|
@ -0,0 +1,12 @@
|
||||||
|
{% extends "base.html" %}
|
||||||
|
|
||||||
|
{% block content %}
|
||||||
|
|
||||||
|
<h3>Account E-mail Address Changed</h3>
|
||||||
|
|
||||||
|
The email address for user {{ username | user_reference }} has been changed from this e-mail address to {{ new_email }}.
|
||||||
|
<br>
|
||||||
|
<br>
|
||||||
|
If this change was not expected, please immediately log into your {{ username | admin_reference }} and reset your email address.
|
||||||
|
|
||||||
|
{% endblock %}
|
13
emails/passwordchanged.html
Normal file
13
emails/passwordchanged.html
Normal file
|
@ -0,0 +1,13 @@
|
||||||
|
{% extends "base.html" %}
|
||||||
|
|
||||||
|
{% block content %}
|
||||||
|
|
||||||
|
<h3>Account Password Changed</h3>
|
||||||
|
|
||||||
|
The password for user {{ username | user_reference }} has been updated.
|
||||||
|
<br>
|
||||||
|
<br>
|
||||||
|
If this change was not expected, please immediately log into your account settings and reset your email address,
|
||||||
|
or <a href="https://quay.io/contact">contact support</a>.
|
||||||
|
|
||||||
|
{% endblock %}
|
13
emails/paymentfailure.html
Normal file
13
emails/paymentfailure.html
Normal file
|
@ -0,0 +1,13 @@
|
||||||
|
{% extends "base.html" %}
|
||||||
|
|
||||||
|
{% block content %}
|
||||||
|
|
||||||
|
<h3>Subscription Payment Failure</h3>
|
||||||
|
|
||||||
|
Your recent payment for account {{ username | user_reference }} failed, which usually results in our payments processor canceling
|
||||||
|
your subscription automatically. If you would like to continue to use {{ app_title }} without interruption,
|
||||||
|
please add a new card to {{ app_title }} and re-subscribe to your plan.<br>
|
||||||
|
<br>
|
||||||
|
You can find the card and subscription management features under your {{ username | admin_reference }}<br>
|
||||||
|
|
||||||
|
{% endblock %}
|
18
emails/recovery.html
Normal file
18
emails/recovery.html
Normal file
|
@ -0,0 +1,18 @@
|
||||||
|
{% extends "base.html" %}
|
||||||
|
|
||||||
|
{% block content %}
|
||||||
|
|
||||||
|
<h3>Account recovery</h3>
|
||||||
|
|
||||||
|
A user at {{ app_link() }} has attempted to recover their account
|
||||||
|
using this email address.
|
||||||
|
<br>
|
||||||
|
<br>
|
||||||
|
If you made this request, please click the following link to recover your account and
|
||||||
|
change your password:
|
||||||
|
{{ app_link('recovery?code=' + token) }}
|
||||||
|
<br><br>
|
||||||
|
If you did not make this request, your account has not been compromised and the user was
|
||||||
|
not given access. Please disregard this email.
|
||||||
|
|
||||||
|
{% endblock %}
|
13
emails/repoauthorizeemail.html
Normal file
13
emails/repoauthorizeemail.html
Normal file
|
@ -0,0 +1,13 @@
|
||||||
|
{% extends "base.html" %}
|
||||||
|
|
||||||
|
{% block content %}
|
||||||
|
|
||||||
|
<h3>Verify e-mail to receive repository notifications</h3>
|
||||||
|
|
||||||
|
A request has been made to send <a href="http://docs.quay.io/guides/notifications.html">notifications</a> to this email address for repository {{ (namespace, repository) | repository_reference }}
|
||||||
|
|
||||||
|
<br><br>
|
||||||
|
To verify this email address, please click the following link:<br>
|
||||||
|
{{ app_link('authrepoemail?code=' + token) }}
|
||||||
|
|
||||||
|
{% endblock %}
|
17
emails/teaminvite.html
Normal file
17
emails/teaminvite.html
Normal file
|
@ -0,0 +1,17 @@
|
||||||
|
{% extends "base.html" %}
|
||||||
|
|
||||||
|
{% block content %}
|
||||||
|
|
||||||
|
<h3>Invitation to join team: {{ teamname }}</h3>
|
||||||
|
|
||||||
|
{{ inviter | user_reference }} has invited you to join the team <b>{{ teamname }}</b> under organization {{ organization | user_reference }}.
|
||||||
|
|
||||||
|
<br><br>
|
||||||
|
|
||||||
|
To join the team, please click the following link:<br>
|
||||||
|
{{ app_link('confirminvite?code=' + token) }}
|
||||||
|
|
||||||
|
<br><br>
|
||||||
|
If you were not expecting this invitation, you can ignore this email.
|
||||||
|
|
||||||
|
{% endblock %}
|
|
@ -27,8 +27,8 @@ api_bp = Blueprint('api', __name__)
|
||||||
api = Api()
|
api = Api()
|
||||||
api.init_app(api_bp)
|
api.init_app(api_bp)
|
||||||
api.decorators = [csrf_protect,
|
api.decorators = [csrf_protect,
|
||||||
process_oauth,
|
crossdomain(origin='*', headers=['Authorization', 'Content-Type']),
|
||||||
crossdomain(origin='*', headers=['Authorization', 'Content-Type'])]
|
process_oauth]
|
||||||
|
|
||||||
|
|
||||||
class ApiException(Exception):
|
class ApiException(Exception):
|
||||||
|
@ -90,6 +90,7 @@ def handle_api_error(error):
|
||||||
if error.error_type is not None:
|
if error.error_type is not None:
|
||||||
response.headers['WWW-Authenticate'] = ('Bearer error="%s" error_description="%s"' %
|
response.headers['WWW-Authenticate'] = ('Bearer error="%s" error_description="%s"' %
|
||||||
(error.error_type, error.error_description))
|
(error.error_type, error.error_description))
|
||||||
|
|
||||||
return response
|
return response
|
||||||
|
|
||||||
|
|
||||||
|
@ -191,6 +192,7 @@ def query_param(name, help_str, type=reqparse.text_type, default=None,
|
||||||
'default': default,
|
'default': default,
|
||||||
'choices': choices,
|
'choices': choices,
|
||||||
'required': required,
|
'required': required,
|
||||||
|
'location': ('args')
|
||||||
})
|
})
|
||||||
return func
|
return func
|
||||||
return add_param
|
return add_param
|
||||||
|
|
|
@ -169,7 +169,7 @@ class RepositoryBuildList(RepositoryParamResource):
|
||||||
# was used.
|
# was used.
|
||||||
associated_repository = model.get_repository_for_resource(dockerfile_id)
|
associated_repository = model.get_repository_for_resource(dockerfile_id)
|
||||||
if associated_repository:
|
if associated_repository:
|
||||||
if not ModifyRepositoryPermission(associated_repository.namespace,
|
if not ModifyRepositoryPermission(associated_repository.namespace_user.username,
|
||||||
associated_repository.name):
|
associated_repository.name):
|
||||||
raise Unauthorized()
|
raise Unauthorized()
|
||||||
|
|
||||||
|
|
|
@ -125,7 +125,11 @@ def swagger_route_data(include_internal=False, compact=False):
|
||||||
new_operation['requires_fresh_login'] = True
|
new_operation['requires_fresh_login'] = True
|
||||||
|
|
||||||
if not internal or (internal and include_internal):
|
if not internal or (internal and include_internal):
|
||||||
operations.append(new_operation)
|
# Swagger requires valid nicknames on all operations.
|
||||||
|
if new_operation.get('nickname'):
|
||||||
|
operations.append(new_operation)
|
||||||
|
else:
|
||||||
|
logger.debug('Operation missing nickname: %s' % method)
|
||||||
|
|
||||||
swagger_path = PARAM_REGEX.sub(r'{\2}', rule.rule)
|
swagger_path = PARAM_REGEX.sub(r'{\2}', rule.rule)
|
||||||
new_resource = {
|
new_resource = {
|
||||||
|
|
|
@ -9,22 +9,33 @@ from data import model
|
||||||
from util.cache import cache_control_flask_restful
|
from util.cache import cache_control_flask_restful
|
||||||
|
|
||||||
|
|
||||||
def image_view(image):
|
def image_view(image, image_map):
|
||||||
extended_props = image
|
extended_props = image
|
||||||
if image.storage and image.storage.id:
|
if image.storage and image.storage.id:
|
||||||
extended_props = image.storage
|
extended_props = image.storage
|
||||||
|
|
||||||
command = extended_props.command
|
command = extended_props.command
|
||||||
|
|
||||||
|
def docker_id(aid):
|
||||||
|
if not aid:
|
||||||
|
return ''
|
||||||
|
|
||||||
|
return image_map[aid]
|
||||||
|
|
||||||
|
# Calculate the ancestors string, with the DBID's replaced with the docker IDs.
|
||||||
|
ancestors = [docker_id(a) for a in image.ancestors.split('/')]
|
||||||
|
ancestors_string = '/'.join(ancestors)
|
||||||
|
|
||||||
return {
|
return {
|
||||||
'id': image.docker_image_id,
|
'id': image.docker_image_id,
|
||||||
'created': format_date(extended_props.created),
|
'created': format_date(extended_props.created),
|
||||||
'comment': extended_props.comment,
|
'comment': extended_props.comment,
|
||||||
'command': json.loads(command) if command else None,
|
'command': json.loads(command) if command else None,
|
||||||
'ancestors': image.ancestors,
|
|
||||||
'dbid': image.id,
|
|
||||||
'size': extended_props.image_size,
|
'size': extended_props.image_size,
|
||||||
'locations': list(image.storage.locations),
|
'locations': list(image.storage.locations),
|
||||||
'uploading': image.storage.uploading,
|
'uploading': image.storage.uploading,
|
||||||
|
'ancestors': ancestors_string,
|
||||||
|
'sort_index': len(image.ancestors)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
@ -42,14 +53,16 @@ class RepositoryImageList(RepositoryParamResource):
|
||||||
for tag in all_tags:
|
for tag in all_tags:
|
||||||
tags_by_image_id[tag.image.docker_image_id].append(tag.name)
|
tags_by_image_id[tag.image.docker_image_id].append(tag.name)
|
||||||
|
|
||||||
|
image_map = {}
|
||||||
|
for image in all_images:
|
||||||
|
image_map[str(image.id)] = image.docker_image_id
|
||||||
|
|
||||||
def add_tags(image_json):
|
def add_tags(image_json):
|
||||||
image_json['tags'] = tags_by_image_id[image_json['id']]
|
image_json['tags'] = tags_by_image_id[image_json['id']]
|
||||||
return image_json
|
return image_json
|
||||||
|
|
||||||
|
|
||||||
return {
|
return {
|
||||||
'images': [add_tags(image_view(image)) for image in all_images]
|
'images': [add_tags(image_view(image, image_map)) for image in all_images]
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
@ -64,7 +77,12 @@ class RepositoryImage(RepositoryParamResource):
|
||||||
if not image:
|
if not image:
|
||||||
raise NotFound()
|
raise NotFound()
|
||||||
|
|
||||||
return image_view(image)
|
# Lookup all the ancestor images for the image.
|
||||||
|
image_map = {}
|
||||||
|
for current_image in model.get_parent_images(namespace, repository, image):
|
||||||
|
image_map[str(current_image.id)] = image.docker_image_id
|
||||||
|
|
||||||
|
return image_view(image, image_map)
|
||||||
|
|
||||||
|
|
||||||
@resource('/v1/repository/<repopath:repository>/image/<image_id>/changes')
|
@resource('/v1/repository/<repopath:repository>/image/<image_id>/changes')
|
||||||
|
|
|
@ -3,7 +3,8 @@ import logging
|
||||||
from flask import request, abort
|
from flask import request, abort
|
||||||
|
|
||||||
from endpoints.api import (resource, nickname, require_repo_admin, RepositoryParamResource,
|
from endpoints.api import (resource, nickname, require_repo_admin, RepositoryParamResource,
|
||||||
log_action, validate_json_request, NotFound, internal_only)
|
log_action, validate_json_request, NotFound, internal_only,
|
||||||
|
show_if)
|
||||||
|
|
||||||
from app import tf
|
from app import tf
|
||||||
from data import model
|
from data import model
|
||||||
|
@ -19,12 +20,13 @@ def record_view(record):
|
||||||
return {
|
return {
|
||||||
'email': record.email,
|
'email': record.email,
|
||||||
'repository': record.repository.name,
|
'repository': record.repository.name,
|
||||||
'namespace': record.repository.namespace,
|
'namespace': record.repository.namespace_user.username,
|
||||||
'confirmed': record.confirmed
|
'confirmed': record.confirmed
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
@internal_only
|
@internal_only
|
||||||
|
@show_if(features.MAILING)
|
||||||
@resource('/v1/repository/<repopath:repository>/authorizedemail/<email>')
|
@resource('/v1/repository/<repopath:repository>/authorizedemail/<email>')
|
||||||
class RepositoryAuthorizedEmail(RepositoryParamResource):
|
class RepositoryAuthorizedEmail(RepositoryParamResource):
|
||||||
""" Resource for checking and authorizing e-mail addresses to receive repo notifications. """
|
""" Resource for checking and authorizing e-mail addresses to receive repo notifications. """
|
||||||
|
|
|
@ -80,8 +80,7 @@ class RepositoryList(ApiResource):
|
||||||
|
|
||||||
visibility = req['visibility']
|
visibility = req['visibility']
|
||||||
|
|
||||||
repo = model.create_repository(namespace_name, repository_name, owner,
|
repo = model.create_repository(namespace_name, repository_name, owner, visibility)
|
||||||
visibility)
|
|
||||||
repo.description = req['description']
|
repo.description = req['description']
|
||||||
repo.save()
|
repo.save()
|
||||||
|
|
||||||
|
@ -110,7 +109,7 @@ class RepositoryList(ApiResource):
|
||||||
"""Fetch the list of repositories under a variety of situations."""
|
"""Fetch the list of repositories under a variety of situations."""
|
||||||
def repo_view(repo_obj):
|
def repo_view(repo_obj):
|
||||||
return {
|
return {
|
||||||
'namespace': repo_obj.namespace,
|
'namespace': repo_obj.namespace_user.username,
|
||||||
'name': repo_obj.name,
|
'name': repo_obj.name,
|
||||||
'description': repo_obj.description,
|
'description': repo_obj.description,
|
||||||
'is_public': repo_obj.visibility.name == 'public',
|
'is_public': repo_obj.visibility.name == 'public',
|
||||||
|
@ -134,7 +133,8 @@ class RepositoryList(ApiResource):
|
||||||
|
|
||||||
response['repositories'] = [repo_view(repo) for repo in repo_query
|
response['repositories'] = [repo_view(repo) for repo in repo_query
|
||||||
if (repo.visibility.name == 'public' or
|
if (repo.visibility.name == 'public' or
|
||||||
ReadRepositoryPermission(repo.namespace, repo.name).can())]
|
ReadRepositoryPermission(repo.namespace_user.username,
|
||||||
|
repo.name).can())]
|
||||||
|
|
||||||
return response
|
return response
|
||||||
|
|
||||||
|
@ -168,8 +168,7 @@ class Repository(RepositoryParamResource):
|
||||||
def tag_view(tag):
|
def tag_view(tag):
|
||||||
return {
|
return {
|
||||||
'name': tag.name,
|
'name': tag.name,
|
||||||
'image_id': tag.image.docker_image_id,
|
'image_id': tag.image.docker_image_id
|
||||||
'dbid': tag.image.id
|
|
||||||
}
|
}
|
||||||
|
|
||||||
organization = None
|
organization = None
|
||||||
|
|
|
@ -111,7 +111,7 @@ class FindRepositories(ApiResource):
|
||||||
|
|
||||||
def repo_view(repo):
|
def repo_view(repo):
|
||||||
return {
|
return {
|
||||||
'namespace': repo.namespace,
|
'namespace': repo.namespace_user.username,
|
||||||
'name': repo.name,
|
'name': repo.name,
|
||||||
'description': repo.description
|
'description': repo.description
|
||||||
}
|
}
|
||||||
|
@ -125,5 +125,5 @@ class FindRepositories(ApiResource):
|
||||||
return {
|
return {
|
||||||
'repositories': [repo_view(repo) for repo in matching
|
'repositories': [repo_view(repo) for repo in matching
|
||||||
if (repo.visibility.name == 'public' or
|
if (repo.visibility.name == 'public' or
|
||||||
ReadRepositoryPermission(repo.namespace, repo.name).can())]
|
ReadRepositoryPermission(repo.namespace_user.username, repo.name).can())]
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,20 +1,22 @@
|
||||||
|
import string
|
||||||
import logging
|
import logging
|
||||||
import json
|
import json
|
||||||
|
|
||||||
|
from random import SystemRandom
|
||||||
from app import app
|
from app import app
|
||||||
|
|
||||||
from flask import request
|
from flask import request
|
||||||
|
|
||||||
from endpoints.api import (ApiResource, nickname, resource, validate_json_request, request_error,
|
from endpoints.api import (ApiResource, nickname, resource, validate_json_request, request_error,
|
||||||
log_action, internal_only, NotFound, require_user_admin, format_date,
|
log_action, internal_only, NotFound, require_user_admin, format_date,
|
||||||
InvalidToken, require_scope, format_date, hide_if, show_if, parse_args,
|
InvalidToken, require_scope, format_date, hide_if, show_if, parse_args,
|
||||||
query_param, abort)
|
query_param, abort, require_fresh_login)
|
||||||
|
|
||||||
from endpoints.api.logs import get_logs
|
from endpoints.api.logs import get_logs
|
||||||
|
|
||||||
from data import model
|
from data import model
|
||||||
from auth.permissions import SuperUserPermission
|
from auth.permissions import SuperUserPermission
|
||||||
from auth.auth_context import get_authenticated_user
|
from auth.auth_context import get_authenticated_user
|
||||||
|
from util.useremails import send_confirmation_email, send_recovery_email
|
||||||
|
|
||||||
import features
|
import features
|
||||||
|
|
||||||
|
@ -55,6 +57,26 @@ def user_view(user):
|
||||||
@show_if(features.SUPER_USERS)
|
@show_if(features.SUPER_USERS)
|
||||||
class SuperUserList(ApiResource):
|
class SuperUserList(ApiResource):
|
||||||
""" Resource for listing users in the system. """
|
""" Resource for listing users in the system. """
|
||||||
|
schemas = {
|
||||||
|
'CreateInstallUser': {
|
||||||
|
'id': 'CreateInstallUser',
|
||||||
|
'description': 'Data for creating a user',
|
||||||
|
'required': ['username', 'email'],
|
||||||
|
'properties': {
|
||||||
|
'username': {
|
||||||
|
'type': 'string',
|
||||||
|
'description': 'The username of the user being created'
|
||||||
|
},
|
||||||
|
|
||||||
|
'email': {
|
||||||
|
'type': 'string',
|
||||||
|
'description': 'The email address of the user being created'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@require_fresh_login
|
||||||
@nickname('listAllUsers')
|
@nickname('listAllUsers')
|
||||||
def get(self):
|
def get(self):
|
||||||
""" Returns a list of all users in the system. """
|
""" Returns a list of all users in the system. """
|
||||||
|
@ -67,6 +89,63 @@ class SuperUserList(ApiResource):
|
||||||
abort(403)
|
abort(403)
|
||||||
|
|
||||||
|
|
||||||
|
@require_fresh_login
|
||||||
|
@nickname('createInstallUser')
|
||||||
|
@validate_json_request('CreateInstallUser')
|
||||||
|
def post(self):
|
||||||
|
""" Creates a new user. """
|
||||||
|
user_information = request.get_json()
|
||||||
|
if SuperUserPermission().can():
|
||||||
|
username = user_information['username']
|
||||||
|
email = user_information['email']
|
||||||
|
|
||||||
|
# Generate a temporary password for the user.
|
||||||
|
random = SystemRandom()
|
||||||
|
password = ''.join([random.choice(string.ascii_uppercase + string.digits) for _ in range(32)])
|
||||||
|
|
||||||
|
# Create the user.
|
||||||
|
user = model.create_user(username, password, email, auto_verify=not features.MAILING)
|
||||||
|
|
||||||
|
# If mailing is turned on, send the user a verification email.
|
||||||
|
if features.MAILING:
|
||||||
|
confirmation = model.create_confirm_email_code(user, new_email=user.email)
|
||||||
|
send_confirmation_email(user.username, user.email, confirmation.code)
|
||||||
|
|
||||||
|
return {
|
||||||
|
'username': username,
|
||||||
|
'email': email,
|
||||||
|
'password': password
|
||||||
|
}
|
||||||
|
|
||||||
|
abort(403)
|
||||||
|
|
||||||
|
|
||||||
|
@resource('/v1/superusers/users/<username>/sendrecovery')
|
||||||
|
@internal_only
|
||||||
|
@show_if(features.SUPER_USERS)
|
||||||
|
@show_if(features.MAILING)
|
||||||
|
class SuperUserSendRecoveryEmail(ApiResource):
|
||||||
|
""" Resource for sending a recovery user on behalf of a user. """
|
||||||
|
@require_fresh_login
|
||||||
|
@nickname('sendInstallUserRecoveryEmail')
|
||||||
|
def post(self, username):
|
||||||
|
if SuperUserPermission().can():
|
||||||
|
user = model.get_user(username)
|
||||||
|
if not user or user.organization or user.robot:
|
||||||
|
abort(404)
|
||||||
|
|
||||||
|
if username in app.config['SUPER_USERS']:
|
||||||
|
abort(403)
|
||||||
|
|
||||||
|
code = model.create_reset_password_email_code(user.email)
|
||||||
|
send_recovery_email(user.email, code.code)
|
||||||
|
return {
|
||||||
|
'email': user.email
|
||||||
|
}
|
||||||
|
|
||||||
|
abort(403)
|
||||||
|
|
||||||
|
|
||||||
@resource('/v1/superuser/users/<username>')
|
@resource('/v1/superuser/users/<username>')
|
||||||
@internal_only
|
@internal_only
|
||||||
@show_if(features.SUPER_USERS)
|
@show_if(features.SUPER_USERS)
|
||||||
|
@ -90,18 +169,20 @@ class SuperUserManagement(ApiResource):
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@require_fresh_login
|
||||||
@nickname('getInstallUser')
|
@nickname('getInstallUser')
|
||||||
def get(self, username):
|
def get(self, username):
|
||||||
""" Returns information about the specified user. """
|
""" Returns information about the specified user. """
|
||||||
if SuperUserPermission().can():
|
if SuperUserPermission().can():
|
||||||
user = model.get_user(username)
|
user = model.get_user(username)
|
||||||
if not user or user.organization or user.robot:
|
if not user or user.organization or user.robot:
|
||||||
abort(404)
|
abort(404)
|
||||||
|
|
||||||
return user_view(user)
|
return user_view(user)
|
||||||
|
|
||||||
abort(403)
|
abort(403)
|
||||||
|
|
||||||
|
@require_fresh_login
|
||||||
@nickname('deleteInstallUser')
|
@nickname('deleteInstallUser')
|
||||||
def delete(self, username):
|
def delete(self, username):
|
||||||
""" Deletes the specified user. """
|
""" Deletes the specified user. """
|
||||||
|
@ -118,6 +199,7 @@ class SuperUserManagement(ApiResource):
|
||||||
|
|
||||||
abort(403)
|
abort(403)
|
||||||
|
|
||||||
|
@require_fresh_login
|
||||||
@nickname('changeInstallUser')
|
@nickname('changeInstallUser')
|
||||||
@validate_json_request('UpdateUser')
|
@validate_json_request('UpdateUser')
|
||||||
def put(self, username):
|
def put(self, username):
|
||||||
|
|
|
@ -85,11 +85,14 @@ class RepositoryTagImages(RepositoryParamResource):
|
||||||
raise NotFound()
|
raise NotFound()
|
||||||
|
|
||||||
parent_images = model.get_parent_images(namespace, repository, tag_image)
|
parent_images = model.get_parent_images(namespace, repository, tag_image)
|
||||||
|
image_map = {}
|
||||||
|
for image in parent_images:
|
||||||
|
image_map[str(image.id)] = image.docker_image_id
|
||||||
|
|
||||||
parents = list(parent_images)
|
parents = list(parent_images)
|
||||||
parents.reverse()
|
parents.reverse()
|
||||||
all_images = [tag_image] + parents
|
all_images = [tag_image] + parents
|
||||||
|
|
||||||
return {
|
return {
|
||||||
'images': [image_view(image) for image in all_images]
|
'images': [image_view(image, image_map) for image in all_images]
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,12 +1,51 @@
|
||||||
from flask import request
|
from flask import request
|
||||||
|
|
||||||
from endpoints.api import (resource, nickname, ApiResource, validate_json_request, request_error,
|
from endpoints.api import (resource, nickname, ApiResource, validate_json_request, request_error,
|
||||||
log_action, Unauthorized, NotFound, internal_only, require_scope)
|
log_action, Unauthorized, NotFound, internal_only, require_scope,
|
||||||
|
query_param, truthy_bool, parse_args, require_user_admin, show_if)
|
||||||
from auth.permissions import AdministerOrganizationPermission, ViewTeamPermission
|
from auth.permissions import AdministerOrganizationPermission, ViewTeamPermission
|
||||||
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 data import model
|
from data import model
|
||||||
|
from util.useremails import send_org_invite_email
|
||||||
|
from util.gravatar import compute_hash
|
||||||
|
|
||||||
|
import features
|
||||||
|
|
||||||
|
def try_accept_invite(code, user):
|
||||||
|
(team, inviter) = model.confirm_team_invite(code, user)
|
||||||
|
|
||||||
|
model.delete_matching_notifications(user, 'org_team_invite', code=code)
|
||||||
|
|
||||||
|
orgname = team.organization.username
|
||||||
|
log_action('org_team_member_invite_accepted', orgname, {
|
||||||
|
'member': user.username,
|
||||||
|
'team': team.name,
|
||||||
|
'inviter': inviter.username
|
||||||
|
})
|
||||||
|
|
||||||
|
return team
|
||||||
|
|
||||||
|
|
||||||
|
def handle_addinvite_team(inviter, team, user=None, email=None):
|
||||||
|
invite = model.add_or_invite_to_team(inviter, team, user, email,
|
||||||
|
requires_invite = features.MAILING)
|
||||||
|
if not invite:
|
||||||
|
# User was added to the team directly.
|
||||||
|
return
|
||||||
|
|
||||||
|
orgname = team.organization.username
|
||||||
|
if user:
|
||||||
|
model.create_notification('org_team_invite', user, metadata = {
|
||||||
|
'code': invite.invite_token,
|
||||||
|
'inviter': inviter.username,
|
||||||
|
'org': orgname,
|
||||||
|
'team': team.name
|
||||||
|
})
|
||||||
|
|
||||||
|
send_org_invite_email(user.username if user else email, user.email if user else email,
|
||||||
|
orgname, team.name, inviter.username, invite.invite_token)
|
||||||
|
return invite
|
||||||
|
|
||||||
def team_view(orgname, team):
|
def team_view(orgname, team):
|
||||||
view_permission = ViewTeamPermission(orgname, team.name)
|
view_permission = ViewTeamPermission(orgname, team.name)
|
||||||
|
@ -19,14 +58,28 @@ def team_view(orgname, team):
|
||||||
'role': role
|
'role': role
|
||||||
}
|
}
|
||||||
|
|
||||||
def member_view(member):
|
def member_view(member, invited=False):
|
||||||
return {
|
return {
|
||||||
'name': member.username,
|
'name': member.username,
|
||||||
'kind': 'user',
|
'kind': 'user',
|
||||||
'is_robot': member.robot,
|
'is_robot': member.robot,
|
||||||
|
'gravatar': compute_hash(member.email) if not member.robot else None,
|
||||||
|
'invited': invited,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
def invite_view(invite):
|
||||||
|
if invite.user:
|
||||||
|
return member_view(invite.user, invited=True)
|
||||||
|
else:
|
||||||
|
return {
|
||||||
|
'email': invite.email,
|
||||||
|
'kind': 'invite',
|
||||||
|
'gravatar': compute_hash(invite.email),
|
||||||
|
'invited': True
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
@resource('/v1/organization/<orgname>/team/<teamname>')
|
@resource('/v1/organization/<orgname>/team/<teamname>')
|
||||||
@internal_only
|
@internal_only
|
||||||
class OrganizationTeam(ApiResource):
|
class OrganizationTeam(ApiResource):
|
||||||
|
@ -114,8 +167,10 @@ class OrganizationTeam(ApiResource):
|
||||||
@internal_only
|
@internal_only
|
||||||
class TeamMemberList(ApiResource):
|
class TeamMemberList(ApiResource):
|
||||||
""" Resource for managing the list of members for a team. """
|
""" Resource for managing the list of members for a team. """
|
||||||
|
@parse_args
|
||||||
|
@query_param('includePending', 'Whether to include pending members', type=truthy_bool, default=False)
|
||||||
@nickname('getOrganizationTeamMembers')
|
@nickname('getOrganizationTeamMembers')
|
||||||
def get(self, orgname, teamname):
|
def get(self, args, orgname, teamname):
|
||||||
""" Retrieve the list of members for the specified team. """
|
""" Retrieve the list of members for the specified team. """
|
||||||
view_permission = ViewTeamPermission(orgname, teamname)
|
view_permission = ViewTeamPermission(orgname, teamname)
|
||||||
edit_permission = AdministerOrganizationPermission(orgname)
|
edit_permission = AdministerOrganizationPermission(orgname)
|
||||||
|
@ -128,11 +183,18 @@ class TeamMemberList(ApiResource):
|
||||||
raise NotFound()
|
raise NotFound()
|
||||||
|
|
||||||
members = model.get_organization_team_members(team.id)
|
members = model.get_organization_team_members(team.id)
|
||||||
return {
|
invites = []
|
||||||
'members': {m.username : member_view(m) for m in members},
|
|
||||||
|
if args['includePending'] and edit_permission.can():
|
||||||
|
invites = model.get_organization_team_member_invites(team.id)
|
||||||
|
|
||||||
|
data = {
|
||||||
|
'members': [member_view(m) for m in members] + [invite_view(i) for i in invites],
|
||||||
'can_edit': edit_permission.can()
|
'can_edit': edit_permission.can()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
return data
|
||||||
|
|
||||||
raise Unauthorized()
|
raise Unauthorized()
|
||||||
|
|
||||||
|
|
||||||
|
@ -142,7 +204,7 @@ class TeamMember(ApiResource):
|
||||||
@require_scope(scopes.ORG_ADMIN)
|
@require_scope(scopes.ORG_ADMIN)
|
||||||
@nickname('updateOrganizationTeamMember')
|
@nickname('updateOrganizationTeamMember')
|
||||||
def put(self, orgname, teamname, membername):
|
def put(self, orgname, teamname, membername):
|
||||||
""" Add a member to an existing team. """
|
""" Adds or invites a member to an existing team. """
|
||||||
permission = AdministerOrganizationPermission(orgname)
|
permission = AdministerOrganizationPermission(orgname)
|
||||||
if permission.can():
|
if permission.can():
|
||||||
team = None
|
team = None
|
||||||
|
@ -159,23 +221,151 @@ class TeamMember(ApiResource):
|
||||||
if not user:
|
if not user:
|
||||||
raise request_error(message='Unknown user')
|
raise request_error(message='Unknown user')
|
||||||
|
|
||||||
# Add the user to the team.
|
# Add or invite the user to the team.
|
||||||
model.add_user_to_team(user, team)
|
inviter = get_authenticated_user()
|
||||||
log_action('org_add_team_member', orgname, {'member': membername, 'team': teamname})
|
invite = handle_addinvite_team(inviter, team, user=user)
|
||||||
return member_view(user)
|
if not invite:
|
||||||
|
log_action('org_add_team_member', orgname, {'member': membername, 'team': teamname})
|
||||||
|
return member_view(user, invited=False)
|
||||||
|
|
||||||
|
# User was invited.
|
||||||
|
log_action('org_invite_team_member', orgname, {
|
||||||
|
'user': membername,
|
||||||
|
'member': membername,
|
||||||
|
'team': teamname
|
||||||
|
})
|
||||||
|
return member_view(user, invited=True)
|
||||||
|
|
||||||
raise Unauthorized()
|
raise Unauthorized()
|
||||||
|
|
||||||
@require_scope(scopes.ORG_ADMIN)
|
@require_scope(scopes.ORG_ADMIN)
|
||||||
@nickname('deleteOrganizationTeamMember')
|
@nickname('deleteOrganizationTeamMember')
|
||||||
def delete(self, orgname, teamname, membername):
|
def delete(self, orgname, teamname, membername):
|
||||||
""" Delete an existing member of a team. """
|
""" Delete a member of a team. If the user is merely invited to join
|
||||||
|
the team, then the invite is removed instead.
|
||||||
|
"""
|
||||||
permission = AdministerOrganizationPermission(orgname)
|
permission = AdministerOrganizationPermission(orgname)
|
||||||
if permission.can():
|
if permission.can():
|
||||||
# Remote the user from the team.
|
# Remote the user from the team.
|
||||||
invoking_user = get_authenticated_user().username
|
invoking_user = get_authenticated_user().username
|
||||||
|
|
||||||
|
# Find the team.
|
||||||
|
try:
|
||||||
|
team = model.get_organization_team(orgname, teamname)
|
||||||
|
except model.InvalidTeamException:
|
||||||
|
raise NotFound()
|
||||||
|
|
||||||
|
# Find the member.
|
||||||
|
member = model.get_user(membername)
|
||||||
|
if not member:
|
||||||
|
raise NotFound()
|
||||||
|
|
||||||
|
# First attempt to delete an invite for the user to this team. If none found,
|
||||||
|
# then we try to remove the user directly.
|
||||||
|
if model.delete_team_user_invite(team, member):
|
||||||
|
log_action('org_delete_team_member_invite', orgname, {
|
||||||
|
'user': membername,
|
||||||
|
'team': teamname,
|
||||||
|
'member': membername
|
||||||
|
})
|
||||||
|
return 'Deleted', 204
|
||||||
|
|
||||||
model.remove_user_from_team(orgname, teamname, membername, invoking_user)
|
model.remove_user_from_team(orgname, teamname, membername, invoking_user)
|
||||||
log_action('org_remove_team_member', orgname, {'member': membername, 'team': teamname})
|
log_action('org_remove_team_member', orgname, {'member': membername, 'team': teamname})
|
||||||
return 'Deleted', 204
|
return 'Deleted', 204
|
||||||
|
|
||||||
raise Unauthorized()
|
raise Unauthorized()
|
||||||
|
|
||||||
|
|
||||||
|
@resource('/v1/organization/<orgname>/team/<teamname>/invite/<email>')
|
||||||
|
@show_if(features.MAILING)
|
||||||
|
class InviteTeamMember(ApiResource):
|
||||||
|
""" Resource for inviting a team member via email address. """
|
||||||
|
@require_scope(scopes.ORG_ADMIN)
|
||||||
|
@nickname('inviteTeamMemberEmail')
|
||||||
|
def put(self, orgname, teamname, email):
|
||||||
|
""" Invites an email address to an existing team. """
|
||||||
|
permission = AdministerOrganizationPermission(orgname)
|
||||||
|
if permission.can():
|
||||||
|
team = None
|
||||||
|
|
||||||
|
# Find the team.
|
||||||
|
try:
|
||||||
|
team = model.get_organization_team(orgname, teamname)
|
||||||
|
except model.InvalidTeamException:
|
||||||
|
raise NotFound()
|
||||||
|
|
||||||
|
# Invite the email to the team.
|
||||||
|
inviter = get_authenticated_user()
|
||||||
|
invite = handle_addinvite_team(inviter, team, email=email)
|
||||||
|
log_action('org_invite_team_member', orgname, {
|
||||||
|
'email': email,
|
||||||
|
'team': teamname,
|
||||||
|
'member': email
|
||||||
|
})
|
||||||
|
return invite_view(invite)
|
||||||
|
|
||||||
|
raise Unauthorized()
|
||||||
|
|
||||||
|
@require_scope(scopes.ORG_ADMIN)
|
||||||
|
@nickname('deleteTeamMemberEmailInvite')
|
||||||
|
def delete(self, orgname, teamname, email):
|
||||||
|
""" Delete an invite of an email address to join a team. """
|
||||||
|
permission = AdministerOrganizationPermission(orgname)
|
||||||
|
if permission.can():
|
||||||
|
team = None
|
||||||
|
|
||||||
|
# Find the team.
|
||||||
|
try:
|
||||||
|
team = model.get_organization_team(orgname, teamname)
|
||||||
|
except model.InvalidTeamException:
|
||||||
|
raise NotFound()
|
||||||
|
|
||||||
|
# Delete the invite.
|
||||||
|
model.delete_team_email_invite(team, email)
|
||||||
|
log_action('org_delete_team_member_invite', orgname, {
|
||||||
|
'email': email,
|
||||||
|
'team': teamname,
|
||||||
|
'member': email
|
||||||
|
})
|
||||||
|
return 'Deleted', 204
|
||||||
|
|
||||||
|
raise Unauthorized()
|
||||||
|
|
||||||
|
|
||||||
|
@resource('/v1/teaminvite/<code>')
|
||||||
|
@internal_only
|
||||||
|
@show_if(features.MAILING)
|
||||||
|
class TeamMemberInvite(ApiResource):
|
||||||
|
""" Resource for managing invites to jon a team. """
|
||||||
|
@require_user_admin
|
||||||
|
@nickname('acceptOrganizationTeamInvite')
|
||||||
|
def put(self, code):
|
||||||
|
""" Accepts an invite to join a team in an organization. """
|
||||||
|
# Accept the invite for the current user.
|
||||||
|
team = try_accept_invite(code, get_authenticated_user())
|
||||||
|
if not team:
|
||||||
|
raise NotFound()
|
||||||
|
|
||||||
|
orgname = team.organization.username
|
||||||
|
return {
|
||||||
|
'org': orgname,
|
||||||
|
'team': team.name
|
||||||
|
}
|
||||||
|
|
||||||
|
@nickname('declineOrganizationTeamInvite')
|
||||||
|
@require_user_admin
|
||||||
|
def delete(self, code):
|
||||||
|
""" Delete an existing member of a team. """
|
||||||
|
(team, inviter) = model.delete_team_invite(code, get_authenticated_user())
|
||||||
|
|
||||||
|
model.delete_matching_notifications(get_authenticated_user(), 'org_team_invite', code=code)
|
||||||
|
|
||||||
|
orgname = team.organization.username
|
||||||
|
log_action('org_team_member_invite_declined', orgname, {
|
||||||
|
'member': get_authenticated_user().username,
|
||||||
|
'team': team.name,
|
||||||
|
'inviter': inviter.username
|
||||||
|
})
|
||||||
|
|
||||||
|
return 'Deleted', 204
|
||||||
|
|
|
@ -14,7 +14,7 @@ from endpoints.api.build import (build_status_view, trigger_view, RepositoryBuil
|
||||||
from endpoints.common import start_build
|
from endpoints.common import start_build
|
||||||
from endpoints.trigger import (BuildTrigger as BuildTriggerBase, TriggerDeactivationException,
|
from endpoints.trigger import (BuildTrigger as BuildTriggerBase, TriggerDeactivationException,
|
||||||
TriggerActivationException, EmptyRepositoryException,
|
TriggerActivationException, EmptyRepositoryException,
|
||||||
RepositoryReadException)
|
RepositoryReadException, TriggerStartException)
|
||||||
from data import model
|
from data import model
|
||||||
from auth.permissions import UserAdminPermission, AdministerOrganizationPermission, ReadRepositoryPermission
|
from auth.permissions import UserAdminPermission, AdministerOrganizationPermission, ReadRepositoryPermission
|
||||||
from util.names import parse_robot_username
|
from util.names import parse_robot_username
|
||||||
|
@ -205,7 +205,7 @@ class BuildTriggerActivate(RepositoryParamResource):
|
||||||
'write')
|
'write')
|
||||||
|
|
||||||
try:
|
try:
|
||||||
repository_path = '%s/%s' % (trigger.repository.namespace,
|
repository_path = '%s/%s' % (trigger.repository.namespace_user.username,
|
||||||
trigger.repository.name)
|
trigger.repository.name)
|
||||||
path = url_for('webhooks.build_trigger_webhook',
|
path = url_for('webhooks.build_trigger_webhook',
|
||||||
repository=repository_path, trigger_uuid=trigger.uuid)
|
repository=repository_path, trigger_uuid=trigger.uuid)
|
||||||
|
@ -374,9 +374,24 @@ class BuildTriggerAnalyze(RepositoryParamResource):
|
||||||
@resource('/v1/repository/<repopath:repository>/trigger/<trigger_uuid>/start')
|
@resource('/v1/repository/<repopath:repository>/trigger/<trigger_uuid>/start')
|
||||||
class ActivateBuildTrigger(RepositoryParamResource):
|
class ActivateBuildTrigger(RepositoryParamResource):
|
||||||
""" Custom verb to manually activate a build trigger. """
|
""" Custom verb to manually activate a build trigger. """
|
||||||
|
schemas = {
|
||||||
|
'RunParameters': {
|
||||||
|
'id': 'RunParameters',
|
||||||
|
'type': 'object',
|
||||||
|
'description': 'Optional run parameters for activating the build trigger',
|
||||||
|
'additional_properties': False,
|
||||||
|
'properties': {
|
||||||
|
'branch_name': {
|
||||||
|
'type': 'string',
|
||||||
|
'description': '(GitHub Only) If specified, the name of the GitHub branch to build.'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
@require_repo_admin
|
@require_repo_admin
|
||||||
@nickname('manuallyStartBuildTrigger')
|
@nickname('manuallyStartBuildTrigger')
|
||||||
|
@validate_json_request('RunParameters')
|
||||||
def post(self, namespace, repository, trigger_uuid):
|
def post(self, namespace, repository, trigger_uuid):
|
||||||
""" Manually start a build from the specified trigger. """
|
""" Manually start a build from the specified trigger. """
|
||||||
try:
|
try:
|
||||||
|
@ -389,14 +404,18 @@ class ActivateBuildTrigger(RepositoryParamResource):
|
||||||
if not handler.is_active(config_dict):
|
if not handler.is_active(config_dict):
|
||||||
raise InvalidRequest('Trigger is not active.')
|
raise InvalidRequest('Trigger is not active.')
|
||||||
|
|
||||||
specs = handler.manual_start(trigger.auth_token, config_dict)
|
try:
|
||||||
dockerfile_id, tags, name, subdir = specs
|
run_parameters = request.get_json()
|
||||||
|
specs = handler.manual_start(trigger.auth_token, config_dict, run_parameters=run_parameters)
|
||||||
|
dockerfile_id, tags, name, subdir = specs
|
||||||
|
|
||||||
repo = model.get_repository(namespace, repository)
|
repo = model.get_repository(namespace, repository)
|
||||||
pull_robot_name = model.get_pull_robot_name(trigger)
|
pull_robot_name = model.get_pull_robot_name(trigger)
|
||||||
|
|
||||||
build_request = start_build(repo, dockerfile_id, tags, name, subdir, True,
|
build_request = start_build(repo, dockerfile_id, tags, name, subdir, True,
|
||||||
pull_robot_name=pull_robot_name)
|
pull_robot_name=pull_robot_name)
|
||||||
|
except TriggerStartException as tse:
|
||||||
|
raise InvalidRequest(tse.message)
|
||||||
|
|
||||||
resp = build_status_view(build_request, True)
|
resp = build_status_view(build_request, True)
|
||||||
repo_string = '%s/%s' % (namespace, repository)
|
repo_string = '%s/%s' % (namespace, repository)
|
||||||
|
@ -424,6 +443,36 @@ class TriggerBuildList(RepositoryParamResource):
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
@resource('/v1/repository/<repopath:repository>/trigger/<trigger_uuid>/fields/<field_name>')
|
||||||
|
@internal_only
|
||||||
|
class BuildTriggerFieldValues(RepositoryParamResource):
|
||||||
|
""" Custom verb to fetch a values list for a particular field name. """
|
||||||
|
@require_repo_admin
|
||||||
|
@nickname('listTriggerFieldValues')
|
||||||
|
def get(self, namespace, repository, trigger_uuid, field_name):
|
||||||
|
""" List the field values for a custom run field. """
|
||||||
|
try:
|
||||||
|
trigger = model.get_build_trigger(namespace, repository, trigger_uuid)
|
||||||
|
except model.InvalidBuildTriggerException:
|
||||||
|
raise NotFound()
|
||||||
|
|
||||||
|
user_permission = UserAdminPermission(trigger.connected_user.username)
|
||||||
|
if user_permission.can():
|
||||||
|
trigger_handler = BuildTriggerBase.get_trigger_for_service(trigger.service.name)
|
||||||
|
values = trigger_handler.list_field_values(trigger.auth_token, json.loads(trigger.config),
|
||||||
|
field_name)
|
||||||
|
|
||||||
|
if values is None:
|
||||||
|
raise NotFound()
|
||||||
|
|
||||||
|
return {
|
||||||
|
'values': values
|
||||||
|
}
|
||||||
|
else:
|
||||||
|
raise Unauthorized()
|
||||||
|
|
||||||
|
|
||||||
@resource('/v1/repository/<repopath:repository>/trigger/<trigger_uuid>/sources')
|
@resource('/v1/repository/<repopath:repository>/trigger/<trigger_uuid>/sources')
|
||||||
@internal_only
|
@internal_only
|
||||||
class BuildTriggerSources(RepositoryParamResource):
|
class BuildTriggerSources(RepositoryParamResource):
|
||||||
|
|
|
@ -12,6 +12,8 @@ from endpoints.api import (ApiResource, nickname, resource, validate_json_reques
|
||||||
license_error, require_fresh_login)
|
license_error, require_fresh_login)
|
||||||
from endpoints.api.subscribe import subscribe
|
from endpoints.api.subscribe import subscribe
|
||||||
from endpoints.common import common_login
|
from endpoints.common import common_login
|
||||||
|
from endpoints.api.team import try_accept_invite
|
||||||
|
|
||||||
from data import model
|
from data import model
|
||||||
from data.billing import get_plan
|
from data.billing import get_plan
|
||||||
from auth.permissions import (AdministerOrganizationPermission, CreateRepositoryPermission,
|
from auth.permissions import (AdministerOrganizationPermission, CreateRepositoryPermission,
|
||||||
|
@ -19,7 +21,8 @@ from auth.permissions import (AdministerOrganizationPermission, CreateRepository
|
||||||
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
|
from util.gravatar import compute_hash
|
||||||
from util.useremails import (send_confirmation_email, send_recovery_email, send_change_email)
|
from util.useremails import (send_confirmation_email, send_recovery_email, send_change_email, send_password_changed)
|
||||||
|
from util.names import parse_single_urn
|
||||||
|
|
||||||
import features
|
import features
|
||||||
|
|
||||||
|
@ -117,6 +120,10 @@ class User(ApiResource):
|
||||||
'type': 'string',
|
'type': 'string',
|
||||||
'description': 'The user\'s email address',
|
'description': 'The user\'s email address',
|
||||||
},
|
},
|
||||||
|
'invite_code': {
|
||||||
|
'type': 'string',
|
||||||
|
'description': 'The optional invite code'
|
||||||
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
'UpdateUser': {
|
'UpdateUser': {
|
||||||
|
@ -166,6 +173,9 @@ class User(ApiResource):
|
||||||
log_action('account_change_password', user.username)
|
log_action('account_change_password', user.username)
|
||||||
model.change_password(user, user_data['password'])
|
model.change_password(user, user_data['password'])
|
||||||
|
|
||||||
|
if features.MAILING:
|
||||||
|
send_password_changed(user.username, user.email)
|
||||||
|
|
||||||
if 'invoice_email' in user_data:
|
if 'invoice_email' in user_data:
|
||||||
logger.debug('Changing invoice_email for user: %s', user.username)
|
logger.debug('Changing invoice_email for user: %s', user.username)
|
||||||
model.change_invoice_email(user, user_data['invoice_email'])
|
model.change_invoice_email(user, user_data['invoice_email'])
|
||||||
|
@ -176,22 +186,27 @@ class User(ApiResource):
|
||||||
# Email already used.
|
# Email already used.
|
||||||
raise request_error(message='E-mail address already used')
|
raise request_error(message='E-mail address already used')
|
||||||
|
|
||||||
logger.debug('Sending email to change email address for user: %s',
|
if features.MAILING:
|
||||||
user.username)
|
logger.debug('Sending email to change email address for user: %s',
|
||||||
code = model.create_confirm_email_code(user, new_email=new_email)
|
user.username)
|
||||||
send_change_email(user.username, user_data['email'], code.code)
|
code = model.create_confirm_email_code(user, new_email=new_email)
|
||||||
|
send_change_email(user.username, user_data['email'], code.code)
|
||||||
|
else:
|
||||||
|
model.update_email(user, new_email, auto_verify=not features.MAILING)
|
||||||
|
|
||||||
except model.InvalidPasswordException, ex:
|
except model.InvalidPasswordException, ex:
|
||||||
raise request_error(exception=ex)
|
raise request_error(exception=ex)
|
||||||
|
|
||||||
return user_view(user)
|
return user_view(user)
|
||||||
|
|
||||||
|
@show_if(features.USER_CREATION)
|
||||||
@nickname('createNewUser')
|
@nickname('createNewUser')
|
||||||
@internal_only
|
@internal_only
|
||||||
@validate_json_request('NewUser')
|
@validate_json_request('NewUser')
|
||||||
def post(self):
|
def post(self):
|
||||||
""" Create a new user. """
|
""" Create a new user. """
|
||||||
user_data = request.get_json()
|
user_data = request.get_json()
|
||||||
|
invite_code = user_data.get('invite_code', '')
|
||||||
|
|
||||||
existing_user = model.get_user(user_data['username'])
|
existing_user = model.get_user(user_data['username'])
|
||||||
if existing_user:
|
if existing_user:
|
||||||
|
@ -199,10 +214,29 @@ class User(ApiResource):
|
||||||
|
|
||||||
try:
|
try:
|
||||||
new_user = model.create_user(user_data['username'], user_data['password'],
|
new_user = model.create_user(user_data['username'], user_data['password'],
|
||||||
user_data['email'])
|
user_data['email'], auto_verify=not features.MAILING)
|
||||||
code = model.create_confirm_email_code(new_user)
|
|
||||||
send_confirmation_email(new_user.username, new_user.email, code.code)
|
# Handle any invite codes.
|
||||||
return 'Created', 201
|
parsed_invite = parse_single_urn(invite_code)
|
||||||
|
if parsed_invite is not None:
|
||||||
|
if parsed_invite[0] == 'teaminvite':
|
||||||
|
# Add the user to the team.
|
||||||
|
try:
|
||||||
|
try_accept_invite(invite_code, new_user)
|
||||||
|
except model.DataModelException:
|
||||||
|
pass
|
||||||
|
|
||||||
|
|
||||||
|
if features.MAILING:
|
||||||
|
code = model.create_confirm_email_code(new_user)
|
||||||
|
send_confirmation_email(new_user.username, new_user.email, code.code)
|
||||||
|
return {
|
||||||
|
'awaiting_verification': True
|
||||||
|
}
|
||||||
|
else:
|
||||||
|
common_login(new_user)
|
||||||
|
return user_view(new_user)
|
||||||
|
|
||||||
except model.TooManyUsersException as ex:
|
except model.TooManyUsersException as ex:
|
||||||
raise license_error(exception=ex)
|
raise license_error(exception=ex)
|
||||||
except model.DataModelException as ex:
|
except model.DataModelException as ex:
|
||||||
|
@ -422,6 +456,7 @@ class DetachExternal(ApiResource):
|
||||||
|
|
||||||
|
|
||||||
@resource("/v1/recovery")
|
@resource("/v1/recovery")
|
||||||
|
@show_if(features.MAILING)
|
||||||
@internal_only
|
@internal_only
|
||||||
class Recovery(ApiResource):
|
class Recovery(ApiResource):
|
||||||
""" Resource for requesting a password recovery email. """
|
""" Resource for requesting a password recovery email. """
|
||||||
|
|
|
@ -26,7 +26,8 @@ def render_ologin_error(service_name,
|
||||||
error_message='Could not load user data. The token may have expired.'):
|
error_message='Could not load user data. The token may have expired.'):
|
||||||
return render_page_template('ologinerror.html', service_name=service_name,
|
return render_page_template('ologinerror.html', service_name=service_name,
|
||||||
error_message=error_message,
|
error_message=error_message,
|
||||||
service_url=get_app_url())
|
service_url=get_app_url(),
|
||||||
|
user_creation=features.USER_CREATION)
|
||||||
|
|
||||||
def exchange_code_for_token(code, service_name='GITHUB', for_login=True, form_encode=False,
|
def exchange_code_for_token(code, service_name='GITHUB', for_login=True, form_encode=False,
|
||||||
redirect_suffix=''):
|
redirect_suffix=''):
|
||||||
|
@ -85,7 +86,12 @@ def get_google_user(token):
|
||||||
def conduct_oauth_login(service_name, user_id, username, email, metadata={}):
|
def conduct_oauth_login(service_name, user_id, username, email, metadata={}):
|
||||||
to_login = model.verify_federated_login(service_name.lower(), user_id)
|
to_login = model.verify_federated_login(service_name.lower(), user_id)
|
||||||
if not to_login:
|
if not to_login:
|
||||||
# try to create the user
|
# See if we can create a new user.
|
||||||
|
if not features.USER_CREATION:
|
||||||
|
error_message = 'User creation is disabled. Please contact your administrator'
|
||||||
|
return render_ologin_error(service_name, error_message)
|
||||||
|
|
||||||
|
# Try to create the user
|
||||||
try:
|
try:
|
||||||
valid = next(generate_valid_usernames(username))
|
valid = next(generate_valid_usernames(username))
|
||||||
to_login = model.create_federated_user(valid, email, service_name.lower(),
|
to_login = model.create_federated_user(valid, email, service_name.lower(),
|
||||||
|
@ -147,7 +153,7 @@ def github_oauth_callback():
|
||||||
|
|
||||||
token = exchange_code_for_token(request.args.get('code'), service_name='GITHUB')
|
token = exchange_code_for_token(request.args.get('code'), service_name='GITHUB')
|
||||||
user_data = get_github_user(token)
|
user_data = get_github_user(token)
|
||||||
if not user_data:
|
if not user_data or not 'login' in user_data:
|
||||||
return render_ologin_error('GitHub')
|
return render_ologin_error('GitHub')
|
||||||
|
|
||||||
username = user_data['login']
|
username = user_data['login']
|
||||||
|
|
|
@ -82,20 +82,23 @@ def param_required(param_name):
|
||||||
|
|
||||||
|
|
||||||
@login_manager.user_loader
|
@login_manager.user_loader
|
||||||
def load_user(username):
|
def load_user(user_db_id):
|
||||||
logger.debug('User loader loading deferred user: %s' % username)
|
logger.debug('User loader loading deferred user id: %s' % user_db_id)
|
||||||
return _LoginWrappedDBUser(username)
|
try:
|
||||||
|
user_db_id_int = int(user_db_id)
|
||||||
|
return _LoginWrappedDBUser(user_db_id_int)
|
||||||
|
except ValueError:
|
||||||
|
return None
|
||||||
|
|
||||||
|
|
||||||
class _LoginWrappedDBUser(UserMixin):
|
class _LoginWrappedDBUser(UserMixin):
|
||||||
def __init__(self, db_username, db_user=None):
|
def __init__(self, user_db_id, db_user=None):
|
||||||
|
self._db_id = user_db_id
|
||||||
self._db_username = db_username
|
|
||||||
self._db_user = db_user
|
self._db_user = db_user
|
||||||
|
|
||||||
def db_user(self):
|
def db_user(self):
|
||||||
if not self._db_user:
|
if not self._db_user:
|
||||||
self._db_user = model.get_user(self._db_username)
|
self._db_user = model.get_user_by_id(self._db_id)
|
||||||
return self._db_user
|
return self._db_user
|
||||||
|
|
||||||
def is_authenticated(self):
|
def is_authenticated(self):
|
||||||
|
@ -105,13 +108,13 @@ class _LoginWrappedDBUser(UserMixin):
|
||||||
return self.db_user().verified
|
return self.db_user().verified
|
||||||
|
|
||||||
def get_id(self):
|
def get_id(self):
|
||||||
return unicode(self._db_username)
|
return unicode(self._db_id)
|
||||||
|
|
||||||
|
|
||||||
def common_login(db_user):
|
def common_login(db_user):
|
||||||
if login_user(_LoginWrappedDBUser(db_user.username, db_user)):
|
if login_user(_LoginWrappedDBUser(db_user.id, db_user)):
|
||||||
logger.debug('Successfully signed in as: %s' % db_user.username)
|
logger.debug('Successfully signed in as: %s' % db_user.username)
|
||||||
new_identity = QuayDeferredPermissionUser(db_user.username, 'username', {scopes.DIRECT_LOGIN})
|
new_identity = QuayDeferredPermissionUser(db_user.id, 'user_db_id', {scopes.DIRECT_LOGIN})
|
||||||
identity_changed.send(app, identity=new_identity)
|
identity_changed.send(app, identity=new_identity)
|
||||||
session['login_time'] = datetime.datetime.now()
|
session['login_time'] = datetime.datetime.now()
|
||||||
return True
|
return True
|
||||||
|
@ -202,7 +205,7 @@ def check_repository_usage(user_or_org, plan_found):
|
||||||
def start_build(repository, dockerfile_id, tags, build_name, subdir, manual,
|
def start_build(repository, dockerfile_id, tags, build_name, subdir, manual,
|
||||||
trigger=None, pull_robot_name=None):
|
trigger=None, pull_robot_name=None):
|
||||||
host = urlparse.urlparse(request.url).netloc
|
host = urlparse.urlparse(request.url).netloc
|
||||||
repo_path = '%s/%s/%s' % (host, repository.namespace, repository.name)
|
repo_path = '%s/%s/%s' % (host, repository.namespace_user.username, repository.name)
|
||||||
|
|
||||||
token = model.create_access_token(repository, 'write')
|
token = model.create_access_token(repository, 'write')
|
||||||
logger.debug('Creating build %s with repo %s tags %s and dockerfile_id %s',
|
logger.debug('Creating build %s with repo %s tags %s and dockerfile_id %s',
|
||||||
|
@ -218,9 +221,9 @@ def start_build(repository, dockerfile_id, tags, build_name, subdir, manual,
|
||||||
dockerfile_id, build_name,
|
dockerfile_id, build_name,
|
||||||
trigger, pull_robot_name=pull_robot_name)
|
trigger, pull_robot_name=pull_robot_name)
|
||||||
|
|
||||||
dockerfile_build_queue.put([repository.namespace, repository.name], json.dumps({
|
dockerfile_build_queue.put([repository.namespace_user.username, repository.name], json.dumps({
|
||||||
'build_uuid': build_request.uuid,
|
'build_uuid': build_request.uuid,
|
||||||
'namespace': repository.namespace,
|
'namespace': repository.namespace_user.username,
|
||||||
'repository': repository.name,
|
'repository': repository.name,
|
||||||
'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)
|
||||||
|
@ -228,7 +231,7 @@ def start_build(repository, dockerfile_id, tags, build_name, subdir, manual,
|
||||||
# Add the build to the repo's log.
|
# Add the build to the repo's log.
|
||||||
metadata = {
|
metadata = {
|
||||||
'repo': repository.name,
|
'repo': repository.name,
|
||||||
'namespace': repository.namespace,
|
'namespace': repository.namespace_user.username,
|
||||||
'fileid': dockerfile_id,
|
'fileid': dockerfile_id,
|
||||||
'manual': manual,
|
'manual': manual,
|
||||||
}
|
}
|
||||||
|
@ -238,9 +241,8 @@ def start_build(repository, dockerfile_id, tags, build_name, subdir, manual,
|
||||||
metadata['config'] = json.loads(trigger.config)
|
metadata['config'] = json.loads(trigger.config)
|
||||||
metadata['service'] = trigger.service.name
|
metadata['service'] = trigger.service.name
|
||||||
|
|
||||||
model.log_action('build_dockerfile', repository.namespace,
|
model.log_action('build_dockerfile', repository.namespace_user.username, ip=request.remote_addr,
|
||||||
ip=request.remote_addr, metadata=metadata,
|
metadata=metadata, repository=repository)
|
||||||
repository=repository)
|
|
||||||
|
|
||||||
# Add notifications for the build queue.
|
# Add notifications for the build queue.
|
||||||
profile.debug('Adding notifications for repository')
|
profile.debug('Adding notifications for repository')
|
||||||
|
|
|
@ -19,6 +19,7 @@ from auth.permissions import (ModifyRepositoryPermission, UserAdminPermission,
|
||||||
from util.http import abort
|
from util.http import abort
|
||||||
from endpoints.notificationhelper import spawn_notification
|
from endpoints.notificationhelper import spawn_notification
|
||||||
|
|
||||||
|
import features
|
||||||
|
|
||||||
logger = logging.getLogger(__name__)
|
logger = logging.getLogger(__name__)
|
||||||
profile = logging.getLogger('application.profiler')
|
profile = logging.getLogger('application.profiler')
|
||||||
|
@ -65,6 +66,9 @@ def generate_headers(role='read'):
|
||||||
@index.route('/users', methods=['POST'])
|
@index.route('/users', methods=['POST'])
|
||||||
@index.route('/users/', methods=['POST'])
|
@index.route('/users/', methods=['POST'])
|
||||||
def create_user():
|
def create_user():
|
||||||
|
if not features.USER_CREATION:
|
||||||
|
abort(400, 'User creation is disabled. Please speak to your administrator.')
|
||||||
|
|
||||||
user_data = request.get_json()
|
user_data = request.get_json()
|
||||||
if not 'username' in user_data:
|
if not 'username' in user_data:
|
||||||
abort(400, 'Missing username')
|
abort(400, 'Missing username')
|
||||||
|
@ -420,7 +424,7 @@ def put_repository_auth(namespace, repository):
|
||||||
def get_search():
|
def get_search():
|
||||||
def result_view(repo):
|
def result_view(repo):
|
||||||
return {
|
return {
|
||||||
"name": repo.namespace + '/' + repo.name,
|
"name": repo.namespace_user.username + '/' + repo.name,
|
||||||
"description": repo.description
|
"description": repo.description
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -438,7 +442,7 @@ def get_search():
|
||||||
|
|
||||||
results = [result_view(repo) for repo in matching
|
results = [result_view(repo) for repo in matching
|
||||||
if (repo.visibility.name == 'public' or
|
if (repo.visibility.name == 'public' or
|
||||||
ReadRepositoryPermission(repo.namespace, repo.name).can())]
|
ReadRepositoryPermission(repo.namespace_user.username, repo.name).can())]
|
||||||
|
|
||||||
data = {
|
data = {
|
||||||
"query": query,
|
"query": query,
|
||||||
|
@ -454,6 +458,7 @@ def get_search():
|
||||||
@index.route('/_ping')
|
@index.route('/_ping')
|
||||||
@index.route('/_ping')
|
@index.route('/_ping')
|
||||||
def ping():
|
def ping():
|
||||||
|
# NOTE: any changes made here must also be reflected in the nginx config
|
||||||
response = make_response('true', 200)
|
response = make_response('true', 200)
|
||||||
response.headers['X-Docker-Registry-Version'] = '0.6.0'
|
response.headers['X-Docker-Registry-Version'] = '0.6.0'
|
||||||
response.headers['X-Docker-Registry-Standalone'] = '0'
|
response.headers['X-Docker-Registry-Standalone'] = '0'
|
||||||
|
|
|
@ -1,8 +1,4 @@
|
||||||
import logging
|
import logging
|
||||||
import io
|
|
||||||
import os.path
|
|
||||||
import tarfile
|
|
||||||
import base64
|
|
||||||
|
|
||||||
from notificationhelper import build_event_data
|
from notificationhelper import build_event_data
|
||||||
|
|
||||||
|
|
|
@ -4,7 +4,7 @@ from data import model
|
||||||
import json
|
import json
|
||||||
|
|
||||||
def build_event_data(repo, extra_data={}, subpage=None):
|
def build_event_data(repo, extra_data={}, subpage=None):
|
||||||
repo_string = '%s/%s' % (repo.namespace, repo.name)
|
repo_string = '%s/%s' % (repo.namespace_user.username, repo.name)
|
||||||
homepage = '%s://%s/repository/%s' % (app.config['PREFERRED_URL_SCHEME'],
|
homepage = '%s://%s/repository/%s' % (app.config['PREFERRED_URL_SCHEME'],
|
||||||
app.config['SERVER_HOSTNAME'],
|
app.config['SERVER_HOSTNAME'],
|
||||||
repo_string)
|
repo_string)
|
||||||
|
@ -17,7 +17,7 @@ def build_event_data(repo, extra_data={}, subpage=None):
|
||||||
|
|
||||||
event_data = {
|
event_data = {
|
||||||
'repository': repo_string,
|
'repository': repo_string,
|
||||||
'namespace': repo.namespace,
|
'namespace': repo.namespace_user.username,
|
||||||
'name': repo.name,
|
'name': repo.name,
|
||||||
'docker_url': '%s/%s' % (app.config['SERVER_HOSTNAME'], repo_string),
|
'docker_url': '%s/%s' % (app.config['SERVER_HOSTNAME'], repo_string),
|
||||||
'homepage': homepage,
|
'homepage': homepage,
|
||||||
|
@ -30,7 +30,7 @@ def build_event_data(repo, extra_data={}, subpage=None):
|
||||||
def build_notification_data(notification, event_data):
|
def build_notification_data(notification, event_data):
|
||||||
return {
|
return {
|
||||||
'notification_uuid': notification.uuid,
|
'notification_uuid': notification.uuid,
|
||||||
'repository_namespace': notification.repository.namespace,
|
'repository_namespace': notification.repository.namespace_user.username,
|
||||||
'repository_name': notification.repository.name,
|
'repository_name': notification.repository.name,
|
||||||
'event_data': event_data
|
'event_data': event_data
|
||||||
}
|
}
|
||||||
|
@ -39,8 +39,9 @@ def build_notification_data(notification, event_data):
|
||||||
def spawn_notification(repo, event_name, extra_data={}, subpage=None, pathargs=[]):
|
def spawn_notification(repo, event_name, extra_data={}, subpage=None, pathargs=[]):
|
||||||
event_data = build_event_data(repo, extra_data=extra_data, subpage=subpage)
|
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)
|
notifications = model.list_repo_notifications(repo.namespace_user.username, repo.name,
|
||||||
|
event_name=event_name)
|
||||||
for notification in notifications:
|
for notification in notifications:
|
||||||
notification_data = build_notification_data(notification, event_data)
|
notification_data = build_notification_data(notification, event_data)
|
||||||
path = [repo.namespace, repo.name, event_name] + pathargs
|
path = [repo.namespace_user.username, repo.name, event_name] + pathargs
|
||||||
notification_queue.put(path, json.dumps(notification_data))
|
notification_queue.put(path, json.dumps(notification_data))
|
||||||
|
|
|
@ -10,6 +10,7 @@ import re
|
||||||
from flask.ext.mail import Message
|
from flask.ext.mail import Message
|
||||||
from app import mail, app, get_app_url
|
from app import mail, app, get_app_url
|
||||||
from data import model
|
from data import model
|
||||||
|
from workers.worker import JobException
|
||||||
|
|
||||||
logger = logging.getLogger(__name__)
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
@ -19,6 +20,9 @@ class InvalidNotificationMethodException(Exception):
|
||||||
class CannotValidateNotificationMethodException(Exception):
|
class CannotValidateNotificationMethodException(Exception):
|
||||||
pass
|
pass
|
||||||
|
|
||||||
|
class NotificationMethodPerformException(JobException):
|
||||||
|
pass
|
||||||
|
|
||||||
|
|
||||||
class NotificationMethod(object):
|
class NotificationMethod(object):
|
||||||
def __init__(self):
|
def __init__(self):
|
||||||
|
@ -84,7 +88,7 @@ class QuayNotificationMethod(NotificationMethod):
|
||||||
return (True, 'Unknown organization %s' % target_info['name'], None)
|
return (True, 'Unknown organization %s' % target_info['name'], None)
|
||||||
|
|
||||||
# Only repositories under the organization can cause notifications to that org.
|
# Only repositories under the organization can cause notifications to that org.
|
||||||
if target_info['name'] != repository.namespace:
|
if target_info['name'] != repository.namespace_user.username:
|
||||||
return (False, 'Organization name must match repository namespace')
|
return (False, 'Organization name must match repository namespace')
|
||||||
|
|
||||||
return (True, None, [target])
|
return (True, None, [target])
|
||||||
|
@ -92,7 +96,7 @@ class QuayNotificationMethod(NotificationMethod):
|
||||||
# Lookup the team.
|
# Lookup the team.
|
||||||
team = None
|
team = None
|
||||||
try:
|
try:
|
||||||
team = model.get_organization_team(repository.namespace, target_info['name'])
|
team = model.get_organization_team(repository.namespace_user.username, target_info['name'])
|
||||||
except model.InvalidTeamException:
|
except model.InvalidTeamException:
|
||||||
# Probably deleted.
|
# Probably deleted.
|
||||||
return (True, 'Unknown team %s' % target_info['name'], None)
|
return (True, 'Unknown team %s' % target_info['name'], None)
|
||||||
|
@ -105,19 +109,18 @@ class QuayNotificationMethod(NotificationMethod):
|
||||||
repository = notification.repository
|
repository = notification.repository
|
||||||
if not repository:
|
if not repository:
|
||||||
# Probably deleted.
|
# Probably deleted.
|
||||||
return True
|
return
|
||||||
|
|
||||||
# Lookup the target user or team to which we'll send the notification.
|
# Lookup the target user or team to which we'll send the notification.
|
||||||
config_data = json.loads(notification.config_json)
|
config_data = json.loads(notification.config_json)
|
||||||
status, err_message, target_users = self.find_targets(repository, config_data)
|
status, err_message, target_users = self.find_targets(repository, config_data)
|
||||||
if not status:
|
if not status:
|
||||||
return False
|
raise NotificationMethodPerformException(err_message)
|
||||||
|
|
||||||
# For each of the target users, create a notification.
|
# For each of the target users, create a notification.
|
||||||
for target_user in set(target_users or []):
|
for target_user in set(target_users or []):
|
||||||
model.create_notification(event_handler.event_name(), target_user,
|
model.create_notification(event_handler.event_name(), target_user,
|
||||||
metadata=notification_data['event_data'])
|
metadata=notification_data['event_data'])
|
||||||
return True
|
|
||||||
|
|
||||||
|
|
||||||
class EmailMethod(NotificationMethod):
|
class EmailMethod(NotificationMethod):
|
||||||
|
@ -130,7 +133,8 @@ class EmailMethod(NotificationMethod):
|
||||||
if not email:
|
if not email:
|
||||||
raise CannotValidateNotificationMethodException('Missing e-mail address')
|
raise CannotValidateNotificationMethodException('Missing e-mail address')
|
||||||
|
|
||||||
record = model.get_email_authorized_for_repo(repository.namespace, repository.name, email)
|
record = model.get_email_authorized_for_repo(repository.namespace_user.username,
|
||||||
|
repository.name, email)
|
||||||
if not record or not record.confirmed:
|
if not record or not record.confirmed:
|
||||||
raise CannotValidateNotificationMethodException('The specified e-mail address '
|
raise CannotValidateNotificationMethodException('The specified e-mail address '
|
||||||
'is not authorized to receive '
|
'is not authorized to receive '
|
||||||
|
@ -141,7 +145,7 @@ class EmailMethod(NotificationMethod):
|
||||||
config_data = json.loads(notification.config_json)
|
config_data = json.loads(notification.config_json)
|
||||||
email = config_data.get('email', '')
|
email = config_data.get('email', '')
|
||||||
if not email:
|
if not email:
|
||||||
return False
|
return
|
||||||
|
|
||||||
msg = Message(event_handler.get_summary(notification_data['event_data'], notification_data),
|
msg = Message(event_handler.get_summary(notification_data['event_data'], notification_data),
|
||||||
sender='support@quay.io',
|
sender='support@quay.io',
|
||||||
|
@ -153,9 +157,7 @@ class EmailMethod(NotificationMethod):
|
||||||
mail.send(msg)
|
mail.send(msg)
|
||||||
except Exception as ex:
|
except Exception as ex:
|
||||||
logger.exception('Email was unable to be sent: %s' % ex.message)
|
logger.exception('Email was unable to be sent: %s' % ex.message)
|
||||||
return False
|
raise NotificationMethodPerformException(ex.message)
|
||||||
|
|
||||||
return True
|
|
||||||
|
|
||||||
|
|
||||||
class WebhookMethod(NotificationMethod):
|
class WebhookMethod(NotificationMethod):
|
||||||
|
@ -172,7 +174,7 @@ class WebhookMethod(NotificationMethod):
|
||||||
config_data = json.loads(notification.config_json)
|
config_data = json.loads(notification.config_json)
|
||||||
url = config_data.get('url', '')
|
url = config_data.get('url', '')
|
||||||
if not url:
|
if not url:
|
||||||
return False
|
return
|
||||||
|
|
||||||
payload = notification_data['event_data']
|
payload = notification_data['event_data']
|
||||||
headers = {'Content-type': 'application/json'}
|
headers = {'Content-type': 'application/json'}
|
||||||
|
@ -180,15 +182,14 @@ class WebhookMethod(NotificationMethod):
|
||||||
try:
|
try:
|
||||||
resp = requests.post(url, data=json.dumps(payload), headers=headers)
|
resp = requests.post(url, data=json.dumps(payload), headers=headers)
|
||||||
if resp.status_code/100 != 2:
|
if resp.status_code/100 != 2:
|
||||||
logger.error('%s response for webhook to url: %s' % (resp.status_code,
|
error_message = '%s response for webhook to url: %s' % (resp.status_code, url)
|
||||||
url))
|
logger.error(error_message)
|
||||||
return False
|
logger.error(resp.content)
|
||||||
|
raise NotificationMethodPerformException(error_message)
|
||||||
|
|
||||||
except requests.exceptions.RequestException as ex:
|
except requests.exceptions.RequestException as ex:
|
||||||
logger.exception('Webhook was unable to be sent: %s' % ex.message)
|
logger.exception('Webhook was unable to be sent: %s' % ex.message)
|
||||||
return False
|
raise NotificationMethodPerformException(ex.message)
|
||||||
|
|
||||||
return True
|
|
||||||
|
|
||||||
|
|
||||||
class FlowdockMethod(NotificationMethod):
|
class FlowdockMethod(NotificationMethod):
|
||||||
|
@ -208,12 +209,12 @@ class FlowdockMethod(NotificationMethod):
|
||||||
config_data = json.loads(notification.config_json)
|
config_data = json.loads(notification.config_json)
|
||||||
token = config_data.get('flow_api_token', '')
|
token = config_data.get('flow_api_token', '')
|
||||||
if not token:
|
if not token:
|
||||||
return False
|
return
|
||||||
|
|
||||||
owner = model.get_user(notification.repository.namespace)
|
owner = model.get_user(notification.repository.namespace_user.username)
|
||||||
if not owner:
|
if not owner:
|
||||||
# Something went wrong.
|
# Something went wrong.
|
||||||
return False
|
return
|
||||||
|
|
||||||
url = 'https://api.flowdock.com/v1/messages/team_inbox/%s' % token
|
url = 'https://api.flowdock.com/v1/messages/team_inbox/%s' % token
|
||||||
headers = {'Content-type': 'application/json'}
|
headers = {'Content-type': 'application/json'}
|
||||||
|
@ -223,7 +224,8 @@ class FlowdockMethod(NotificationMethod):
|
||||||
'subject': event_handler.get_summary(notification_data['event_data'], notification_data),
|
'subject': event_handler.get_summary(notification_data['event_data'], notification_data),
|
||||||
'content': event_handler.get_message(notification_data['event_data'], notification_data),
|
'content': event_handler.get_message(notification_data['event_data'], notification_data),
|
||||||
'from_name': owner.username,
|
'from_name': owner.username,
|
||||||
'project': notification.repository.namespace + ' ' + notification.repository.name,
|
'project': (notification.repository.namespace_user.username + ' ' +
|
||||||
|
notification.repository.name),
|
||||||
'tags': ['#' + event_handler.event_name()],
|
'tags': ['#' + event_handler.event_name()],
|
||||||
'link': notification_data['event_data']['homepage']
|
'link': notification_data['event_data']['homepage']
|
||||||
}
|
}
|
||||||
|
@ -231,16 +233,14 @@ class FlowdockMethod(NotificationMethod):
|
||||||
try:
|
try:
|
||||||
resp = requests.post(url, data=json.dumps(payload), headers=headers)
|
resp = requests.post(url, data=json.dumps(payload), headers=headers)
|
||||||
if resp.status_code/100 != 2:
|
if resp.status_code/100 != 2:
|
||||||
logger.error('%s response for flowdock to url: %s' % (resp.status_code,
|
error_message = '%s response for flowdock to url: %s' % (resp.status_code, url)
|
||||||
url))
|
logger.error(error_message)
|
||||||
logger.error(resp.content)
|
logger.error(resp.content)
|
||||||
return False
|
raise NotificationMethodPerformException(error_message)
|
||||||
|
|
||||||
except requests.exceptions.RequestException as ex:
|
except requests.exceptions.RequestException as ex:
|
||||||
logger.exception('Flowdock method was unable to be sent: %s' % ex.message)
|
logger.exception('Flowdock method was unable to be sent: %s' % ex.message)
|
||||||
return False
|
raise NotificationMethodPerformException(ex.message)
|
||||||
|
|
||||||
return True
|
|
||||||
|
|
||||||
|
|
||||||
class HipchatMethod(NotificationMethod):
|
class HipchatMethod(NotificationMethod):
|
||||||
|
@ -265,12 +265,12 @@ class HipchatMethod(NotificationMethod):
|
||||||
room_id = config_data.get('room_id', '')
|
room_id = config_data.get('room_id', '')
|
||||||
|
|
||||||
if not token or not room_id:
|
if not token or not room_id:
|
||||||
return False
|
return
|
||||||
|
|
||||||
owner = model.get_user(notification.repository.namespace)
|
owner = model.get_user(notification.repository.namespace_user.username)
|
||||||
if not owner:
|
if not owner:
|
||||||
# Something went wrong.
|
# Something went wrong.
|
||||||
return False
|
return
|
||||||
|
|
||||||
url = 'https://api.hipchat.com/v2/room/%s/notification?auth_token=%s' % (room_id, token)
|
url = 'https://api.hipchat.com/v2/room/%s/notification?auth_token=%s' % (room_id, token)
|
||||||
|
|
||||||
|
@ -293,16 +293,14 @@ class HipchatMethod(NotificationMethod):
|
||||||
try:
|
try:
|
||||||
resp = requests.post(url, data=json.dumps(payload), headers=headers)
|
resp = requests.post(url, data=json.dumps(payload), headers=headers)
|
||||||
if resp.status_code/100 != 2:
|
if resp.status_code/100 != 2:
|
||||||
logger.error('%s response for hipchat to url: %s' % (resp.status_code,
|
error_message = '%s response for hipchat to url: %s' % (resp.status_code, url)
|
||||||
url))
|
logger.error(error_message)
|
||||||
logger.error(resp.content)
|
logger.error(resp.content)
|
||||||
return False
|
raise NotificationMethodPerformException(error_message)
|
||||||
|
|
||||||
except requests.exceptions.RequestException as ex:
|
except requests.exceptions.RequestException as ex:
|
||||||
logger.exception('Hipchat method was unable to be sent: %s' % ex.message)
|
logger.exception('Hipchat method was unable to be sent: %s' % ex.message)
|
||||||
return False
|
raise NotificationMethodPerformException(ex.message)
|
||||||
|
|
||||||
return True
|
|
||||||
|
|
||||||
|
|
||||||
class SlackMethod(NotificationMethod):
|
class SlackMethod(NotificationMethod):
|
||||||
|
@ -334,12 +332,12 @@ class SlackMethod(NotificationMethod):
|
||||||
subdomain = config_data.get('subdomain', '')
|
subdomain = config_data.get('subdomain', '')
|
||||||
|
|
||||||
if not token or not subdomain:
|
if not token or not subdomain:
|
||||||
return False
|
return
|
||||||
|
|
||||||
owner = model.get_user(notification.repository.namespace)
|
owner = model.get_user(notification.repository.namespace_user.username)
|
||||||
if not owner:
|
if not owner:
|
||||||
# Something went wrong.
|
# Something went wrong.
|
||||||
return False
|
return
|
||||||
|
|
||||||
url = 'https://%s.slack.com/services/hooks/incoming-webhook?token=%s' % (subdomain, token)
|
url = 'https://%s.slack.com/services/hooks/incoming-webhook?token=%s' % (subdomain, token)
|
||||||
|
|
||||||
|
@ -370,13 +368,11 @@ class SlackMethod(NotificationMethod):
|
||||||
try:
|
try:
|
||||||
resp = requests.post(url, data=json.dumps(payload), headers=headers)
|
resp = requests.post(url, data=json.dumps(payload), headers=headers)
|
||||||
if resp.status_code/100 != 2:
|
if resp.status_code/100 != 2:
|
||||||
logger.error('%s response for Slack to url: %s' % (resp.status_code,
|
error_message = '%s response for Slack to url: %s' % (resp.status_code, url)
|
||||||
url))
|
logger.error(error_message)
|
||||||
logger.error(resp.content)
|
logger.error(resp.content)
|
||||||
return False
|
raise NotificationMethodPerformException(error_message)
|
||||||
|
|
||||||
except requests.exceptions.RequestException as ex:
|
except requests.exceptions.RequestException as ex:
|
||||||
logger.exception('Slack method was unable to be sent: %s' % ex.message)
|
logger.exception('Slack method was unable to be sent: %s' % ex.message)
|
||||||
return False
|
raise NotificationMethodPerformException(ex.message)
|
||||||
|
|
||||||
return True
|
|
||||||
|
|
|
@ -14,6 +14,7 @@ from util.http import abort, exact_abort
|
||||||
from auth.permissions import (ReadRepositoryPermission,
|
from auth.permissions import (ReadRepositoryPermission,
|
||||||
ModifyRepositoryPermission)
|
ModifyRepositoryPermission)
|
||||||
from data import model
|
from data import model
|
||||||
|
from util import gzipstream
|
||||||
|
|
||||||
|
|
||||||
registry = Blueprint('registry', __name__)
|
registry = Blueprint('registry', __name__)
|
||||||
|
@ -193,21 +194,33 @@ def put_image_layer(namespace, repository, image_id):
|
||||||
# encoding (Gunicorn)
|
# encoding (Gunicorn)
|
||||||
input_stream = request.environ['wsgi.input']
|
input_stream = request.environ['wsgi.input']
|
||||||
|
|
||||||
# compute checksums
|
# Create a socket reader to read the input stream containing the layer data.
|
||||||
csums = []
|
|
||||||
sr = SocketReader(input_stream)
|
sr = SocketReader(input_stream)
|
||||||
|
|
||||||
|
# Add a handler that store the data in storage.
|
||||||
tmp, store_hndlr = store.temp_store_handler()
|
tmp, store_hndlr = store.temp_store_handler()
|
||||||
sr.add_handler(store_hndlr)
|
sr.add_handler(store_hndlr)
|
||||||
|
|
||||||
|
# Add a handler to compute the uncompressed size of the layer.
|
||||||
|
uncompressed_size_info, size_hndlr = gzipstream.calculate_size_handler()
|
||||||
|
sr.add_handler(size_hndlr)
|
||||||
|
|
||||||
|
# Add a handler which computes the checksum.
|
||||||
h, sum_hndlr = checksums.simple_checksum_handler(json_data)
|
h, sum_hndlr = checksums.simple_checksum_handler(json_data)
|
||||||
sr.add_handler(sum_hndlr)
|
sr.add_handler(sum_hndlr)
|
||||||
|
|
||||||
|
# Stream write the data to storage.
|
||||||
store.stream_write(repo_image.storage.locations, layer_path, sr)
|
store.stream_write(repo_image.storage.locations, layer_path, sr)
|
||||||
|
|
||||||
|
# Append the computed checksum.
|
||||||
|
csums = []
|
||||||
csums.append('sha256:{0}'.format(h.hexdigest()))
|
csums.append('sha256:{0}'.format(h.hexdigest()))
|
||||||
|
|
||||||
try:
|
try:
|
||||||
image_size = tmp.tell()
|
image_size = tmp.tell()
|
||||||
|
|
||||||
# Save the size of the image.
|
# Save the size of the image.
|
||||||
model.set_image_size(image_id, namespace, repository, image_size)
|
model.set_image_size(image_id, namespace, repository, image_size, uncompressed_size_info.size)
|
||||||
|
|
||||||
tmp.seek(0)
|
tmp.seek(0)
|
||||||
csums.append(checksums.compute_tarsum(tmp, json_data))
|
csums.append(checksums.compute_tarsum(tmp, json_data))
|
||||||
|
@ -451,12 +464,6 @@ def put_image_json(namespace, repository, image_id):
|
||||||
|
|
||||||
set_uploading_flag(repo_image, True)
|
set_uploading_flag(repo_image, True)
|
||||||
|
|
||||||
# We cleanup any old checksum in case it's a retry after a fail
|
|
||||||
profile.debug('Cleanup old checksum')
|
|
||||||
repo_image.storage.uncompressed_size = data.get('Size')
|
|
||||||
repo_image.storage.checksum = None
|
|
||||||
repo_image.storage.save()
|
|
||||||
|
|
||||||
# If we reach that point, it means that this is a new image or a retry
|
# If we reach that point, it means that this is a new image or a retry
|
||||||
# on a failed push
|
# on a failed push
|
||||||
# save the metadata
|
# save the metadata
|
||||||
|
|
|
@ -36,6 +36,9 @@ class TriggerActivationException(Exception):
|
||||||
class TriggerDeactivationException(Exception):
|
class TriggerDeactivationException(Exception):
|
||||||
pass
|
pass
|
||||||
|
|
||||||
|
class TriggerStartException(Exception):
|
||||||
|
pass
|
||||||
|
|
||||||
class ValidationRequestException(Exception):
|
class ValidationRequestException(Exception):
|
||||||
pass
|
pass
|
||||||
|
|
||||||
|
@ -109,12 +112,19 @@ class BuildTrigger(object):
|
||||||
"""
|
"""
|
||||||
raise NotImplementedError
|
raise NotImplementedError
|
||||||
|
|
||||||
def manual_start(self, auth_token, config):
|
def manual_start(self, auth_token, config, run_parameters = None):
|
||||||
"""
|
"""
|
||||||
Manually creates a repository build for this trigger.
|
Manually creates a repository build for this trigger.
|
||||||
"""
|
"""
|
||||||
raise NotImplementedError
|
raise NotImplementedError
|
||||||
|
|
||||||
|
def list_field_values(self, auth_token, config, field_name):
|
||||||
|
"""
|
||||||
|
Lists all values for the given custom trigger field. For example, a trigger might have a
|
||||||
|
field named "branches", and this method would return all branches.
|
||||||
|
"""
|
||||||
|
raise NotImplementedError
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def service_name(cls):
|
def service_name(cls):
|
||||||
"""
|
"""
|
||||||
|
@ -345,14 +355,37 @@ class GithubBuildTrigger(BuildTrigger):
|
||||||
return GithubBuildTrigger._prepare_build(config, repo, commit_sha,
|
return GithubBuildTrigger._prepare_build(config, repo, commit_sha,
|
||||||
short_sha, ref)
|
short_sha, ref)
|
||||||
|
|
||||||
def manual_start(self, auth_token, config):
|
def manual_start(self, auth_token, config, run_parameters = None):
|
||||||
source = config['build_source']
|
try:
|
||||||
|
source = config['build_source']
|
||||||
|
run_parameters = run_parameters or {}
|
||||||
|
|
||||||
gh_client = self._get_client(auth_token)
|
gh_client = self._get_client(auth_token)
|
||||||
repo = gh_client.get_repo(source)
|
repo = gh_client.get_repo(source)
|
||||||
master = repo.get_branch(repo.default_branch)
|
master = repo.get_branch(repo.default_branch)
|
||||||
master_sha = master.commit.sha
|
master_sha = master.commit.sha
|
||||||
short_sha = GithubBuildTrigger.get_display_name(master_sha)
|
short_sha = GithubBuildTrigger.get_display_name(master_sha)
|
||||||
ref = 'refs/heads/%s' % repo.default_branch
|
ref = 'refs/heads/%s' % (run_parameters.get('branch_name') or repo.default_branch)
|
||||||
|
|
||||||
return self._prepare_build(config, repo, master_sha, short_sha, ref)
|
return self._prepare_build(config, repo, master_sha, short_sha, ref)
|
||||||
|
except GithubException as ghe:
|
||||||
|
raise TriggerStartException(ghe.data['message'])
|
||||||
|
|
||||||
|
|
||||||
|
def list_field_values(self, auth_token, config, field_name):
|
||||||
|
if field_name == 'branch_name':
|
||||||
|
gh_client = self._get_client(auth_token)
|
||||||
|
source = config['build_source']
|
||||||
|
repo = gh_client.get_repo(source)
|
||||||
|
branches = [branch.name for branch in repo.get_branches()]
|
||||||
|
|
||||||
|
if not repo.default_branch in branches:
|
||||||
|
branches.insert(0, repo.default_branch)
|
||||||
|
|
||||||
|
if branches[0] != repo.default_branch:
|
||||||
|
branches.remove(repo.default_branch)
|
||||||
|
branches.insert(0, repo.default_branch)
|
||||||
|
|
||||||
|
return branches
|
||||||
|
|
||||||
|
return None
|
||||||
|
|
|
@ -18,6 +18,7 @@ from endpoints.common import common_login, render_page_template, route_show_if,
|
||||||
from endpoints.csrf import csrf_protect, generate_csrf_token
|
from endpoints.csrf import csrf_protect, generate_csrf_token
|
||||||
from util.names import parse_repository_name
|
from util.names import parse_repository_name
|
||||||
from util.gravatar import compute_hash
|
from util.gravatar import compute_hash
|
||||||
|
from util.useremails import send_email_changed
|
||||||
from auth import scopes
|
from auth import scopes
|
||||||
|
|
||||||
import features
|
import features
|
||||||
|
@ -32,8 +33,8 @@ STATUS_TAGS = app.config['STATUS_TAGS']
|
||||||
@web.route('/', methods=['GET'], defaults={'path': ''})
|
@web.route('/', methods=['GET'], defaults={'path': ''})
|
||||||
@web.route('/organization/<path:path>', methods=['GET'])
|
@web.route('/organization/<path:path>', methods=['GET'])
|
||||||
@no_cache
|
@no_cache
|
||||||
def index(path):
|
def index(path, **kwargs):
|
||||||
return render_page_template('index.html')
|
return render_page_template('index.html', **kwargs)
|
||||||
|
|
||||||
|
|
||||||
@web.route('/500', methods=['GET'])
|
@web.route('/500', methods=['GET'])
|
||||||
|
@ -101,7 +102,7 @@ def superuser():
|
||||||
|
|
||||||
@web.route('/signin/')
|
@web.route('/signin/')
|
||||||
@no_cache
|
@no_cache
|
||||||
def signin():
|
def signin(redirect=None):
|
||||||
return index('')
|
return index('')
|
||||||
|
|
||||||
|
|
||||||
|
@ -123,6 +124,13 @@ def new():
|
||||||
return index('')
|
return index('')
|
||||||
|
|
||||||
|
|
||||||
|
@web.route('/confirminvite')
|
||||||
|
@no_cache
|
||||||
|
def confirm_invite():
|
||||||
|
code = request.values['code']
|
||||||
|
return index('', code=code)
|
||||||
|
|
||||||
|
|
||||||
@web.route('/repository/', defaults={'path': ''})
|
@web.route('/repository/', defaults={'path': ''})
|
||||||
@web.route('/repository/<path:path>', methods=['GET'])
|
@web.route('/repository/<path:path>', methods=['GET'])
|
||||||
@no_cache
|
@no_cache
|
||||||
|
@ -215,6 +223,7 @@ def receipt():
|
||||||
|
|
||||||
|
|
||||||
@web.route('/authrepoemail', methods=['GET'])
|
@web.route('/authrepoemail', methods=['GET'])
|
||||||
|
@route_show_if(features.MAILING)
|
||||||
def confirm_repo_email():
|
def confirm_repo_email():
|
||||||
code = request.values['code']
|
code = request.values['code']
|
||||||
record = None
|
record = None
|
||||||
|
@ -228,23 +237,27 @@ def confirm_repo_email():
|
||||||
Your E-mail address has been authorized to receive notifications for repository
|
Your E-mail address has been authorized to receive notifications for repository
|
||||||
<a href="%s://%s/repository/%s/%s">%s/%s</a>.
|
<a href="%s://%s/repository/%s/%s">%s/%s</a>.
|
||||||
""" % (app.config['PREFERRED_URL_SCHEME'], app.config['SERVER_HOSTNAME'],
|
""" % (app.config['PREFERRED_URL_SCHEME'], app.config['SERVER_HOSTNAME'],
|
||||||
record.repository.namespace, record.repository.name,
|
record.repository.namespace_user.username, record.repository.name,
|
||||||
record.repository.namespace, record.repository.name)
|
record.repository.namespace_user.username, record.repository.name)
|
||||||
|
|
||||||
return render_page_template('message.html', message=message)
|
return render_page_template('message.html', message=message)
|
||||||
|
|
||||||
|
|
||||||
@web.route('/confirm', methods=['GET'])
|
@web.route('/confirm', methods=['GET'])
|
||||||
|
@route_show_if(features.MAILING)
|
||||||
def confirm_email():
|
def confirm_email():
|
||||||
code = request.values['code']
|
code = request.values['code']
|
||||||
user = None
|
user = None
|
||||||
new_email = None
|
new_email = None
|
||||||
|
|
||||||
try:
|
try:
|
||||||
user, new_email = model.confirm_user_email(code)
|
user, new_email, old_email = model.confirm_user_email(code)
|
||||||
except model.DataModelException as ex:
|
except model.DataModelException as ex:
|
||||||
return render_page_template('confirmerror.html', error_message=ex.message)
|
return render_page_template('confirmerror.html', error_message=ex.message)
|
||||||
|
|
||||||
|
if new_email:
|
||||||
|
send_email_changed(user.username, old_email, new_email)
|
||||||
|
|
||||||
common_login(user)
|
common_login(user)
|
||||||
|
|
||||||
return redirect(url_for('web.user', tab='email')
|
return redirect(url_for('web.user', tab='email')
|
||||||
|
|
55
initdb.py
55
initdb.py
|
@ -51,7 +51,7 @@ def __gen_checksum(image_id):
|
||||||
|
|
||||||
|
|
||||||
def __gen_image_id(repo, image_num):
|
def __gen_image_id(repo, image_num):
|
||||||
str_to_hash = "%s/%s/%s" % (repo.namespace, repo.name, image_num)
|
str_to_hash = "%s/%s/%s" % (repo.namespace_user.username, repo.name, image_num)
|
||||||
|
|
||||||
h = hashlib.md5(str_to_hash)
|
h = hashlib.md5(str_to_hash)
|
||||||
return h.hexdigest() + h.hexdigest()
|
return h.hexdigest() + h.hexdigest()
|
||||||
|
@ -79,12 +79,12 @@ def __create_subtree(repo, structure, creator_username, parent):
|
||||||
creation_time = REFERENCE_DATE + timedelta(days=image_num)
|
creation_time = REFERENCE_DATE + timedelta(days=image_num)
|
||||||
command_list = SAMPLE_CMDS[image_num % len(SAMPLE_CMDS)]
|
command_list = SAMPLE_CMDS[image_num % len(SAMPLE_CMDS)]
|
||||||
command = json.dumps(command_list) if command_list else None
|
command = json.dumps(command_list) if command_list else None
|
||||||
new_image = model.set_image_metadata(docker_image_id, repo.namespace,
|
new_image = model.set_image_metadata(docker_image_id, repo.namespace_user.username, repo.name,
|
||||||
repo.name, str(creation_time),
|
str(creation_time), 'no comment', command, parent)
|
||||||
'no comment', command, parent)
|
|
||||||
|
|
||||||
model.set_image_size(docker_image_id, repo.namespace, repo.name,
|
compressed_size = random.randrange(1, 1024 * 1024 * 1024)
|
||||||
random.randrange(1, 1024 * 1024 * 1024))
|
model.set_image_size(docker_image_id, repo.namespace_user.username, repo.name, compressed_size,
|
||||||
|
int(compressed_size * 1.4))
|
||||||
|
|
||||||
# Populate the diff file
|
# Populate the diff file
|
||||||
diff_path = store.image_file_diffs_path(new_image.storage.uuid)
|
diff_path = store.image_file_diffs_path(new_image.storage.uuid)
|
||||||
|
@ -100,7 +100,7 @@ def __create_subtree(repo, structure, creator_username, parent):
|
||||||
last_node_tags = [last_node_tags]
|
last_node_tags = [last_node_tags]
|
||||||
|
|
||||||
for tag_name in last_node_tags:
|
for tag_name in last_node_tags:
|
||||||
model.create_or_update_tag(repo.namespace, repo.name, tag_name,
|
model.create_or_update_tag(repo.namespace_user.username, repo.name, tag_name,
|
||||||
new_image.docker_image_id)
|
new_image.docker_image_id)
|
||||||
|
|
||||||
for subtree in subtrees:
|
for subtree in subtrees:
|
||||||
|
@ -214,7 +214,11 @@ def initialize_database():
|
||||||
|
|
||||||
LogEntryKind.create(name='org_create_team')
|
LogEntryKind.create(name='org_create_team')
|
||||||
LogEntryKind.create(name='org_delete_team')
|
LogEntryKind.create(name='org_delete_team')
|
||||||
|
LogEntryKind.create(name='org_invite_team_member')
|
||||||
|
LogEntryKind.create(name='org_delete_team_member_invite')
|
||||||
LogEntryKind.create(name='org_add_team_member')
|
LogEntryKind.create(name='org_add_team_member')
|
||||||
|
LogEntryKind.create(name='org_team_member_invite_accepted')
|
||||||
|
LogEntryKind.create(name='org_team_member_invite_declined')
|
||||||
LogEntryKind.create(name='org_remove_team_member')
|
LogEntryKind.create(name='org_remove_team_member')
|
||||||
LogEntryKind.create(name='org_set_team_description')
|
LogEntryKind.create(name='org_set_team_description')
|
||||||
LogEntryKind.create(name='org_set_team_role')
|
LogEntryKind.create(name='org_set_team_role')
|
||||||
|
@ -271,6 +275,7 @@ def initialize_database():
|
||||||
NotificationKind.create(name='over_private_usage')
|
NotificationKind.create(name='over_private_usage')
|
||||||
NotificationKind.create(name='expiring_license')
|
NotificationKind.create(name='expiring_license')
|
||||||
NotificationKind.create(name='maintenance')
|
NotificationKind.create(name='maintenance')
|
||||||
|
NotificationKind.create(name='org_team_invite')
|
||||||
|
|
||||||
NotificationKind.create(name='test_notification')
|
NotificationKind.create(name='test_notification')
|
||||||
|
|
||||||
|
@ -302,7 +307,7 @@ def populate_database():
|
||||||
new_user_2.verified = True
|
new_user_2.verified = True
|
||||||
new_user_2.save()
|
new_user_2.save()
|
||||||
|
|
||||||
new_user_3 = model.create_user('freshuser', 'password', 'no@thanks.com')
|
new_user_3 = model.create_user('freshuser', 'password', 'jschorr+test@devtable.com')
|
||||||
new_user_3.verified = True
|
new_user_3.verified = True
|
||||||
new_user_3.save()
|
new_user_3.save()
|
||||||
|
|
||||||
|
@ -323,7 +328,8 @@ def populate_database():
|
||||||
outside_org.verified = True
|
outside_org.verified = True
|
||||||
outside_org.save()
|
outside_org.save()
|
||||||
|
|
||||||
model.create_notification('test_notification', new_user_1, metadata={'some': 'value', 'arr': [1,2,3], 'obj': {'a': 1, 'b': 2}})
|
model.create_notification('test_notification', new_user_1,
|
||||||
|
metadata={'some':'value', 'arr':[1, 2, 3], 'obj':{'a':1, 'b':2}})
|
||||||
|
|
||||||
from_date = datetime.utcnow()
|
from_date = datetime.utcnow()
|
||||||
to_date = from_date + timedelta(hours=1)
|
to_date = from_date + timedelta(hours=1)
|
||||||
|
@ -387,18 +393,20 @@ def populate_database():
|
||||||
})
|
})
|
||||||
trigger.save()
|
trigger.save()
|
||||||
|
|
||||||
repo = 'ci.devtable.com:5000/%s/%s' % (building.namespace, building.name)
|
repo = 'ci.devtable.com:5000/%s/%s' % (building.namespace_user.username, building.name)
|
||||||
job_config = {
|
job_config = {
|
||||||
'repository': repo,
|
'repository': repo,
|
||||||
'docker_tags': ['latest'],
|
'docker_tags': ['latest'],
|
||||||
'build_subdir': '',
|
'build_subdir': '',
|
||||||
}
|
}
|
||||||
|
|
||||||
record = model.create_email_authorization_for_repo(new_user_1.username, 'simple', 'jschorr@devtable.com')
|
record = model.create_email_authorization_for_repo(new_user_1.username, 'simple',
|
||||||
|
'jschorr@devtable.com')
|
||||||
record.confirmed = True
|
record.confirmed = True
|
||||||
record.save()
|
record.save()
|
||||||
|
|
||||||
model.create_email_authorization_for_repo(new_user_1.username, 'simple', 'jschorr+other@devtable.com')
|
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',
|
||||||
|
@ -425,12 +433,12 @@ def populate_database():
|
||||||
|
|
||||||
model.create_robot('coolrobot', org)
|
model.create_robot('coolrobot', org)
|
||||||
|
|
||||||
oauth.create_application(org, 'Some Test App', 'http://localhost:8000', 'http://localhost:8000/o2c.html',
|
oauth.create_application(org, 'Some Test App', 'http://localhost:8000',
|
||||||
client_id='deadbeef')
|
'http://localhost:8000/o2c.html', client_id='deadbeef')
|
||||||
|
|
||||||
oauth.create_application(org, 'Some Other Test App', 'http://quay.io', 'http://localhost:8000/o2c.html',
|
oauth.create_application(org, 'Some Other Test App', 'http://quay.io',
|
||||||
client_id='deadpork',
|
'http://localhost:8000/o2c.html', client_id='deadpork',
|
||||||
description = 'This is another test application')
|
description='This is another test application')
|
||||||
|
|
||||||
model.oauth.create_access_token_for_testing(new_user_1, 'deadbeef', 'repo:admin')
|
model.oauth.create_access_token_for_testing(new_user_1, 'deadbeef', 'repo:admin')
|
||||||
|
|
||||||
|
@ -452,8 +460,8 @@ def populate_database():
|
||||||
|
|
||||||
reader_team = model.create_team('readers', org, 'member',
|
reader_team = model.create_team('readers', org, 'member',
|
||||||
'Readers of orgrepo.')
|
'Readers of orgrepo.')
|
||||||
model.set_team_repo_permission(reader_team.name, org_repo.namespace,
|
model.set_team_repo_permission(reader_team.name, org_repo.namespace_user.username, org_repo.name,
|
||||||
org_repo.name, 'read')
|
'read')
|
||||||
model.add_user_to_team(new_user_2, reader_team)
|
model.add_user_to_team(new_user_2, reader_team)
|
||||||
model.add_user_to_team(reader, reader_team)
|
model.add_user_to_team(reader, reader_team)
|
||||||
|
|
||||||
|
@ -475,12 +483,9 @@ def populate_database():
|
||||||
(2, [], 'latest17'),
|
(2, [], 'latest17'),
|
||||||
(2, [], 'latest18'),])
|
(2, [], 'latest18'),])
|
||||||
|
|
||||||
model.add_prototype_permission(org, 'read', activating_user=new_user_1,
|
model.add_prototype_permission(org, 'read', activating_user=new_user_1, delegate_user=new_user_2)
|
||||||
delegate_user=new_user_2)
|
model.add_prototype_permission(org, 'read', activating_user=new_user_1, delegate_team=reader_team)
|
||||||
model.add_prototype_permission(org, 'read', activating_user=new_user_1,
|
model.add_prototype_permission(org, 'write', activating_user=new_user_2, delegate_user=new_user_1)
|
||||||
delegate_team=reader_team)
|
|
||||||
model.add_prototype_permission(org, 'write', activating_user=new_user_2,
|
|
||||||
delegate_user=new_user_1)
|
|
||||||
|
|
||||||
today = datetime.today()
|
today = datetime.today()
|
||||||
week_ago = today - timedelta(6)
|
week_ago = today - timedelta(6)
|
||||||
|
|
|
@ -144,6 +144,15 @@ nav.navbar-default .navbar-nav>li>a.active {
|
||||||
max-width: 320px;
|
max-width: 320px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.notification-view-element .right-controls button {
|
||||||
|
margin-left: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.notification-view-element .message i.fa {
|
||||||
|
margin-right: 6px;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
.notification-view-element .orginfo {
|
.notification-view-element .orginfo {
|
||||||
margin-top: 8px;
|
margin-top: 8px;
|
||||||
float: left;
|
float: left;
|
||||||
|
@ -3593,6 +3602,12 @@ p.editable:hover i {
|
||||||
white-space: nowrap;
|
white-space: nowrap;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.tt-message {
|
||||||
|
padding: 10px;
|
||||||
|
font-size: 12px;
|
||||||
|
white-space: nowrap;
|
||||||
|
}
|
||||||
|
|
||||||
.tt-suggestion p {
|
.tt-suggestion p {
|
||||||
margin: 0;
|
margin: 0;
|
||||||
}
|
}
|
||||||
|
@ -4284,7 +4299,7 @@ pre.command:before {
|
||||||
}
|
}
|
||||||
|
|
||||||
.user-row.super-user td {
|
.user-row.super-user td {
|
||||||
background-color: #d9edf7;
|
background-color: #eeeeee;
|
||||||
}
|
}
|
||||||
|
|
||||||
.user-row .user-class {
|
.user-row .user-class {
|
||||||
|
@ -4672,4 +4687,68 @@ i.slack-icon {
|
||||||
|
|
||||||
.external-notification-view-element:hover .side-controls button {
|
.external-notification-view-element:hover .side-controls button {
|
||||||
border: 1px solid #eee;
|
border: 1px solid #eee;
|
||||||
|
}
|
||||||
|
|
||||||
|
.member-listing {
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.member-listing .section-header {
|
||||||
|
color: #ccc;
|
||||||
|
margin-top: 20px;
|
||||||
|
margin-bottom: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.member-listing .gravatar {
|
||||||
|
vertical-align: middle;
|
||||||
|
margin-right: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.member-listing .entity-reference {
|
||||||
|
margin-bottom: 10px;
|
||||||
|
display: inline-block;
|
||||||
|
}
|
||||||
|
|
||||||
|
.member-listing .invite-listing {
|
||||||
|
margin-bottom: 10px;
|
||||||
|
display: inline-block;
|
||||||
|
}
|
||||||
|
|
||||||
|
.team-view .organization-header .popover {
|
||||||
|
max-width: none !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.team-view .organization-header .popover.bottom-right .arrow:after {
|
||||||
|
border-bottom-color: #f7f7f7;
|
||||||
|
top: 2px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.team-view .organization-header .popover-content {
|
||||||
|
font-size: 14px;
|
||||||
|
padding-top: 6px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.team-view .organization-header .popover-content input {
|
||||||
|
background: white;
|
||||||
|
}
|
||||||
|
|
||||||
|
.team-view .team-view-add-element .help-text {
|
||||||
|
font-size: 13px;
|
||||||
|
color: #ccc;
|
||||||
|
margin-top: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.team-view .organization-header .popover-content {
|
||||||
|
min-width: 500px;
|
||||||
|
}
|
||||||
|
|
||||||
|
#startTriggerDialog .trigger-description {
|
||||||
|
margin-bottom: 20px;
|
||||||
|
padding-bottom: 20px;
|
||||||
|
border-bottom: 1px solid #eee;
|
||||||
|
}
|
||||||
|
|
||||||
|
#startTriggerDialog #runForm .field-title {
|
||||||
|
width: 120px;
|
||||||
|
padding-right: 10px;
|
||||||
}
|
}
|
|
@ -7,15 +7,19 @@
|
||||||
</span>
|
</span>
|
||||||
</span>
|
</span>
|
||||||
<span ng-if="entity.kind == 'org'">
|
<span ng-if="entity.kind == 'org'">
|
||||||
<img src="//www.gravatar.com/avatar/{{ entity.gravatar }}?s=16&d=identicon">
|
<img ng-src="//www.gravatar.com/avatar/{{ entity.gravatar }}?s={{ gravatarSize || '16' }}&d=identicon">
|
||||||
<span class="entity-name">
|
<span class="entity-name">
|
||||||
<span ng-if="!getIsAdmin(entity.name)">{{entity.name}}</span>
|
<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 ng-if="getIsAdmin(entity.name)"><a href="/organization/{{ entity.name }}">{{entity.name}}</a></span>
|
||||||
</span>
|
</span>
|
||||||
</span>
|
</span>
|
||||||
<span ng-if="entity.kind != 'team' && entity.kind != 'org'">
|
<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>
|
<img class="gravatar" ng-if="showGravatar == 'true' && entity.gravatar" ng-src="//www.gravatar.com/avatar/{{ entity.gravatar }}?s={{ gravatarSize || '16' }}&d=identicon">
|
||||||
<i class="fa fa-wrench" ng-show="entity.is_robot" data-title="Robot Account" bs-tooltip="tooltip.title" data-container="body"></i>
|
<span ng-if="showGravatar != 'true' || !entity.gravatar">
|
||||||
|
<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>
|
||||||
|
</span>
|
||||||
|
|
||||||
<span class="entity-name" ng-if="entity.is_robot">
|
<span class="entity-name" ng-if="entity.is_robot">
|
||||||
<a href="{{ getRobotUrl(entity.name) }}" ng-if="getIsAdmin(getPrefix(entity.name))">
|
<a href="{{ getRobotUrl(entity.name) }}" ng-if="getIsAdmin(getPrefix(entity.name))">
|
||||||
<span class="prefix">{{ getPrefix(entity.name) }}+</span><span>{{ getShortenedName(entity.name) }}</span>
|
<span class="prefix">{{ getPrefix(entity.name) }}+</span><span>{{ getShortenedName(entity.name) }}</span>
|
||||||
|
|
|
@ -5,7 +5,7 @@
|
||||||
ng-click="lazyLoad()">
|
ng-click="lazyLoad()">
|
||||||
<span class="caret"></span>
|
<span class="caret"></span>
|
||||||
</button>
|
</button>
|
||||||
<ul class="dropdown-menu" role="menu" aria-labelledby="entityDropdownMenu">
|
<ul class="dropdown-menu" ng-class="pullRight == 'true' ? 'pull-right': ''" role="menu" aria-labelledby="entityDropdownMenu">
|
||||||
<li ng-show="lazyLoading" style="padding: 10px"><div class="quay-spinner"></div></li>
|
<li ng-show="lazyLoading" style="padding: 10px"><div class="quay-spinner"></div></li>
|
||||||
|
|
||||||
<li role="presentation" class="dropdown-header" ng-show="!lazyLoading && !robots && !isAdmin && !teams">
|
<li role="presentation" class="dropdown-header" ng-show="!lazyLoading && !robots && !isAdmin && !teams">
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
<span class="external-login-button-element">
|
<span class="external-login-button-element">
|
||||||
<span ng-if="provider == 'github'">
|
<span ng-if="provider == 'github'">
|
||||||
<a href="javascript:void(0)" class="btn btn-primary btn-block" quay-require="['GITHUB_LOGIN']" ng-click="startSignin('github')" style="margin-bottom: 10px">
|
<a href="javascript:void(0)" class="btn btn-primary btn-block" quay-require="['GITHUB_LOGIN']" ng-click="startSignin('github')" style="margin-bottom: 10px" ng-disabled="signingIn">
|
||||||
<i class="fa fa-github fa-lg"></i>
|
<i class="fa fa-github fa-lg"></i>
|
||||||
<span ng-if="action != 'attach'">Sign In with GitHub</span>
|
<span ng-if="action != 'attach'">Sign In with GitHub</span>
|
||||||
<span ng-if="action == 'attach'">Attach to GitHub Account</span>
|
<span ng-if="action == 'attach'">Attach to GitHub Account</span>
|
||||||
|
@ -8,7 +8,7 @@
|
||||||
</span>
|
</span>
|
||||||
|
|
||||||
<span ng-if="provider == 'google'">
|
<span ng-if="provider == 'google'">
|
||||||
<a href="javascript:void(0)" class="btn btn-primary btn-block" quay-require="['GOOGLE_LOGIN']" ng-click="startSignin('google')">
|
<a href="javascript:void(0)" class="btn btn-primary btn-block" quay-require="['GOOGLE_LOGIN']" ng-click="startSignin('google')" ng-disabled="signingIn">
|
||||||
<i class="fa fa-google fa-lg"></i>
|
<i class="fa fa-google fa-lg"></i>
|
||||||
<span ng-if="action != 'attach'">Sign In with Google</span>
|
<span ng-if="action != 'attach'">Sign In with Google</span>
|
||||||
<span ng-if="action == 'attach'">Attach to Google Account</span>
|
<span ng-if="action == 'attach'">Attach to Google Account</span>
|
||||||
|
|
|
@ -3,7 +3,7 @@
|
||||||
<div class="container header">
|
<div class="container header">
|
||||||
<span class="header-text">
|
<span class="header-text">
|
||||||
<span ng-show="!performer">Usage Logs</span>
|
<span ng-show="!performer">Usage Logs</span>
|
||||||
<span class="entity-reference" name="performer.username" isrobot="performer.is_robot" ng-show="performer"></span>
|
<span class="entity-reference" entity="performer" ng-show="performer"></span>
|
||||||
<span id="logs-range" class="mini">
|
<span id="logs-range" class="mini">
|
||||||
From
|
From
|
||||||
<input type="text" class="logs-date-picker input-sm" name="start" ng-model="logStartDate" data-max-date="{{ logEndDate }}" data-container="body" bs-datepicker/>
|
<input type="text" class="logs-date-picker input-sm" name="start" ng-model="logStartDate" data-max-date="{{ logEndDate }}" data-container="body" bs-datepicker/>
|
||||||
|
|
38
static/directives/manual-trigger-build-dialog.html
Normal file
38
static/directives/manual-trigger-build-dialog.html
Normal file
|
@ -0,0 +1,38 @@
|
||||||
|
<!-- Modal message dialog -->
|
||||||
|
<div class="modal fade" id="startTriggerDialog">
|
||||||
|
<div class="modal-dialog">
|
||||||
|
<div class="modal-content">
|
||||||
|
<div class="modal-header">
|
||||||
|
<button type="button" class="close" data-dismiss="modal" aria-hidden="true">×</button>
|
||||||
|
<h4 class="modal-title">Manully Start Build Trigger</h4>
|
||||||
|
</div>
|
||||||
|
<div class="modal-body">
|
||||||
|
<div class="trigger-description" trigger="trigger"></div>
|
||||||
|
|
||||||
|
<form name="runForm" id="runForm">
|
||||||
|
<table width="100%">
|
||||||
|
<tr ng-repeat="field in runParameters">
|
||||||
|
<td class="field-title" valign="top">{{ field.title }}:</td>
|
||||||
|
<td>
|
||||||
|
<div ng-switch on="field.type">
|
||||||
|
<span ng-switch-when="option">
|
||||||
|
<span class="quay-spinner" ng-show="!fieldOptions[field.name]"></span>
|
||||||
|
<select ng-model="parameters[field.name]" ng-show="fieldOptions[field.name]"
|
||||||
|
ng-options="value for value in fieldOptions[field.name]"
|
||||||
|
required>
|
||||||
|
</select>
|
||||||
|
</span>
|
||||||
|
<input type="text" class="form-control" ng-model="parameters[field.name]" ng-switch-when="string" required>
|
||||||
|
</div>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
</table>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
<div class="modal-footer">
|
||||||
|
<button type="button" class="btn btn-primary" ng-disabled="runForm.$invalid" ng-click="startTrigger()">Start Build</button>
|
||||||
|
<button type="button" class="btn btn-default" data-dismiss="modal">Cancel</button>
|
||||||
|
</div>
|
||||||
|
</div><!-- /.modal-content -->
|
||||||
|
</div><!-- /.modal-dialog -->
|
||||||
|
</div><!-- /.modal -->
|
|
@ -7,10 +7,13 @@
|
||||||
<span class="orgname">{{ notification.organization }}</span>
|
<span class="orgname">{{ notification.organization }}</span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="datetime">{{ parseDate(notification.created) | date:'medium'}}</div>
|
|
||||||
<div class="right-controls">
|
<div class="right-controls">
|
||||||
<a href="javascript:void(0)" ng-if="canDismiss(notification)" ng-click="dismissNotification(notification)">
|
<a href="javascript:void(0)" ng-if="canDismiss(notification)" ng-click="dismissNotification(notification)">
|
||||||
Dismiss Notification
|
Dismiss Notification
|
||||||
</a>
|
</a>
|
||||||
|
<button class="btn" ng-class="'btn-' + action.kind" ng-repeat="action in getActions(notification)" ng-click="action.handler(notification)">
|
||||||
|
{{ action.title }}
|
||||||
|
</button>
|
||||||
</div>
|
</div>
|
||||||
|
<div class="datetime">{{ parseDate(notification.created) | date:'medium'}}</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
@ -3,7 +3,7 @@
|
||||||
|
|
||||||
<div class="container" ng-show="!loading">
|
<div class="container" ng-show="!loading">
|
||||||
<div class="alert alert-info">
|
<div class="alert alert-info">
|
||||||
Default permissions provide a means of specifying <span class="context-tooltip" data-title="By default, all repositories have the creating user added as an 'Admin'" bs-tooltip="tooltip.title">additional</span> permissions that should be granted automatically to a repository.
|
Default permissions provide a means of specifying <span class="context-tooltip" data-title="By default, all repositories have the creating user added as an 'Admin'" bs-tooltip="tooltip.title">additional</span> permissions that should be granted automatically to a repository <strong>when it is created</strong>.
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="side-controls">
|
<div class="side-controls">
|
||||||
|
|
|
@ -1,5 +1,6 @@
|
||||||
<div class="signin-form-element">
|
<div class="signin-form-element">
|
||||||
<form class="form-signin" ng-submit="signin();">
|
<span class="quay-spinner" ng-show="signingIn"></span>
|
||||||
|
<form class="form-signin" ng-submit="signin();" ng-show="!signingIn">
|
||||||
<input type="text" class="form-control input-lg" name="username"
|
<input type="text" class="form-control input-lg" name="username"
|
||||||
placeholder="Username or E-mail Address" ng-model="user.username" autofocus>
|
placeholder="Username or E-mail Address" ng-model="user.username" autofocus>
|
||||||
<input type="password" class="form-control input-lg" name="password"
|
<input type="password" class="form-control input-lg" name="password"
|
||||||
|
|
|
@ -1,5 +1,5 @@
|
||||||
<div class="signup-form-element">
|
<div class="signup-form-element" quay-show="Features.USER_CREATION">
|
||||||
<form class="form-signup" name="signupForm" ng-submit="register()" ngshow="!awaitingConfirmation && !registering">
|
<form class="form-signup" name="signupForm" ng-submit="register()" ng-show="!awaitingConfirmation && !registering">
|
||||||
<input type="text" class="form-control" placeholder="Create a username" name="username" ng-model="newUser.username" autofocus required ng-pattern="/^[a-z0-9_]{4,30}$/">
|
<input type="text" class="form-control" placeholder="Create a username" name="username" ng-model="newUser.username" autofocus required ng-pattern="/^[a-z0-9_]{4,30}$/">
|
||||||
<input type="email" class="form-control" placeholder="Email address" ng-model="newUser.email" required>
|
<input type="email" class="form-control" placeholder="Email address" ng-model="newUser.email" required>
|
||||||
<input type="password" class="form-control" placeholder="Create a password" ng-model="newUser.password" required
|
<input type="password" class="form-control" placeholder="Create a password" ng-model="newUser.password" required
|
||||||
|
|
17
static/directives/team-view-add.html
Normal file
17
static/directives/team-view-add.html
Normal file
|
@ -0,0 +1,17 @@
|
||||||
|
<div class="team-view-add-element" focusable-popover-content>
|
||||||
|
<div class="entity-search"
|
||||||
|
namespace="orgname" placeholder="'Add a registered user or robot...'"
|
||||||
|
entity-selected="addNewMember(entity)"
|
||||||
|
email-selected="inviteEmail(email)"
|
||||||
|
current-entity="selectedMember"
|
||||||
|
auto-clear="true"
|
||||||
|
allowed-entities="['user', 'robot']"
|
||||||
|
pull-right="true"
|
||||||
|
allow-emails="allowEmail"
|
||||||
|
email-message="Press enter to invite the entered e-mail address to this team"
|
||||||
|
ng-show="!addingMember"></div>
|
||||||
|
<div class="quay-spinner" ng-show="addingMember"></div>
|
||||||
|
<div class="help-text" ng-show="!addingMember">
|
||||||
|
Search by Quay.io username or robot account name
|
||||||
|
</div>
|
||||||
|
</div>
|
|
@ -14,7 +14,7 @@
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="panel panel-default">
|
<div class="panel panel-default" quay-show="Features.USER_CREATION">
|
||||||
<div class="panel-heading">
|
<div class="panel-heading">
|
||||||
<h6 class="panel-title accordion-title">
|
<h6 class="panel-title accordion-title">
|
||||||
<a class="accordion-toggle" data-toggle="collapse" data-parent="#accordion" data-target="#collapseRegister">
|
<a class="accordion-toggle" data-toggle="collapse" data-parent="#accordion" data-target="#collapseRegister">
|
||||||
|
@ -24,11 +24,11 @@
|
||||||
</div>
|
</div>
|
||||||
<div id="collapseRegister" class="panel-collapse collapse" ng-class="hasSignedIn() ? 'out' : 'in'">
|
<div id="collapseRegister" class="panel-collapse collapse" ng-class="hasSignedIn() ? 'out' : 'in'">
|
||||||
<div class="panel-body">
|
<div class="panel-body">
|
||||||
<div class="signup-form"></div>
|
<div class="signup-form" user-registered="handleUserRegistered(username)" invite-code="inviteCode"></div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="panel panel-default">
|
<div class="panel panel-default" quay-show="Features.MAILING">
|
||||||
<div class="panel-heading">
|
<div class="panel-heading">
|
||||||
<h6 class="panel-title accordion-title">
|
<h6 class="panel-title accordion-title">
|
||||||
<a class="accordion-toggle" data-toggle="collapse" data-parent="#accordion" data-target="#collapseForgot">
|
<a class="accordion-toggle" data-toggle="collapse" data-parent="#accordion" data-target="#collapseForgot">
|
||||||
|
@ -37,7 +37,8 @@
|
||||||
</h6>
|
</h6>
|
||||||
</div>
|
</div>
|
||||||
<div id="collapseForgot" class="panel-collapse collapse out">
|
<div id="collapseForgot" class="panel-collapse collapse out">
|
||||||
<div class="panel-body">
|
<div class="quay-spinner" ng-show="sendingRecovery"></div>
|
||||||
|
<div class="panel-body" ng-show="!sendingRecovery">
|
||||||
<form class="form-signin" ng-submit="sendRecovery();">
|
<form class="form-signin" ng-submit="sendRecovery();">
|
||||||
<input type="text" class="form-control input-lg" placeholder="Email" ng-model="recovery.email">
|
<input type="text" class="form-control input-lg" placeholder="Email" ng-model="recovery.email">
|
||||||
<button class="btn btn-lg btn-primary btn-block" type="submit">Send Recovery Email</button>
|
<button class="btn btn-lg btn-primary btn-block" type="submit">Send Recovery Email</button>
|
||||||
|
|
380
static/js/app.js
380
static/js/app.js
|
@ -499,6 +499,11 @@ quayApp = angular.module('quay', quayDependencies, function($provide, cfpLoading
|
||||||
$provide.factory('UtilService', ['$sanitize', function($sanitize) {
|
$provide.factory('UtilService', ['$sanitize', function($sanitize) {
|
||||||
var utilService = {};
|
var utilService = {};
|
||||||
|
|
||||||
|
utilService.isEmailAddress = function(val) {
|
||||||
|
var emailRegex = /^[a-zA-Z0-9.!#$%&’*+/=?^_`{|}~-]+@[a-zA-Z0-9-]+(?:\.[a-zA-Z0-9-]+)*$/;
|
||||||
|
return emailRegex.test(val);
|
||||||
|
};
|
||||||
|
|
||||||
utilService.escapeHtmlString = function(text) {
|
utilService.escapeHtmlString = function(text) {
|
||||||
var adjusted = text.replace(/&/g, "&")
|
var adjusted = text.replace(/&/g, "&")
|
||||||
.replace(/</g, "<")
|
.replace(/</g, "<")
|
||||||
|
@ -615,24 +620,46 @@ quayApp = angular.module('quay', quayDependencies, function($provide, cfpLoading
|
||||||
}]);
|
}]);
|
||||||
|
|
||||||
|
|
||||||
$provide.factory('TriggerDescriptionBuilder', ['UtilService', '$sanitize', function(UtilService, $sanitize) {
|
$provide.factory('TriggerService', ['UtilService', '$sanitize', function(UtilService, $sanitize) {
|
||||||
var builderService = {};
|
var triggerService = {};
|
||||||
|
|
||||||
builderService.getDescription = function(name, config) {
|
var triggerTypes = {
|
||||||
switch (name) {
|
'github': {
|
||||||
case 'github':
|
'description': function(config) {
|
||||||
var source = UtilService.textToSafeHtml(config['build_source']);
|
var source = UtilService.textToSafeHtml(config['build_source']);
|
||||||
var desc = '<i class="fa fa-github fa-lg" style="margin-left: 2px; margin-right: 2px"></i> Push to Github Repository ';
|
var desc = '<i class="fa fa-github fa-lg" style="margin-left: 2px; margin-right: 2px"></i> Push to Github Repository ';
|
||||||
desc += '<a href="https://github.com/' + source + '" target="_blank">' + source + '</a>';
|
desc += '<a href="https://github.com/' + source + '" target="_blank">' + source + '</a>';
|
||||||
desc += '<br>Dockerfile folder: //' + UtilService.textToSafeHtml(config['subdir']);
|
desc += '<br>Dockerfile folder: //' + UtilService.textToSafeHtml(config['subdir']);
|
||||||
return desc;
|
return desc;
|
||||||
|
},
|
||||||
|
|
||||||
default:
|
'run_parameters': [
|
||||||
return 'Unknown';
|
{
|
||||||
|
'title': 'Branch',
|
||||||
|
'type': 'option',
|
||||||
|
'name': 'branch_name'
|
||||||
|
}
|
||||||
|
]
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
triggerService.getDescription = function(name, config) {
|
||||||
|
var type = triggerTypes[name];
|
||||||
|
if (!type) {
|
||||||
|
return 'Unknown';
|
||||||
|
}
|
||||||
|
return type['description'](config);
|
||||||
};
|
};
|
||||||
|
|
||||||
return builderService;
|
triggerService.getRunParameters = function(name, config) {
|
||||||
|
var type = triggerTypes[name];
|
||||||
|
if (!type) {
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
return type['run_parameters'];
|
||||||
|
}
|
||||||
|
|
||||||
|
return triggerService;
|
||||||
}]);
|
}]);
|
||||||
|
|
||||||
$provide.factory('StringBuilderService', ['$sce', 'UtilService', function($sce, UtilService) {
|
$provide.factory('StringBuilderService', ['$sce', 'UtilService', function($sce, UtilService) {
|
||||||
|
@ -675,7 +702,10 @@ quayApp = angular.module('quay', quayDependencies, function($provide, cfpLoading
|
||||||
|
|
||||||
stringBuilderService.buildString = function(value_or_func, metadata) {
|
stringBuilderService.buildString = function(value_or_func, metadata) {
|
||||||
var fieldIcons = {
|
var fieldIcons = {
|
||||||
|
'inviter': 'user',
|
||||||
'username': 'user',
|
'username': 'user',
|
||||||
|
'user': 'user',
|
||||||
|
'email': 'envelope',
|
||||||
'activating_username': 'user',
|
'activating_username': 'user',
|
||||||
'delegate_user': 'user',
|
'delegate_user': 'user',
|
||||||
'delegate_team': 'group',
|
'delegate_team': 'group',
|
||||||
|
@ -885,6 +915,8 @@ quayApp = angular.module('quay', quayDependencies, function($provide, cfpLoading
|
||||||
// We already have /api/v1/ on the URLs, so remove them from the paths.
|
// We already have /api/v1/ on the URLs, so remove them from the paths.
|
||||||
path = path.substr('/api/v1/'.length, path.length);
|
path = path.substr('/api/v1/'.length, path.length);
|
||||||
|
|
||||||
|
// Build the path, adjusted with the inline parameters.
|
||||||
|
var used = {};
|
||||||
var url = '';
|
var url = '';
|
||||||
for (var i = 0; i < path.length; ++i) {
|
for (var i = 0; i < path.length; ++i) {
|
||||||
var c = path[i];
|
var c = path[i];
|
||||||
|
@ -896,6 +928,7 @@ quayApp = angular.module('quay', quayDependencies, function($provide, cfpLoading
|
||||||
throw new Error('Missing parameter: ' + varName);
|
throw new Error('Missing parameter: ' + varName);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
used[varName] = true;
|
||||||
url += parameters[varName];
|
url += parameters[varName];
|
||||||
i = end;
|
i = end;
|
||||||
continue;
|
continue;
|
||||||
|
@ -904,6 +937,20 @@ quayApp = angular.module('quay', quayDependencies, function($provide, cfpLoading
|
||||||
url += c;
|
url += c;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Append any query parameters.
|
||||||
|
var isFirst = true;
|
||||||
|
for (var paramName in parameters) {
|
||||||
|
if (!parameters.hasOwnProperty(paramName)) { continue; }
|
||||||
|
if (used[paramName]) { continue; }
|
||||||
|
|
||||||
|
var value = parameters[paramName];
|
||||||
|
if (value) {
|
||||||
|
url += isFirst ? '?' : '&';
|
||||||
|
url += paramName + '=' + encodeURIComponent(value)
|
||||||
|
isFirst = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
return url;
|
return url;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@ -1257,7 +1304,7 @@ quayApp = angular.module('quay', quayDependencies, function($provide, cfpLoading
|
||||||
return userService;
|
return userService;
|
||||||
}]);
|
}]);
|
||||||
|
|
||||||
$provide.factory('ExternalNotificationData', ['Config', function(Config) {
|
$provide.factory('ExternalNotificationData', ['Config', 'Features', function(Config, Features) {
|
||||||
var externalNotificationData = {};
|
var externalNotificationData = {};
|
||||||
|
|
||||||
var events = [
|
var events = [
|
||||||
|
@ -1311,7 +1358,8 @@ quayApp = angular.module('quay', quayDependencies, function($provide, cfpLoading
|
||||||
'type': 'email',
|
'type': 'email',
|
||||||
'title': 'E-mail address'
|
'title': 'E-mail address'
|
||||||
}
|
}
|
||||||
]
|
],
|
||||||
|
'enabled': Features.MAILING
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
'id': 'webhook',
|
'id': 'webhook',
|
||||||
|
@ -1351,7 +1399,8 @@ quayApp = angular.module('quay', quayDependencies, function($provide, cfpLoading
|
||||||
{
|
{
|
||||||
'name': 'notification_token',
|
'name': 'notification_token',
|
||||||
'type': 'string',
|
'type': 'string',
|
||||||
'title': 'Notification Token'
|
'title': 'Room Notification Token',
|
||||||
|
'help_url': 'https://hipchat.com/rooms/tokens/{room_id}'
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
|
@ -1391,7 +1440,13 @@ quayApp = angular.module('quay', quayDependencies, function($provide, cfpLoading
|
||||||
};
|
};
|
||||||
|
|
||||||
externalNotificationData.getSupportedMethods = function() {
|
externalNotificationData.getSupportedMethods = function() {
|
||||||
return methods;
|
var filtered = [];
|
||||||
|
for (var i = 0; i < methods.length; ++i) {
|
||||||
|
if (methods[i].enabled !== false) {
|
||||||
|
filtered.push(methods[i]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return filtered;
|
||||||
};
|
};
|
||||||
|
|
||||||
externalNotificationData.getEventInfo = function(event) {
|
externalNotificationData.getEventInfo = function(event) {
|
||||||
|
@ -1405,8 +1460,8 @@ quayApp = angular.module('quay', quayDependencies, function($provide, cfpLoading
|
||||||
return externalNotificationData;
|
return externalNotificationData;
|
||||||
}]);
|
}]);
|
||||||
|
|
||||||
$provide.factory('NotificationService', ['$rootScope', '$interval', 'UserService', 'ApiService', 'StringBuilderService', 'PlanService', 'UserService', 'Config',
|
$provide.factory('NotificationService', ['$rootScope', '$interval', 'UserService', 'ApiService', 'StringBuilderService', 'PlanService', 'UserService', 'Config', '$location',
|
||||||
function($rootScope, $interval, UserService, ApiService, StringBuilderService, PlanService, UserService, Config) {
|
function($rootScope, $interval, UserService, ApiService, StringBuilderService, PlanService, UserService, Config, $location) {
|
||||||
var notificationService = {
|
var notificationService = {
|
||||||
'user': null,
|
'user': null,
|
||||||
'notifications': [],
|
'notifications': [],
|
||||||
|
@ -1424,6 +1479,28 @@ quayApp = angular.module('quay', quayDependencies, function($provide, cfpLoading
|
||||||
'page': '/about/',
|
'page': '/about/',
|
||||||
'dismissable': true
|
'dismissable': true
|
||||||
},
|
},
|
||||||
|
'org_team_invite': {
|
||||||
|
'level': 'primary',
|
||||||
|
'message': '{inviter} is inviting you to join team {team} under organization {org}',
|
||||||
|
'actions': [
|
||||||
|
{
|
||||||
|
'title': 'Join team',
|
||||||
|
'kind': 'primary',
|
||||||
|
'handler': function(notification) {
|
||||||
|
window.location = '/confirminvite?code=' + notification.metadata['code'];
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
'title': 'Decline',
|
||||||
|
'kind': 'default',
|
||||||
|
'handler': function(notification) {
|
||||||
|
ApiService.declineOrganizationTeamInvite(null, {'code': notification.metadata['code']}).then(function() {
|
||||||
|
notificationService.update();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
'password_required': {
|
'password_required': {
|
||||||
'level': 'error',
|
'level': 'error',
|
||||||
'message': 'In order to begin pushing and pulling repositories, a password must be set for your account',
|
'message': 'In order to begin pushing and pulling repositories, a password must be set for your account',
|
||||||
|
@ -1518,6 +1595,15 @@ quayApp = angular.module('quay', quayDependencies, function($provide, cfpLoading
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
notificationService.getActions = function(notification) {
|
||||||
|
var kindInfo = notificationKinds[notification['kind']];
|
||||||
|
if (!kindInfo) {
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
|
||||||
|
return kindInfo['actions'] || [];
|
||||||
|
};
|
||||||
|
|
||||||
notificationService.canDismiss = function(notification) {
|
notificationService.canDismiss = function(notification) {
|
||||||
var kindInfo = notificationKinds[notification['kind']];
|
var kindInfo = notificationKinds[notification['kind']];
|
||||||
if (!kindInfo) {
|
if (!kindInfo) {
|
||||||
|
@ -1533,10 +1619,10 @@ quayApp = angular.module('quay', quayDependencies, function($provide, cfpLoading
|
||||||
}
|
}
|
||||||
|
|
||||||
var page = kindInfo['page'];
|
var page = kindInfo['page'];
|
||||||
if (typeof page != 'string') {
|
if (page != null && typeof page != 'string') {
|
||||||
page = page(notification['metadata']);
|
page = page(notification['metadata']);
|
||||||
}
|
}
|
||||||
return page;
|
return page || '';
|
||||||
};
|
};
|
||||||
|
|
||||||
notificationService.getMessage = function(notification) {
|
notificationService.getMessage = function(notification) {
|
||||||
|
@ -2058,7 +2144,7 @@ quayApp = angular.module('quay', quayDependencies, function($provide, cfpLoading
|
||||||
templateUrl: '/static/partials/plans.html', controller: PlansCtrl}).
|
templateUrl: '/static/partials/plans.html', controller: PlansCtrl}).
|
||||||
when('/security/', {title: 'Security', description: 'Security features used when transmitting and storing data',
|
when('/security/', {title: 'Security', description: 'Security features used when transmitting and storing data',
|
||||||
templateUrl: '/static/partials/security.html'}).
|
templateUrl: '/static/partials/security.html'}).
|
||||||
when('/signin/', {title: 'Sign In', description: 'Sign into ' + title, templateUrl: '/static/partials/signin.html'}).
|
when('/signin/', {title: 'Sign In', description: 'Sign into ' + title, templateUrl: '/static/partials/signin.html', controller: SignInCtrl, reloadOnSearch: false}).
|
||||||
when('/new/', {title: 'Create new repository', description: 'Create a new public or private docker repository, optionally constructing from a dockerfile',
|
when('/new/', {title: 'Create new repository', description: 'Create a new public or private docker repository, optionally constructing from a dockerfile',
|
||||||
templateUrl: '/static/partials/new-repo.html', controller: NewRepoCtrl}).
|
templateUrl: '/static/partials/new-repo.html', controller: NewRepoCtrl}).
|
||||||
when('/organizations/', {title: 'Organizations', description: 'Private docker repository hosting for businesses and organizations',
|
when('/organizations/', {title: 'Organizations', description: 'Private docker repository hosting for businesses and organizations',
|
||||||
|
@ -2079,6 +2165,8 @@ quayApp = angular.module('quay', quayDependencies, function($provide, cfpLoading
|
||||||
when('/tour/features', {title: title + ' Features', templateUrl: '/static/partials/tour.html', controller: TourCtrl}).
|
when('/tour/features', {title: title + ' Features', templateUrl: '/static/partials/tour.html', controller: TourCtrl}).
|
||||||
when('/tour/enterprise', {title: 'Enterprise Edition', templateUrl: '/static/partials/tour.html', controller: TourCtrl}).
|
when('/tour/enterprise', {title: 'Enterprise Edition', templateUrl: '/static/partials/tour.html', controller: TourCtrl}).
|
||||||
|
|
||||||
|
when('/confirminvite', {title: 'Confirm Invite', templateUrl: '/static/partials/confirm-invite.html', controller: ConfirmInviteCtrl, reloadOnSearch: false}).
|
||||||
|
|
||||||
when('/', {title: 'Hosted Private Docker Registry', templateUrl: '/static/partials/landing.html', controller: LandingCtrl,
|
when('/', {title: 'Hosted Private Docker Registry', templateUrl: '/static/partials/landing.html', controller: LandingCtrl,
|
||||||
pageClass: 'landing-page'}).
|
pageClass: 'landing-page'}).
|
||||||
otherwise({redirectTo: '/'});
|
otherwise({redirectTo: '/'});
|
||||||
|
@ -2167,6 +2255,19 @@ quayApp.directive('quayShow', function($animate, Features, Config) {
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
||||||
|
quayApp.directive('ngIfMedia', function ($animate) {
|
||||||
|
return {
|
||||||
|
transclude: 'element',
|
||||||
|
priority: 600,
|
||||||
|
terminal: true,
|
||||||
|
restrict: 'A',
|
||||||
|
link: buildConditionalLinker($animate, 'ngIfMedia', function(value) {
|
||||||
|
return window.matchMedia(value).matches;
|
||||||
|
})
|
||||||
|
};
|
||||||
|
});
|
||||||
|
|
||||||
|
|
||||||
quayApp.directive('quaySection', function($animate, $location, $rootScope) {
|
quayApp.directive('quaySection', function($animate, $location, $rootScope) {
|
||||||
return {
|
return {
|
||||||
priority: 590,
|
priority: 590,
|
||||||
|
@ -2300,7 +2401,9 @@ quayApp.directive('entityReference', function () {
|
||||||
restrict: 'C',
|
restrict: 'C',
|
||||||
scope: {
|
scope: {
|
||||||
'entity': '=entity',
|
'entity': '=entity',
|
||||||
'namespace': '=namespace'
|
'namespace': '=namespace',
|
||||||
|
'showGravatar': '@showGravatar',
|
||||||
|
'gravatarSize': '@gravatarSize'
|
||||||
},
|
},
|
||||||
controller: function($scope, $element, UserService, UtilService) {
|
controller: function($scope, $element, UserService, UtilService) {
|
||||||
$scope.getIsAdmin = function(namespace) {
|
$scope.getIsAdmin = function(namespace) {
|
||||||
|
@ -2437,6 +2540,36 @@ quayApp.directive('repoBreadcrumb', function () {
|
||||||
return directiveDefinitionObject;
|
return directiveDefinitionObject;
|
||||||
});
|
});
|
||||||
|
|
||||||
|
quayApp.directive('focusablePopoverContent', ['$timeout', '$popover', function ($timeout, $popover) {
|
||||||
|
return {
|
||||||
|
restrict: "A",
|
||||||
|
link: function (scope, element, attrs) {
|
||||||
|
$body = $('body');
|
||||||
|
var hide = function() {
|
||||||
|
$body.off('click');
|
||||||
|
scope.$apply(function() {
|
||||||
|
scope.$hide();
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
scope.$on('$destroy', function() {
|
||||||
|
$body.off('click');
|
||||||
|
});
|
||||||
|
|
||||||
|
$timeout(function() {
|
||||||
|
$body.on('click', function(evt) {
|
||||||
|
var target = evt.target;
|
||||||
|
var isPanelMember = $(element).has(target).length > 0 || target == element;
|
||||||
|
if (!isPanelMember) {
|
||||||
|
hide();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
$(element).find('input').focus();
|
||||||
|
}, 100);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}]);
|
||||||
|
|
||||||
quayApp.directive('repoCircle', function () {
|
quayApp.directive('repoCircle', function () {
|
||||||
var directiveDefinitionObject = {
|
var directiveDefinitionObject = {
|
||||||
|
@ -2495,22 +2628,34 @@ quayApp.directive('userSetup', function () {
|
||||||
restrict: 'C',
|
restrict: 'C',
|
||||||
scope: {
|
scope: {
|
||||||
'redirectUrl': '=redirectUrl',
|
'redirectUrl': '=redirectUrl',
|
||||||
|
|
||||||
|
'inviteCode': '=inviteCode',
|
||||||
|
|
||||||
'signInStarted': '&signInStarted',
|
'signInStarted': '&signInStarted',
|
||||||
'signedIn': '&signedIn'
|
'signedIn': '&signedIn',
|
||||||
|
'userRegistered': '&userRegistered'
|
||||||
},
|
},
|
||||||
controller: function($scope, $location, $timeout, ApiService, KeyService, UserService) {
|
controller: function($scope, $location, $timeout, ApiService, KeyService, UserService) {
|
||||||
$scope.sendRecovery = function() {
|
$scope.sendRecovery = function() {
|
||||||
|
$scope.sendingRecovery = true;
|
||||||
|
|
||||||
ApiService.requestRecoveryEmail($scope.recovery).then(function() {
|
ApiService.requestRecoveryEmail($scope.recovery).then(function() {
|
||||||
$scope.invalidRecovery = false;
|
$scope.invalidRecovery = false;
|
||||||
$scope.errorMessage = '';
|
$scope.errorMessage = '';
|
||||||
$scope.sent = true;
|
$scope.sent = true;
|
||||||
|
$scope.sendingRecovery = false;
|
||||||
}, function(result) {
|
}, function(result) {
|
||||||
$scope.invalidRecovery = true;
|
$scope.invalidRecovery = true;
|
||||||
$scope.errorMessage = result.data;
|
$scope.errorMessage = result.data;
|
||||||
$scope.sent = false;
|
$scope.sent = false;
|
||||||
|
$scope.sendingRecovery = false;
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
|
$scope.handleUserRegistered = function(username) {
|
||||||
|
$scope.userRegistered({'username': username});
|
||||||
|
};
|
||||||
|
|
||||||
$scope.hasSignedIn = function() {
|
$scope.hasSignedIn = function() {
|
||||||
return UserService.hasEverLoggedIn();
|
return UserService.hasEverLoggedIn();
|
||||||
};
|
};
|
||||||
|
@ -2534,6 +2679,7 @@ quayApp.directive('externalLoginButton', function () {
|
||||||
'action': '@action'
|
'action': '@action'
|
||||||
},
|
},
|
||||||
controller: function($scope, $timeout, $interval, ApiService, KeyService, CookieService, Features, Config) {
|
controller: function($scope, $timeout, $interval, ApiService, KeyService, CookieService, Features, Config) {
|
||||||
|
$scope.signingIn = false;
|
||||||
$scope.startSignin = function(service) {
|
$scope.startSignin = function(service) {
|
||||||
$scope.signInStarted({'service': service});
|
$scope.signInStarted({'service': service});
|
||||||
|
|
||||||
|
@ -2545,6 +2691,7 @@ quayApp.directive('externalLoginButton', function () {
|
||||||
|
|
||||||
// Needed to ensure that UI work done by the started callback is finished before the location
|
// Needed to ensure that UI work done by the started callback is finished before the location
|
||||||
// changes.
|
// changes.
|
||||||
|
$scope.signingIn = true;
|
||||||
$timeout(function() {
|
$timeout(function() {
|
||||||
document.location = url;
|
document.location = url;
|
||||||
}, 250);
|
}, 250);
|
||||||
|
@ -2570,8 +2717,10 @@ quayApp.directive('signinForm', function () {
|
||||||
controller: function($scope, $location, $timeout, $interval, ApiService, KeyService, UserService, CookieService, Features, Config) {
|
controller: function($scope, $location, $timeout, $interval, ApiService, KeyService, UserService, CookieService, Features, Config) {
|
||||||
$scope.tryAgainSoon = 0;
|
$scope.tryAgainSoon = 0;
|
||||||
$scope.tryAgainInterval = null;
|
$scope.tryAgainInterval = null;
|
||||||
|
$scope.signingIn = false;
|
||||||
|
|
||||||
$scope.markStarted = function() {
|
$scope.markStarted = function() {
|
||||||
|
$scope.signingIn = true;
|
||||||
if ($scope.signInStarted != null) {
|
if ($scope.signInStarted != null) {
|
||||||
$scope.signInStarted();
|
$scope.signInStarted();
|
||||||
}
|
}
|
||||||
|
@ -2602,25 +2751,30 @@ quayApp.directive('signinForm', function () {
|
||||||
$scope.cancelInterval();
|
$scope.cancelInterval();
|
||||||
|
|
||||||
ApiService.signinUser($scope.user).then(function() {
|
ApiService.signinUser($scope.user).then(function() {
|
||||||
|
$scope.signingIn = false;
|
||||||
$scope.needsEmailVerification = false;
|
$scope.needsEmailVerification = false;
|
||||||
$scope.invalidCredentials = false;
|
$scope.invalidCredentials = false;
|
||||||
|
|
||||||
if ($scope.signedIn != null) {
|
if ($scope.signedIn != null) {
|
||||||
$scope.signedIn();
|
$scope.signedIn();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Load the newly created user.
|
||||||
UserService.load();
|
UserService.load();
|
||||||
|
|
||||||
// Redirect to the specified page or the landing page
|
// Redirect to the specified page or the landing page
|
||||||
// Note: The timeout of 500ms is needed to ensure dialogs containing sign in
|
// Note: The timeout of 500ms is needed to ensure dialogs containing sign in
|
||||||
// forms get removed before the location changes.
|
// forms get removed before the location changes.
|
||||||
$timeout(function() {
|
$timeout(function() {
|
||||||
if ($scope.redirectUrl == $location.path()) {
|
var redirectUrl = $scope.redirectUrl;
|
||||||
return;
|
if (redirectUrl == $location.path() || redirectUrl == null) {
|
||||||
}
|
return;
|
||||||
$location.path($scope.redirectUrl ? $scope.redirectUrl : '/');
|
}
|
||||||
|
window.location = (redirectUrl ? redirectUrl : '/');
|
||||||
}, 500);
|
}, 500);
|
||||||
}, function(result) {
|
}, function(result) {
|
||||||
|
$scope.signingIn = false;
|
||||||
|
|
||||||
if (result.status == 429 /* try again later */) {
|
if (result.status == 429 /* try again later */) {
|
||||||
$scope.needsEmailVerification = false;
|
$scope.needsEmailVerification = false;
|
||||||
$scope.invalidCredentials = false;
|
$scope.invalidCredentials = false;
|
||||||
|
@ -2654,25 +2808,37 @@ quayApp.directive('signupForm', function () {
|
||||||
transclude: true,
|
transclude: true,
|
||||||
restrict: 'C',
|
restrict: 'C',
|
||||||
scope: {
|
scope: {
|
||||||
|
'inviteCode': '=inviteCode',
|
||||||
|
|
||||||
|
'userRegistered': '&userRegistered'
|
||||||
},
|
},
|
||||||
controller: function($scope, $location, $timeout, ApiService, KeyService, UserService, Config, UIService) {
|
controller: function($scope, $location, $timeout, ApiService, KeyService, UserService, Config, UIService) {
|
||||||
$('.form-signup').popover();
|
$('.form-signup').popover();
|
||||||
|
|
||||||
$scope.awaitingConfirmation = false;
|
$scope.awaitingConfirmation = false;
|
||||||
$scope.registering = false;
|
$scope.registering = false;
|
||||||
|
|
||||||
$scope.register = function() {
|
$scope.register = function() {
|
||||||
UIService.hidePopover('#signupButton');
|
UIService.hidePopover('#signupButton');
|
||||||
$scope.registering = true;
|
$scope.registering = true;
|
||||||
|
|
||||||
ApiService.createNewUser($scope.newUser).then(function() {
|
if ($scope.inviteCode) {
|
||||||
|
$scope.newUser['invite_code'] = $scope.inviteCode;
|
||||||
|
}
|
||||||
|
|
||||||
|
ApiService.createNewUser($scope.newUser).then(function(resp) {
|
||||||
$scope.registering = false;
|
$scope.registering = false;
|
||||||
$scope.awaitingConfirmation = true;
|
$scope.awaitingConfirmation = !!resp['awaiting_verification'];
|
||||||
|
|
||||||
if (Config.MIXPANEL_KEY) {
|
if (Config.MIXPANEL_KEY) {
|
||||||
mixpanel.alias($scope.newUser.username);
|
mixpanel.alias($scope.newUser.username);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
$scope.userRegistered({'username': $scope.newUser.username});
|
||||||
|
|
||||||
|
if (!$scope.awaitingConfirmation) {
|
||||||
|
document.location = '/';
|
||||||
|
}
|
||||||
}, function(result) {
|
}, function(result) {
|
||||||
$scope.registering = false;
|
$scope.registering = false;
|
||||||
UIService.showFormError('#signupButton', result);
|
UIService.showFormError('#signupButton', result);
|
||||||
|
@ -2790,7 +2956,7 @@ quayApp.directive('dockerAuthDialog', function (Config) {
|
||||||
$scope.downloadCfg = function() {
|
$scope.downloadCfg = function() {
|
||||||
var auth = $.base64.encode($scope.username + ":" + $scope.token);
|
var auth = $.base64.encode($scope.username + ":" + $scope.token);
|
||||||
config = {}
|
config = {}
|
||||||
config[Config.getUrl('/v1/')] = {
|
config[Config['SERVER_HOSTNAME']] = {
|
||||||
"auth": auth,
|
"auth": auth,
|
||||||
"email": ""
|
"email": ""
|
||||||
};
|
};
|
||||||
|
@ -2917,9 +3083,10 @@ quayApp.directive('logsView', function () {
|
||||||
'user': '=user',
|
'user': '=user',
|
||||||
'makevisible': '=makevisible',
|
'makevisible': '=makevisible',
|
||||||
'repository': '=repository',
|
'repository': '=repository',
|
||||||
'performer': '=performer'
|
'performer': '=performer',
|
||||||
|
'allLogs': '@allLogs'
|
||||||
},
|
},
|
||||||
controller: function($scope, $element, $sce, Restangular, ApiService, TriggerDescriptionBuilder,
|
controller: function($scope, $element, $sce, Restangular, ApiService, TriggerService,
|
||||||
StringBuilderService, ExternalNotificationData) {
|
StringBuilderService, ExternalNotificationData) {
|
||||||
$scope.loading = true;
|
$scope.loading = true;
|
||||||
$scope.logs = null;
|
$scope.logs = null;
|
||||||
|
@ -2984,7 +3151,7 @@ quayApp.directive('logsView', function () {
|
||||||
'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) {
|
||||||
var triggerDescription = TriggerDescriptionBuilder.getDescription(
|
var triggerDescription = TriggerService.getDescription(
|
||||||
metadata['service'], metadata['config']);
|
metadata['service'], metadata['config']);
|
||||||
return 'Build image from Dockerfile for repository {repo} triggered by ' + triggerDescription;
|
return 'Build image from Dockerfile for repository {repo} triggered by ' + triggerDescription;
|
||||||
}
|
}
|
||||||
|
@ -2994,6 +3161,24 @@ quayApp.directive('logsView', function () {
|
||||||
'org_delete_team': 'Delete team: {team}',
|
'org_delete_team': 'Delete team: {team}',
|
||||||
'org_add_team_member': 'Add member {member} to team {team}',
|
'org_add_team_member': 'Add member {member} to team {team}',
|
||||||
'org_remove_team_member': 'Remove member {member} from team {team}',
|
'org_remove_team_member': 'Remove member {member} from team {team}',
|
||||||
|
'org_invite_team_member': function(metadata) {
|
||||||
|
if (metadata.user) {
|
||||||
|
return 'Invite {user} to team {team}';
|
||||||
|
} else {
|
||||||
|
return 'Invite {email} to team {team}';
|
||||||
|
}
|
||||||
|
},
|
||||||
|
'org_delete_team_member_invite': function(metadata) {
|
||||||
|
if (metadata.user) {
|
||||||
|
return 'Rescind invite of {user} to team {team}';
|
||||||
|
} else {
|
||||||
|
return 'Rescind invite of {email} to team {team}';
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
'org_team_member_invite_accepted': 'User {member}, invited by {inviter}, joined team {team}',
|
||||||
|
'org_team_member_invite_declined': 'User {member}, invited by {inviter}, declined to join team {team}',
|
||||||
|
|
||||||
'org_set_team_description': 'Change description of team {team}: {description}',
|
'org_set_team_description': 'Change description of team {team}: {description}',
|
||||||
'org_set_team_role': 'Change permission of team {team} to {role}',
|
'org_set_team_role': 'Change permission of team {team} to {role}',
|
||||||
'create_prototype_permission': function(metadata) {
|
'create_prototype_permission': function(metadata) {
|
||||||
|
@ -3018,12 +3203,12 @@ quayApp.directive('logsView', function () {
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
'setup_repo_trigger': function(metadata) {
|
'setup_repo_trigger': function(metadata) {
|
||||||
var triggerDescription = TriggerDescriptionBuilder.getDescription(
|
var triggerDescription = TriggerService.getDescription(
|
||||||
metadata['service'], metadata['config']);
|
metadata['service'], metadata['config']);
|
||||||
return 'Setup build trigger - ' + triggerDescription;
|
return 'Setup build trigger - ' + triggerDescription;
|
||||||
},
|
},
|
||||||
'delete_repo_trigger': function(metadata) {
|
'delete_repo_trigger': function(metadata) {
|
||||||
var triggerDescription = TriggerDescriptionBuilder.getDescription(
|
var triggerDescription = TriggerService.getDescription(
|
||||||
metadata['service'], metadata['config']);
|
metadata['service'], metadata['config']);
|
||||||
return 'Delete build trigger - ' + triggerDescription;
|
return 'Delete build trigger - ' + triggerDescription;
|
||||||
},
|
},
|
||||||
|
@ -3074,7 +3259,11 @@ quayApp.directive('logsView', function () {
|
||||||
'org_create_team': 'Create team',
|
'org_create_team': 'Create team',
|
||||||
'org_delete_team': 'Delete team',
|
'org_delete_team': 'Delete team',
|
||||||
'org_add_team_member': 'Add team member',
|
'org_add_team_member': 'Add team member',
|
||||||
|
'org_invite_team_member': 'Invite team member',
|
||||||
|
'org_delete_team_member_invite': 'Rescind team member invitation',
|
||||||
'org_remove_team_member': 'Remove team member',
|
'org_remove_team_member': 'Remove team member',
|
||||||
|
'org_team_member_invite_accepted': 'Team invite accepted',
|
||||||
|
'org_team_member_invite_declined': 'Team invite declined',
|
||||||
'org_set_team_description': 'Change team description',
|
'org_set_team_description': 'Change team description',
|
||||||
'org_set_team_role': 'Change team permission',
|
'org_set_team_role': 'Change team permission',
|
||||||
'create_prototype_permission': 'Create default permission',
|
'create_prototype_permission': 'Create default permission',
|
||||||
|
@ -3107,7 +3296,7 @@ quayApp.directive('logsView', function () {
|
||||||
var hasValidUser = !!$scope.user;
|
var hasValidUser = !!$scope.user;
|
||||||
var hasValidOrg = !!$scope.organization;
|
var hasValidOrg = !!$scope.organization;
|
||||||
var hasValidRepo = $scope.repository && $scope.repository.namespace;
|
var hasValidRepo = $scope.repository && $scope.repository.namespace;
|
||||||
var isValid = hasValidUser || hasValidOrg || hasValidRepo;
|
var isValid = hasValidUser || hasValidOrg || hasValidRepo || $scope.allLogs;
|
||||||
|
|
||||||
if (!$scope.makevisible || !isValid) {
|
if (!$scope.makevisible || !isValid) {
|
||||||
return;
|
return;
|
||||||
|
@ -3130,11 +3319,15 @@ quayApp.directive('logsView', function () {
|
||||||
url = getRestUrl('repository', $scope.repository.namespace, $scope.repository.name, 'logs');
|
url = getRestUrl('repository', $scope.repository.namespace, $scope.repository.name, 'logs');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if ($scope.allLogs) {
|
||||||
|
url = getRestUrl('superuser', 'logs')
|
||||||
|
}
|
||||||
|
|
||||||
url += '?starttime=' + encodeURIComponent(getDateString($scope.logStartDate));
|
url += '?starttime=' + encodeURIComponent(getDateString($scope.logStartDate));
|
||||||
url += '&endtime=' + encodeURIComponent(getDateString($scope.logEndDate));
|
url += '&endtime=' + encodeURIComponent(getDateString($scope.logEndDate));
|
||||||
|
|
||||||
if ($scope.performer) {
|
if ($scope.performer) {
|
||||||
url += '&performer=' + encodeURIComponent($scope.performer.username);
|
url += '&performer=' + encodeURIComponent($scope.performer.name);
|
||||||
}
|
}
|
||||||
|
|
||||||
var loadLogs = Restangular.one(url);
|
var loadLogs = Restangular.one(url);
|
||||||
|
@ -3783,7 +3976,9 @@ quayApp.directive('entitySearch', function () {
|
||||||
'allowedEntities': '=allowedEntities',
|
'allowedEntities': '=allowedEntities',
|
||||||
|
|
||||||
'currentEntity': '=currentEntity',
|
'currentEntity': '=currentEntity',
|
||||||
|
|
||||||
'entitySelected': '&entitySelected',
|
'entitySelected': '&entitySelected',
|
||||||
|
'emailSelected': '&emailSelected',
|
||||||
|
|
||||||
// When set to true, the contents of the control will be cleared as soon
|
// When set to true, the contents of the control will be cleared as soon
|
||||||
// as an entity is selected.
|
// as an entity is selected.
|
||||||
|
@ -3791,8 +3986,15 @@ quayApp.directive('entitySearch', function () {
|
||||||
|
|
||||||
// Set this property to immediately clear the contents of the control.
|
// Set this property to immediately clear the contents of the control.
|
||||||
'clearValue': '=clearValue',
|
'clearValue': '=clearValue',
|
||||||
|
|
||||||
|
// Whether e-mail addresses are allowed.
|
||||||
|
'allowEmails': '=allowEmails',
|
||||||
|
'emailMessage': '@emailMessage',
|
||||||
|
|
||||||
|
// True if the menu should pull right.
|
||||||
|
'pullRight': '@pullRight'
|
||||||
},
|
},
|
||||||
controller: function($rootScope, $scope, $element, Restangular, UserService, ApiService, Config) {
|
controller: function($rootScope, $scope, $element, Restangular, UserService, ApiService, UtilService, Config) {
|
||||||
$scope.lazyLoading = true;
|
$scope.lazyLoading = true;
|
||||||
|
|
||||||
$scope.teams = null;
|
$scope.teams = null;
|
||||||
|
@ -3989,8 +4191,12 @@ quayApp.directive('entitySearch', function () {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (val.indexOf('@') > 0) {
|
if (UtilService.isEmailAddress(val)) {
|
||||||
return '<div class="tt-empty">A ' + Config.REGISTRY_TITLE_SHORT + ' username (not an e-mail address) must be specified</div>';
|
if ($scope.allowEmails) {
|
||||||
|
return '<div class="tt-message">' + $scope.emailMessage + '</div>';
|
||||||
|
} else {
|
||||||
|
return '<div class="tt-empty">A ' + Config.REGISTRY_TITLE_SHORT + ' username (not an e-mail address) must be specified</div>';
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
var classes = [];
|
var classes = [];
|
||||||
|
@ -4046,6 +4252,16 @@ quayApp.directive('entitySearch', function () {
|
||||||
}}
|
}}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
$(input).on('keypress', function(e) {
|
||||||
|
var val = $(input).val();
|
||||||
|
var code = e.keyCode || e.which;
|
||||||
|
if (code == 13 && $scope.allowEmails && UtilService.isEmailAddress(val)) {
|
||||||
|
$scope.$apply(function() {
|
||||||
|
$scope.emailSelected({'email': val});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
$(input).on('input', function(e) {
|
$(input).on('input', function(e) {
|
||||||
$scope.$apply(function() {
|
$scope.$apply(function() {
|
||||||
$scope.clearEntityInternal();
|
$scope.clearEntityInternal();
|
||||||
|
@ -4694,6 +4910,66 @@ quayApp.directive('dropdownSelectMenu', function () {
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
||||||
|
quayApp.directive('manualTriggerBuildDialog', function () {
|
||||||
|
var directiveDefinitionObject = {
|
||||||
|
templateUrl: '/static/directives/manual-trigger-build-dialog.html',
|
||||||
|
replace: false,
|
||||||
|
transclude: false,
|
||||||
|
restrict: 'C',
|
||||||
|
scope: {
|
||||||
|
'repository': '=repository',
|
||||||
|
'counter': '=counter',
|
||||||
|
'trigger': '=trigger',
|
||||||
|
'startBuild': '&startBuild'
|
||||||
|
},
|
||||||
|
controller: function($scope, $element, ApiService, TriggerService) {
|
||||||
|
$scope.parameters = {};
|
||||||
|
$scope.fieldOptions = {};
|
||||||
|
|
||||||
|
$scope.startTrigger = function() {
|
||||||
|
$('#startTriggerDialog').modal('hide');
|
||||||
|
$scope.startBuild({
|
||||||
|
'trigger': $scope.trigger,
|
||||||
|
'parameters': $scope.parameters
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
$scope.show = function() {
|
||||||
|
$scope.parameters = {};
|
||||||
|
$scope.fieldOptions = {};
|
||||||
|
|
||||||
|
var parameters = TriggerService.getRunParameters($scope.trigger.service);
|
||||||
|
for (var i = 0; i < parameters.length; ++i) {
|
||||||
|
var parameter = parameters[i];
|
||||||
|
if (parameter['type'] == 'option') {
|
||||||
|
// Load the values for this parameter.
|
||||||
|
var params = {
|
||||||
|
'repository': $scope.repository.namespace + '/' + $scope.repository.name,
|
||||||
|
'trigger_uuid': $scope.trigger.id,
|
||||||
|
'field_name': parameter['name']
|
||||||
|
};
|
||||||
|
|
||||||
|
ApiService.listTriggerFieldValues(null, params).then(function(resp) {
|
||||||
|
$scope.fieldOptions[parameter['name']] = resp['values'];
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
$scope.runParameters = parameters;
|
||||||
|
|
||||||
|
$('#startTriggerDialog').modal('show');
|
||||||
|
};
|
||||||
|
|
||||||
|
$scope.$watch('counter', function(counter) {
|
||||||
|
if (counter) {
|
||||||
|
$scope.show();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
};
|
||||||
|
return directiveDefinitionObject;
|
||||||
|
});
|
||||||
|
|
||||||
|
|
||||||
quayApp.directive('setupTriggerDialog', function () {
|
quayApp.directive('setupTriggerDialog', function () {
|
||||||
var directiveDefinitionObject = {
|
var directiveDefinitionObject = {
|
||||||
templateUrl: '/static/directives/setup-trigger-dialog.html',
|
templateUrl: '/static/directives/setup-trigger-dialog.html',
|
||||||
|
@ -5522,6 +5798,10 @@ quayApp.directive('notificationView', function () {
|
||||||
$scope.getClass = function(notification) {
|
$scope.getClass = function(notification) {
|
||||||
return NotificationService.getClass(notification);
|
return NotificationService.getClass(notification);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
$scope.getActions = function(notification) {
|
||||||
|
return NotificationService.getActions(notification);
|
||||||
|
};
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
return directiveDefinitionObject;
|
return directiveDefinitionObject;
|
||||||
|
@ -5737,7 +6017,7 @@ quayApp.directive('dockerfileBuildForm', function () {
|
||||||
var data = {
|
var data = {
|
||||||
'mimeType': mimeType
|
'mimeType': mimeType
|
||||||
};
|
};
|
||||||
|
|
||||||
var getUploadUrl = ApiService.getFiledropUrl(data).then(function(resp) {
|
var getUploadUrl = ApiService.getFiledropUrl(data).then(function(resp) {
|
||||||
conductUpload(file, resp.url, resp.file_id, mimeType);
|
conductUpload(file, resp.url, resp.file_id, mimeType);
|
||||||
}, function() {
|
}, function() {
|
||||||
|
@ -5890,7 +6170,7 @@ quayApp.directive('tagSpecificImagesView', function () {
|
||||||
}
|
}
|
||||||
|
|
||||||
var currentTag = $scope.repository.tags[$scope.tag];
|
var currentTag = $scope.repository.tags[$scope.tag];
|
||||||
if (image.dbid == currentTag.dbid) {
|
if (image.id == currentTag.image_id) {
|
||||||
classes += 'tag-image ';
|
classes += 'tag-image ';
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -5900,15 +6180,15 @@ quayApp.directive('tagSpecificImagesView', function () {
|
||||||
var forAllTagImages = function(tag, callback, opt_cutoff) {
|
var forAllTagImages = function(tag, callback, opt_cutoff) {
|
||||||
if (!tag) { return; }
|
if (!tag) { return; }
|
||||||
|
|
||||||
if (!$scope.imageByDBID) {
|
if (!$scope.imageByDockerId) {
|
||||||
$scope.imageByDBID = [];
|
$scope.imageByDockerId = [];
|
||||||
for (var i = 0; i < $scope.images.length; ++i) {
|
for (var i = 0; i < $scope.images.length; ++i) {
|
||||||
var currentImage = $scope.images[i];
|
var currentImage = $scope.images[i];
|
||||||
$scope.imageByDBID[currentImage.dbid] = currentImage;
|
$scope.imageByDockerId[currentImage.id] = currentImage;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
var tag_image = $scope.imageByDBID[tag.dbid];
|
var tag_image = $scope.imageByDockerId[tag.image_id];
|
||||||
if (!tag_image) {
|
if (!tag_image) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
@ -5917,7 +6197,7 @@ quayApp.directive('tagSpecificImagesView', function () {
|
||||||
|
|
||||||
var ancestors = tag_image.ancestors.split('/').reverse();
|
var ancestors = tag_image.ancestors.split('/').reverse();
|
||||||
for (var i = 0; i < ancestors.length; ++i) {
|
for (var i = 0; i < ancestors.length; ++i) {
|
||||||
var image = $scope.imageByDBID[ancestors[i]];
|
var image = $scope.imageByDockerId[ancestors[i]];
|
||||||
if (image) {
|
if (image) {
|
||||||
if (image == opt_cutoff) {
|
if (image == opt_cutoff) {
|
||||||
return;
|
return;
|
||||||
|
@ -5943,7 +6223,7 @@ quayApp.directive('tagSpecificImagesView', function () {
|
||||||
var getIdsForTag = function(currentTag) {
|
var getIdsForTag = function(currentTag) {
|
||||||
var ids = {};
|
var ids = {};
|
||||||
forAllTagImages(currentTag, function(image) {
|
forAllTagImages(currentTag, function(image) {
|
||||||
ids[image.dbid] = true;
|
ids[image.id] = true;
|
||||||
}, $scope.imageCutoff);
|
}, $scope.imageCutoff);
|
||||||
return ids;
|
return ids;
|
||||||
};
|
};
|
||||||
|
@ -5953,8 +6233,8 @@ quayApp.directive('tagSpecificImagesView', function () {
|
||||||
for (var currentTagName in $scope.repository.tags) {
|
for (var currentTagName in $scope.repository.tags) {
|
||||||
var currentTag = $scope.repository.tags[currentTagName];
|
var currentTag = $scope.repository.tags[currentTagName];
|
||||||
if (currentTag != tag) {
|
if (currentTag != tag) {
|
||||||
for (var dbid in getIdsForTag(currentTag)) {
|
for (var id in getIdsForTag(currentTag)) {
|
||||||
delete toDelete[dbid];
|
delete toDelete[id];
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -5963,7 +6243,7 @@ quayApp.directive('tagSpecificImagesView', function () {
|
||||||
var images = [];
|
var images = [];
|
||||||
for (var i = 0; i < $scope.images.length; ++i) {
|
for (var i = 0; i < $scope.images.length; ++i) {
|
||||||
var image = $scope.images[i];
|
var image = $scope.images[i];
|
||||||
if (toDelete[image.dbid]) {
|
if (toDelete[image.id]) {
|
||||||
images.push(image);
|
images.push(image);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -5974,7 +6254,7 @@ quayApp.directive('tagSpecificImagesView', function () {
|
||||||
return result;
|
return result;
|
||||||
}
|
}
|
||||||
|
|
||||||
return b.dbid - a.dbid;
|
return b.sort_index - a.sort_index;
|
||||||
});
|
});
|
||||||
|
|
||||||
$scope.tagSpecificImages = images;
|
$scope.tagSpecificImages = images;
|
||||||
|
|
|
@ -1,3 +1,7 @@
|
||||||
|
function SignInCtrl($scope, $location) {
|
||||||
|
$scope.redirectUrl = '/';
|
||||||
|
}
|
||||||
|
|
||||||
function GuideCtrl() {
|
function GuideCtrl() {
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -536,7 +540,7 @@ function RepoCtrl($scope, $sanitize, Restangular, ImageMetadataService, ApiServi
|
||||||
};
|
};
|
||||||
|
|
||||||
$scope.findImageForTag = function(tag) {
|
$scope.findImageForTag = function(tag) {
|
||||||
return tag && $scope.imageByDBID && $scope.imageByDBID[tag.dbid];
|
return tag && $scope.imageByDockerId && $scope.imageByDockerId[tag.image_id];
|
||||||
};
|
};
|
||||||
|
|
||||||
$scope.createOrMoveTag = function(image, tagName, opt_invalid) {
|
$scope.createOrMoveTag = function(image, tagName, opt_invalid) {
|
||||||
|
@ -608,6 +612,8 @@ function RepoCtrl($scope, $sanitize, Restangular, ImageMetadataService, ApiServi
|
||||||
};
|
};
|
||||||
|
|
||||||
$scope.setImage = function(imageId, opt_updateURL) {
|
$scope.setImage = function(imageId, opt_updateURL) {
|
||||||
|
if (!$scope.images) { return; }
|
||||||
|
|
||||||
var image = null;
|
var image = null;
|
||||||
for (var i = 0; i < $scope.images.length; ++i) {
|
for (var i = 0; i < $scope.images.length; ++i) {
|
||||||
var currentImage = $scope.images[i];
|
var currentImage = $scope.images[i];
|
||||||
|
@ -728,9 +734,9 @@ function RepoCtrl($scope, $sanitize, Restangular, ImageMetadataService, ApiServi
|
||||||
};
|
};
|
||||||
|
|
||||||
var forAllTagImages = function(tag, callback) {
|
var forAllTagImages = function(tag, callback) {
|
||||||
if (!tag || !$scope.imageByDBID) { return; }
|
if (!tag || !$scope.imageByDockerId) { return; }
|
||||||
|
|
||||||
var tag_image = $scope.imageByDBID[tag.dbid];
|
var tag_image = $scope.imageByDockerId[tag.image_id];
|
||||||
if (!tag_image) { return; }
|
if (!tag_image) { return; }
|
||||||
|
|
||||||
// Callback the tag's image itself.
|
// Callback the tag's image itself.
|
||||||
|
@ -740,7 +746,7 @@ function RepoCtrl($scope, $sanitize, Restangular, ImageMetadataService, ApiServi
|
||||||
if (!tag_image.ancestors) { return; }
|
if (!tag_image.ancestors) { return; }
|
||||||
var ancestors = tag_image.ancestors.split('/');
|
var ancestors = tag_image.ancestors.split('/');
|
||||||
for (var i = 0; i < ancestors.length; ++i) {
|
for (var i = 0; i < ancestors.length; ++i) {
|
||||||
var image = $scope.imageByDBID[ancestors[i]];
|
var image = $scope.imageByDockerId[ancestors[i]];
|
||||||
if (image) {
|
if (image) {
|
||||||
callback(image);
|
callback(image);
|
||||||
}
|
}
|
||||||
|
@ -829,10 +835,10 @@ function RepoCtrl($scope, $sanitize, Restangular, ImageMetadataService, ApiServi
|
||||||
$scope.specificImages = [];
|
$scope.specificImages = [];
|
||||||
|
|
||||||
// Build various images for quick lookup of images.
|
// Build various images for quick lookup of images.
|
||||||
$scope.imageByDBID = {};
|
$scope.imageByDockerId = {};
|
||||||
for (var i = 0; i < $scope.images.length; ++i) {
|
for (var i = 0; i < $scope.images.length; ++i) {
|
||||||
var currentImage = $scope.images[i];
|
var currentImage = $scope.images[i];
|
||||||
$scope.imageByDBID[currentImage.dbid] = currentImage;
|
$scope.imageByDockerId[currentImage.id] = currentImage;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Dispose of any existing tree.
|
// Dispose of any existing tree.
|
||||||
|
@ -1275,7 +1281,9 @@ function RepoBuildCtrl($scope, Restangular, ApiService, $routeParams, $rootScope
|
||||||
fetchRepository();
|
fetchRepository();
|
||||||
}
|
}
|
||||||
|
|
||||||
function RepoAdminCtrl($scope, Restangular, ApiService, KeyService, $routeParams, $rootScope, $location, UserService, Config, Features, ExternalNotificationData) {
|
function RepoAdminCtrl($scope, Restangular, ApiService, KeyService, TriggerService, $routeParams,
|
||||||
|
$rootScope, $location, UserService, Config, Features, ExternalNotificationData) {
|
||||||
|
|
||||||
var namespace = $routeParams.namespace;
|
var namespace = $routeParams.namespace;
|
||||||
var name = $routeParams.name;
|
var name = $routeParams.name;
|
||||||
|
|
||||||
|
@ -1580,14 +1588,22 @@ function RepoAdminCtrl($scope, Restangular, ApiService, KeyService, $routeParams
|
||||||
$scope.deleteTrigger(trigger);
|
$scope.deleteTrigger(trigger);
|
||||||
};
|
};
|
||||||
|
|
||||||
$scope.startTrigger = function(trigger) {
|
$scope.showManualBuildDialog = 0;
|
||||||
|
|
||||||
|
$scope.startTrigger = function(trigger, opt_custom) {
|
||||||
|
var parameters = TriggerService.getRunParameters(trigger.service);
|
||||||
|
if (parameters.length && !opt_custom) {
|
||||||
|
$scope.currentStartTrigger = trigger;
|
||||||
|
$scope.showManualBuildDialog++;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
var params = {
|
var params = {
|
||||||
'repository': namespace + '/' + name,
|
'repository': namespace + '/' + name,
|
||||||
'trigger_uuid': trigger.id
|
'trigger_uuid': trigger.id
|
||||||
};
|
};
|
||||||
|
|
||||||
ApiService.manuallyStartBuildTrigger(null, params).then(function(resp) {
|
ApiService.manuallyStartBuildTrigger(opt_custom || {}, params).then(function(resp) {
|
||||||
window.console.log(resp);
|
|
||||||
var url = '/repository/' + namespace + '/' + name + '/build?current=' + resp['id'];
|
var url = '/repository/' + namespace + '/' + name + '/build?current=' + resp['id'];
|
||||||
document.location = url;
|
document.location = url;
|
||||||
}, ApiService.errorDisplay('Could not start build'));
|
}, ApiService.errorDisplay('Could not start build'));
|
||||||
|
@ -2326,29 +2342,92 @@ function OrgAdminCtrl($rootScope, $scope, $timeout, Restangular, $routeParams, U
|
||||||
loadOrganization();
|
loadOrganization();
|
||||||
}
|
}
|
||||||
|
|
||||||
function TeamViewCtrl($rootScope, $scope, Restangular, ApiService, $routeParams) {
|
function TeamViewCtrl($rootScope, $scope, $timeout, Features, Restangular, ApiService, $routeParams) {
|
||||||
var teamname = $routeParams.teamname;
|
var teamname = $routeParams.teamname;
|
||||||
var orgname = $routeParams.orgname;
|
var orgname = $routeParams.orgname;
|
||||||
|
|
||||||
$scope.orgname = orgname;
|
$scope.orgname = orgname;
|
||||||
$scope.teamname = teamname;
|
$scope.teamname = teamname;
|
||||||
|
$scope.addingMember = false;
|
||||||
|
$scope.memberMap = null;
|
||||||
|
$scope.allowEmail = Features.MAILING;
|
||||||
|
|
||||||
$rootScope.title = 'Loading...';
|
$rootScope.title = 'Loading...';
|
||||||
|
|
||||||
$scope.addNewMember = function(member) {
|
$scope.filterFunction = function(invited, robots) {
|
||||||
if (!member || $scope.members[member.name]) { return; }
|
return function(item) {
|
||||||
|
// Note: The !! is needed because is_robot will be undefined for invites.
|
||||||
|
var robot_check = (!!item.is_robot == robots);
|
||||||
|
return robot_check && item.invited == invited;
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
$scope.inviteEmail = function(email) {
|
||||||
|
if (!email || $scope.memberMap[email]) { return; }
|
||||||
|
|
||||||
|
$scope.addingMember = true;
|
||||||
|
|
||||||
|
var params = {
|
||||||
|
'orgname': orgname,
|
||||||
|
'teamname': teamname,
|
||||||
|
'email': email
|
||||||
|
};
|
||||||
|
|
||||||
|
var errorHandler = ApiService.errorDisplay('Cannot invite team member', function() {
|
||||||
|
$scope.addingMember = false;
|
||||||
|
});
|
||||||
|
|
||||||
|
ApiService.inviteTeamMemberEmail(null, params).then(function(resp) {
|
||||||
|
$scope.members.push(resp);
|
||||||
|
$scope.memberMap[resp.email] = resp;
|
||||||
|
$scope.addingMember = false;
|
||||||
|
}, errorHandler);
|
||||||
|
};
|
||||||
|
|
||||||
|
$scope.addNewMember = function(member) {
|
||||||
|
if (!member || $scope.memberMap[member.name]) { return; }
|
||||||
|
|
||||||
var params = {
|
var params = {
|
||||||
'orgname': orgname,
|
'orgname': orgname,
|
||||||
'teamname': teamname,
|
'teamname': teamname,
|
||||||
'membername': member.name
|
'membername': member.name
|
||||||
};
|
};
|
||||||
|
|
||||||
ApiService.updateOrganizationTeamMember(null, params).then(function(resp) {
|
var errorHandler = ApiService.errorDisplay('Cannot add team member', function() {
|
||||||
$scope.members[member.name] = resp;
|
$scope.addingMember = false;
|
||||||
}, function() {
|
|
||||||
$('#cannotChangeMembersModal').modal({});
|
|
||||||
});
|
});
|
||||||
|
|
||||||
|
$scope.addingMember = true;
|
||||||
|
ApiService.updateOrganizationTeamMember(null, params).then(function(resp) {
|
||||||
|
$scope.members.push(resp);
|
||||||
|
$scope.memberMap[resp.name] = resp;
|
||||||
|
$scope.addingMember = false;
|
||||||
|
}, errorHandler);
|
||||||
|
};
|
||||||
|
|
||||||
|
$scope.revokeInvite = function(inviteInfo) {
|
||||||
|
if (inviteInfo.kind == 'invite') {
|
||||||
|
// E-mail invite.
|
||||||
|
$scope.revokeEmailInvite(inviteInfo.email);
|
||||||
|
} else {
|
||||||
|
// User invite.
|
||||||
|
$scope.removeMember(inviteInfo.name);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
$scope.revokeEmailInvite = function(email) {
|
||||||
|
var params = {
|
||||||
|
'orgname': orgname,
|
||||||
|
'teamname': teamname,
|
||||||
|
'email': email
|
||||||
|
};
|
||||||
|
|
||||||
|
ApiService.deleteTeamMemberEmailInvite(null, params).then(function(resp) {
|
||||||
|
if (!$scope.memberMap[email]) { return; }
|
||||||
|
var index = $.inArray($scope.memberMap[email], $scope.members);
|
||||||
|
$scope.members.splice(index, 1);
|
||||||
|
delete $scope.memberMap[email];
|
||||||
|
}, ApiService.errorDisplay('Cannot revoke team invite'));
|
||||||
};
|
};
|
||||||
|
|
||||||
$scope.removeMember = function(username) {
|
$scope.removeMember = function(username) {
|
||||||
|
@ -2359,10 +2438,11 @@ function TeamViewCtrl($rootScope, $scope, Restangular, ApiService, $routeParams)
|
||||||
};
|
};
|
||||||
|
|
||||||
ApiService.deleteOrganizationTeamMember(null, params).then(function(resp) {
|
ApiService.deleteOrganizationTeamMember(null, params).then(function(resp) {
|
||||||
delete $scope.members[username];
|
if (!$scope.memberMap[username]) { return; }
|
||||||
}, function() {
|
var index = $.inArray($scope.memberMap[username], $scope.members);
|
||||||
$('#cannotChangeMembersModal').modal({});
|
$scope.members.splice(index, 1);
|
||||||
});
|
delete $scope.memberMap[username];
|
||||||
|
}, ApiService.errorDisplay('Cannot remove team member'));
|
||||||
};
|
};
|
||||||
|
|
||||||
$scope.updateForDescription = function(content) {
|
$scope.updateForDescription = function(content) {
|
||||||
|
@ -2394,7 +2474,8 @@ function TeamViewCtrl($rootScope, $scope, Restangular, ApiService, $routeParams)
|
||||||
var loadMembers = function() {
|
var loadMembers = function() {
|
||||||
var params = {
|
var params = {
|
||||||
'orgname': orgname,
|
'orgname': orgname,
|
||||||
'teamname': teamname
|
'teamname': teamname,
|
||||||
|
'includePending': true
|
||||||
};
|
};
|
||||||
|
|
||||||
$scope.membersResource = ApiService.getOrganizationTeamMembersAsResource(params).get(function(resp) {
|
$scope.membersResource = ApiService.getOrganizationTeamMembersAsResource(params).get(function(resp) {
|
||||||
|
@ -2406,6 +2487,12 @@ function TeamViewCtrl($rootScope, $scope, Restangular, ApiService, $routeParams)
|
||||||
'html': true
|
'html': true
|
||||||
});
|
});
|
||||||
|
|
||||||
|
$scope.memberMap = {};
|
||||||
|
for (var i = 0; i < $scope.members.length; ++i) {
|
||||||
|
var current = $scope.members[i];
|
||||||
|
$scope.memberMap[current.name || current.email] = current;
|
||||||
|
}
|
||||||
|
|
||||||
return resp.members;
|
return resp.members;
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
@ -2533,7 +2620,7 @@ function OrgMemberLogsCtrl($scope, $routeParams, $rootScope, $timeout, Restangul
|
||||||
$scope.memberResource = ApiService.getOrganizationMemberAsResource(params).get(function(resp) {
|
$scope.memberResource = ApiService.getOrganizationMemberAsResource(params).get(function(resp) {
|
||||||
$scope.memberInfo = resp.member;
|
$scope.memberInfo = resp.member;
|
||||||
|
|
||||||
$rootScope.title = 'Logs for ' + $scope.memberInfo.username + ' (' + $scope.orgname + ')';
|
$rootScope.title = 'Logs for ' + $scope.memberInfo.name + ' (' + $scope.orgname + ')';
|
||||||
$rootScope.description = 'Shows all the actions of ' + $scope.memberInfo.username +
|
$rootScope.description = 'Shows all the actions of ' + $scope.memberInfo.username +
|
||||||
' under organization ' + $scope.orgname;
|
' under organization ' + $scope.orgname;
|
||||||
|
|
||||||
|
@ -2656,6 +2743,14 @@ function SuperUserAdminCtrl($scope, ApiService, Features, UserService) {
|
||||||
// Monitor any user changes and place the current user into the scope.
|
// Monitor any user changes and place the current user into the scope.
|
||||||
UserService.updateUserIn($scope);
|
UserService.updateUserIn($scope);
|
||||||
|
|
||||||
|
$scope.logsCounter = 0;
|
||||||
|
$scope.newUser = {};
|
||||||
|
$scope.createdUsers = [];
|
||||||
|
|
||||||
|
$scope.loadLogs = function() {
|
||||||
|
$scope.logsCounter++;
|
||||||
|
};
|
||||||
|
|
||||||
$scope.loadUsers = function() {
|
$scope.loadUsers = function() {
|
||||||
if ($scope.users) {
|
if ($scope.users) {
|
||||||
return;
|
return;
|
||||||
|
@ -2667,6 +2762,7 @@ function SuperUserAdminCtrl($scope, ApiService, Features, UserService) {
|
||||||
$scope.loadUsersInternal = function() {
|
$scope.loadUsersInternal = function() {
|
||||||
ApiService.listAllUsers().then(function(resp) {
|
ApiService.listAllUsers().then(function(resp) {
|
||||||
$scope.users = resp['users'];
|
$scope.users = resp['users'];
|
||||||
|
$scope.showInterface = true;
|
||||||
}, function(resp) {
|
}, function(resp) {
|
||||||
$scope.users = [];
|
$scope.users = [];
|
||||||
$scope.usersError = resp['data']['message'] || resp['data']['error_description'];
|
$scope.usersError = resp['data']['message'] || resp['data']['error_description'];
|
||||||
|
@ -2678,6 +2774,19 @@ function SuperUserAdminCtrl($scope, ApiService, Features, UserService) {
|
||||||
$('#changePasswordModal').modal({});
|
$('#changePasswordModal').modal({});
|
||||||
};
|
};
|
||||||
|
|
||||||
|
$scope.createUser = function() {
|
||||||
|
$scope.creatingUser = true;
|
||||||
|
var errorHandler = ApiService.errorDisplay('Cannot create user', function() {
|
||||||
|
$scope.creatingUser = false;
|
||||||
|
});
|
||||||
|
|
||||||
|
ApiService.createInstallUser($scope.newUser, null).then(function(resp) {
|
||||||
|
$scope.creatingUser = false;
|
||||||
|
$scope.newUser = {};
|
||||||
|
$scope.createdUsers.push(resp);
|
||||||
|
}, errorHandler)
|
||||||
|
};
|
||||||
|
|
||||||
$scope.showDeleteUser = function(user) {
|
$scope.showDeleteUser = function(user) {
|
||||||
if (user.username == UserService.currentUser().username) {
|
if (user.username == UserService.currentUser().username) {
|
||||||
bootbox.dialog({
|
bootbox.dialog({
|
||||||
|
@ -2725,9 +2834,58 @@ function SuperUserAdminCtrl($scope, ApiService, Features, UserService) {
|
||||||
}, ApiService.errorDisplay('Cannot delete user'));
|
}, ApiService.errorDisplay('Cannot delete user'));
|
||||||
};
|
};
|
||||||
|
|
||||||
|
$scope.sendRecoveryEmail = function(user) {
|
||||||
|
var params = {
|
||||||
|
'username': user.username
|
||||||
|
};
|
||||||
|
|
||||||
|
ApiService.sendInstallUserRecoveryEmail(null, params).then(function(resp) {
|
||||||
|
bootbox.dialog({
|
||||||
|
"message": "A recovery email has been sent to " + resp['email'],
|
||||||
|
"title": "Recovery email sent",
|
||||||
|
"buttons": {
|
||||||
|
"close": {
|
||||||
|
"label": "Close",
|
||||||
|
"className": "btn-primary"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
}, ApiService.errorDisplay('Cannot send recovery email'))
|
||||||
|
};
|
||||||
|
|
||||||
$scope.loadUsers();
|
$scope.loadUsers();
|
||||||
}
|
}
|
||||||
|
|
||||||
function TourCtrl($scope, $location) {
|
function TourCtrl($scope, $location) {
|
||||||
$scope.kind = $location.path().substring('/tour/'.length);
|
$scope.kind = $location.path().substring('/tour/'.length);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function ConfirmInviteCtrl($scope, $location, UserService, ApiService, NotificationService) {
|
||||||
|
// Monitor any user changes and place the current user into the scope.
|
||||||
|
$scope.loading = false;
|
||||||
|
$scope.inviteCode = $location.search()['code'] || '';
|
||||||
|
|
||||||
|
UserService.updateUserIn($scope, function(user) {
|
||||||
|
if (!user.anonymous && !$scope.loading) {
|
||||||
|
// Make sure to not redirect now that we have logged in. We'll conduct the redirect
|
||||||
|
// manually.
|
||||||
|
$scope.redirectUrl = null;
|
||||||
|
$scope.loading = true;
|
||||||
|
|
||||||
|
var params = {
|
||||||
|
'code': $location.search()['code']
|
||||||
|
};
|
||||||
|
|
||||||
|
ApiService.acceptOrganizationTeamInvite(null, params).then(function(resp) {
|
||||||
|
NotificationService.update();
|
||||||
|
$location.path('/organization/' + resp.org + '/teams/' + resp.team);
|
||||||
|
}, function(resp) {
|
||||||
|
$scope.loading = false;
|
||||||
|
$scope.invalid = ApiService.getErrorMessage(resp, 'Invalid confirmation code');
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
$scope.redirectUrl = window.location.href;
|
||||||
|
}
|
||||||
|
|
|
@ -262,6 +262,9 @@ ImageHistoryTree.prototype.draw = function(container) {
|
||||||
|
|
||||||
// Update the dimensions of the tree.
|
// Update the dimensions of the tree.
|
||||||
var dimensions = this.updateDimensions_();
|
var dimensions = this.updateDimensions_();
|
||||||
|
if (!dimensions) {
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
// Populate the tree.
|
// Populate the tree.
|
||||||
this.root_.x0 = dimensions.cw / 2;
|
this.root_.x0 = dimensions.cw / 2;
|
||||||
|
@ -307,8 +310,8 @@ ImageHistoryTree.prototype.setHighlightedPath_ = function(image) {
|
||||||
this.markPath_(this.currentNode_, false);
|
this.markPath_(this.currentNode_, false);
|
||||||
}
|
}
|
||||||
|
|
||||||
var imageByDBID = this.imageByDBID_;
|
var imageByDockerId = this.imageByDockerId_;
|
||||||
var currentNode = imageByDBID[image.dbid];
|
var currentNode = imageByDockerId[image.id];
|
||||||
if (currentNode) {
|
if (currentNode) {
|
||||||
this.markPath_(currentNode, true);
|
this.markPath_(currentNode, true);
|
||||||
this.currentNode_ = currentNode;
|
this.currentNode_ = currentNode;
|
||||||
|
@ -386,7 +389,7 @@ ImageHistoryTree.prototype.buildRoot_ = function() {
|
||||||
var formatted = {"name": "No images found"};
|
var formatted = {"name": "No images found"};
|
||||||
|
|
||||||
// Build a node for each image.
|
// Build a node for each image.
|
||||||
var imageByDBID = {};
|
var imageByDockerId = {};
|
||||||
for (var i = 0; i < this.images_.length; ++i) {
|
for (var i = 0; i < this.images_.length; ++i) {
|
||||||
var image = this.images_[i];
|
var image = this.images_[i];
|
||||||
var imageNode = {
|
var imageNode = {
|
||||||
|
@ -395,9 +398,9 @@ ImageHistoryTree.prototype.buildRoot_ = function() {
|
||||||
"image": image,
|
"image": image,
|
||||||
"tags": image.tags
|
"tags": image.tags
|
||||||
};
|
};
|
||||||
imageByDBID[image.dbid] = imageNode;
|
imageByDockerId[image.id] = imageNode;
|
||||||
}
|
}
|
||||||
this.imageByDBID_ = imageByDBID;
|
this.imageByDockerId_ = imageByDockerId;
|
||||||
|
|
||||||
// For each node, attach it to its immediate parent. If there is no immediate parent,
|
// For each node, attach it to its immediate parent. If there is no immediate parent,
|
||||||
// then the node is the root.
|
// then the node is the root.
|
||||||
|
@ -408,10 +411,10 @@ ImageHistoryTree.prototype.buildRoot_ = function() {
|
||||||
// Skip images that are currently uploading.
|
// Skip images that are currently uploading.
|
||||||
if (image.uploading) { continue; }
|
if (image.uploading) { continue; }
|
||||||
|
|
||||||
var imageNode = imageByDBID[image.dbid];
|
var imageNode = imageByDockerId[image.id];
|
||||||
var ancestors = this.getAncestors_(image);
|
var ancestors = this.getAncestors_(image);
|
||||||
var immediateParent = ancestors[ancestors.length - 1] * 1;
|
var immediateParent = ancestors[ancestors.length - 1];
|
||||||
var parent = imageByDBID[immediateParent];
|
var parent = imageByDockerId[immediateParent];
|
||||||
if (parent) {
|
if (parent) {
|
||||||
// Add a reference to the parent. This makes walking the tree later easier.
|
// Add a reference to the parent. This makes walking the tree later easier.
|
||||||
imageNode.parent = parent;
|
imageNode.parent = parent;
|
||||||
|
@ -442,7 +445,7 @@ ImageHistoryTree.prototype.buildRoot_ = function() {
|
||||||
// Skip images that are currently uploading.
|
// Skip images that are currently uploading.
|
||||||
if (image.uploading) { continue; }
|
if (image.uploading) { continue; }
|
||||||
|
|
||||||
var imageNode = imageByDBID[image.dbid];
|
var imageNode = imageByDockerId[image.id];
|
||||||
maxChildCount = Math.max(maxChildCount, this.determineMaximumChildCount_(imageNode));
|
maxChildCount = Math.max(maxChildCount, this.determineMaximumChildCount_(imageNode));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -573,7 +576,7 @@ ImageHistoryTree.prototype.setTag_ = function(tagName) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
var imageByDBID = this.imageByDBID_;
|
var imageByDockerId = this.imageByDockerId_;
|
||||||
|
|
||||||
// Save the current tag.
|
// Save the current tag.
|
||||||
var previousTagName = this.currentTag_;
|
var previousTagName = this.currentTag_;
|
||||||
|
@ -596,10 +599,10 @@ ImageHistoryTree.prototype.setTag_ = function(tagName) {
|
||||||
// Skip images that are currently uploading.
|
// Skip images that are currently uploading.
|
||||||
if (image.uploading) { continue; }
|
if (image.uploading) { continue; }
|
||||||
|
|
||||||
var imageNode = this.imageByDBID_[image.dbid];
|
var imageNode = this.imageByDockerId_[image.id];
|
||||||
var ancestors = this.getAncestors_(image);
|
var ancestors = this.getAncestors_(image);
|
||||||
var immediateParent = ancestors[ancestors.length - 1] * 1;
|
var immediateParent = ancestors[ancestors.length - 1];
|
||||||
var parent = imageByDBID[immediateParent];
|
var parent = imageByDockerId[immediateParent];
|
||||||
if (parent && imageNode.highlighted) {
|
if (parent && imageNode.highlighted) {
|
||||||
var arr = parent.children;
|
var arr = parent.children;
|
||||||
if (parent._children) {
|
if (parent._children) {
|
||||||
|
|
15
static/partials/confirm-invite.html
Normal file
15
static/partials/confirm-invite.html
Normal file
|
@ -0,0 +1,15 @@
|
||||||
|
<div class="confirm-invite">
|
||||||
|
<div class="container signin-container">
|
||||||
|
<div class="row">
|
||||||
|
<div class="col-sm-6 col-sm-offset-3">
|
||||||
|
<div class="user-setup" ng-show="user.anonymous" redirect-url="redirectUrl"
|
||||||
|
invite-code="inviteCode">
|
||||||
|
</div>
|
||||||
|
<div class="quay-spinner" ng-show="!user.anonymous && loading"></div>
|
||||||
|
<div class="alert alert-danger" ng-show="!user.anonymous && invalid">
|
||||||
|
{{ invalid }}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
|
@ -1,6 +1,7 @@
|
||||||
<div class="resource-view" resource="memberResource" error-message="'Member not found'">
|
<div class="resource-view" resource="memberResource" error-message="'Member not found'">
|
||||||
<div class="org-member-logs container">
|
<div class="org-member-logs container">
|
||||||
<div class="organization-header" organization="organization" clickable="true"></div>
|
<div class="organization-header" organization="organization" clickable="true"></div>
|
||||||
<div class="logs-view" organization="organization" performer="memberInfo" makevisible="organization && memberInfo && ready"></div>
|
<div class="logs-view" organization="organization" performer="memberInfo"
|
||||||
|
makevisible="organization && memberInfo && ready"></div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
@ -19,7 +19,7 @@
|
||||||
<ul class="nav nav-pills nav-stacked">
|
<ul class="nav nav-pills nav-stacked">
|
||||||
<li class="active"><a href="javascript:void(0)" data-toggle="tab" data-target="#permissions">Permissions</a></li>
|
<li 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()"
|
<li><a href="javascript:void(0)" data-toggle="tab" data-target="#trigger" ng-click="loadTriggers()"
|
||||||
quay-require="['BUILD_SUPPORT']">Build Triggers</a></li>
|
quay-show="Features.BUILD_SUPPORT">Build Triggers</a></li>
|
||||||
<li><a href="javascript:void(0)" data-toggle="tab" data-target="#badge">Status Badge</a></li>
|
<li><a href="javascript:void(0)" data-toggle="tab" data-target="#badge">Status Badge</a></li>
|
||||||
<li><a href="javascript:void(0)" data-toggle="tab" data-target="#notification" ng-click="loadNotifications()">Notifications</a></li>
|
<li><a href="javascript:void(0)" data-toggle="tab" data-target="#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>
|
||||||
|
@ -226,7 +226,7 @@
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Triggers tab -->
|
<!-- Triggers tab -->
|
||||||
<div id="trigger" class="tab-pane" quay-require="['BUILD_SUPPORT']">
|
<div id="trigger" class="tab-pane" quay-show="['BUILD_SUPPORT']">
|
||||||
<div class="panel panel-default">
|
<div class="panel panel-default">
|
||||||
<div class="panel-heading">Build Triggers
|
<div class="panel-heading">Build Triggers
|
||||||
<i class="info-icon fa fa-info-circle" data-placement="left" data-content="Triggers from various services (such as GitHub) which tell the repository to be built and updated."></i>
|
<i class="info-icon fa fa-info-circle" data-placement="left" data-content="Triggers from various services (such as GitHub) which tell the repository to be built and updated."></i>
|
||||||
|
@ -378,6 +378,12 @@
|
||||||
counter="showNewNotificationCounter"
|
counter="showNewNotificationCounter"
|
||||||
notification-created="handleNotificationCreated(notification)"></div>
|
notification-created="handleNotificationCreated(notification)"></div>
|
||||||
|
|
||||||
|
<!-- Manual trigger dialog -->
|
||||||
|
<div class="manual-trigger-build-dialog" repository="repo"
|
||||||
|
trigger="currentStartTrigger"
|
||||||
|
counter="showManualBuildDialog"
|
||||||
|
start-build="startTrigger(trigger, parameters)"></div>
|
||||||
|
|
||||||
<!-- Modal message dialog -->
|
<!-- Modal message dialog -->
|
||||||
<div class="modal fade" id="makepublicModal">
|
<div class="modal fade" id="makepublicModal">
|
||||||
<div class="modal-dialog">
|
<div class="modal-dialog">
|
||||||
|
|
|
@ -1,7 +1,7 @@
|
||||||
<div class="container signin-container">
|
<div class="container signin-container">
|
||||||
<div class="row">
|
<div class="row">
|
||||||
<div class="col-sm-6 col-sm-offset-3">
|
<div class="col-sm-6 col-sm-offset-3">
|
||||||
<div class="user-setup" redirect-url="'/'"></div>
|
<div class="user-setup" redirect-url="redirectUrl"></div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
@ -1,4 +1,4 @@
|
||||||
<div class="container" quay-show="Features.SUPER_USERS">
|
<div class="container" quay-show="Features.SUPER_USERS && showInterface">
|
||||||
<div class="alert alert-info">
|
<div class="alert alert-info">
|
||||||
This panel provides administrator access to <strong>super users of this installation of the registry</strong>. Super users can be managed in the configuration for this installation.
|
This panel provides administrator access to <strong>super users of this installation of the registry</strong>. Super users can be managed in the configuration for this installation.
|
||||||
</div>
|
</div>
|
||||||
|
@ -10,18 +10,64 @@
|
||||||
<li class="active">
|
<li class="active">
|
||||||
<a href="javascript:void(0)" data-toggle="tab" data-target="#users" ng-click="loadUsers()">Users</a>
|
<a href="javascript:void(0)" data-toggle="tab" data-target="#users" ng-click="loadUsers()">Users</a>
|
||||||
</li>
|
</li>
|
||||||
|
<li>
|
||||||
|
<a href="javascript:void(0)" data-toggle="tab" data-target="#create-user">Create User</a>
|
||||||
|
</li>
|
||||||
|
<li>
|
||||||
|
<a href="javascript:void(0)" data-toggle="tab" data-target="#logs" ng-click="loadLogs()">System Logs</a>
|
||||||
|
</li>
|
||||||
</ul>
|
</ul>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Content -->
|
<!-- Content -->
|
||||||
<div class="col-md-10">
|
<div class="col-md-10">
|
||||||
<div class="tab-content">
|
<div class="tab-content">
|
||||||
|
<!-- Logs tab -->
|
||||||
|
<div id="logs" class="tab-pane">
|
||||||
|
<div class="logsView" makevisible="logsCounter" all-logs="true"></div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Create user tab -->
|
||||||
|
<div id="create-user" class="tab-pane">
|
||||||
|
<span class="quay-spinner" ng-show="creatingUser"></span>
|
||||||
|
<form name="createUserForm" ng-submit="createUser()" ng-show="!creatingUser">
|
||||||
|
<div class="form-group">
|
||||||
|
<label>Username</label>
|
||||||
|
<input class="form-control" type="text" ng-model="newUser.username" ng-pattern="/^[a-z0-9_]{4,30}$/" required>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="form-group">
|
||||||
|
<label>Email address</label>
|
||||||
|
<input class="form-control" type="email" ng-model="newUser.email" required>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<button class="btn btn-primary" type="submit" ng-disabled="!createUserForm.$valid">Create User</button>
|
||||||
|
</form>
|
||||||
|
|
||||||
|
<div style="margin-top: 20px; padding-top: 20px; border-top: 1px solid #eee;" ng-show="createdUsers.length">
|
||||||
|
<table class="table">
|
||||||
|
<thead>
|
||||||
|
<th>Username</th>
|
||||||
|
<th>E-mail address</th>
|
||||||
|
<th>Temporary Password</th>
|
||||||
|
</thead>
|
||||||
|
|
||||||
|
<tr ng-repeat="created_user in createdUsers"
|
||||||
|
class="user-row">
|
||||||
|
<td>{{ created_user.username }}</td>
|
||||||
|
<td>{{ created_user.email }}</td>
|
||||||
|
<td>{{ created_user.password }}</td>
|
||||||
|
</tr>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
<!-- Users tab -->
|
<!-- Users tab -->
|
||||||
<div id="users" class="tab-pane active">
|
<div id="users" class="tab-pane active">
|
||||||
<div class="quay-spinner" ng-show="!users"></div>
|
<div class="quay-spinner" ng-show="!users"></div>
|
||||||
<div class="alert alert-error" ng-show="usersError">
|
<div class="alert alert-error" ng-show="usersError">
|
||||||
{{ usersError }}
|
{{ usersError }}
|
||||||
</div>
|
</div>
|
||||||
<div ng-show="users">
|
<div ng-show="users">
|
||||||
<div class="side-controls">
|
<div class="side-controls">
|
||||||
<div class="result-count">
|
<div class="result-count">
|
||||||
|
@ -37,8 +83,7 @@
|
||||||
<thead>
|
<thead>
|
||||||
<th>Username</th>
|
<th>Username</th>
|
||||||
<th>E-mail address</th>
|
<th>E-mail address</th>
|
||||||
<th></th>
|
<th style="width: 24px;"></th>
|
||||||
<th></th>
|
|
||||||
</thead>
|
</thead>
|
||||||
|
|
||||||
<tr ng-repeat="current_user in (users | filter:search | orderBy:'username' | limitTo:100)"
|
<tr ng-repeat="current_user in (users | filter:search | orderBy:'username' | limitTo:100)"
|
||||||
|
@ -51,19 +96,20 @@
|
||||||
<td>
|
<td>
|
||||||
<a href="mailto:{{ current_user.email }}">{{ current_user.email }}</a>
|
<a href="mailto:{{ current_user.email }}">{{ current_user.email }}</a>
|
||||||
</td>
|
</td>
|
||||||
<td class="user-class">
|
<td style="text-align: center;">
|
||||||
<span ng-if="current_user.super_user">Super user</span>
|
<i class="fa fa-ge fa-lg" ng-if="current_user.super_user" data-title="Super User" bs-tooltip></i>
|
||||||
</td>
|
<div class="dropdown" style="text-align: left;" ng-if="user.username != current_user.username && !current_user.super_user">
|
||||||
<td>
|
|
||||||
<div class="dropdown" ng-if="user.username != current_user.username && !user.super_user">
|
|
||||||
<button class="btn btn-default dropdown-toggle" data-toggle="dropdown">
|
<button class="btn btn-default dropdown-toggle" data-toggle="dropdown">
|
||||||
<i class="fa fa-ellipsis-h"></i>
|
<i class="caret"></i>
|
||||||
</button>
|
</button>
|
||||||
<ul class="dropdown-menu">
|
<ul class="dropdown-menu pull-right">
|
||||||
<li>
|
<li>
|
||||||
<a href="javascript:void(0)" ng-click="showChangePassword(current_user)">
|
<a href="javascript:void(0)" ng-click="showChangePassword(current_user)">
|
||||||
<i class="fa fa-key"></i> Change Password
|
<i class="fa fa-key"></i> Change Password
|
||||||
</a>
|
</a>
|
||||||
|
<a href="javascript:void(0)" ng-click="sendRecoveryEmail(current_user)" quay-show="Features.MAILING">
|
||||||
|
<i class="fa fa-envelope"></i> Send Recovery Email
|
||||||
|
</a>
|
||||||
<a href="javascript:void(0)" ng-click="showDeleteUser(current_user)">
|
<a href="javascript:void(0)" ng-click="showDeleteUser(current_user)">
|
||||||
<i class="fa fa-times"></i> Delete User
|
<i class="fa fa-times"></i> Delete User
|
||||||
</a>
|
</a>
|
||||||
|
|
|
@ -1,40 +1,92 @@
|
||||||
<div class="resource-view" resource="orgResource" error-message="'No matching organization'">
|
<div class="resource-view" resource="orgResource" error-message="'No matching organization'">
|
||||||
<div class="team-view container">
|
<div class="team-view container">
|
||||||
<div class="organization-header" organization="organization" team-name="teamname"></div>
|
<div class="organization-header" organization="organization" team-name="teamname">
|
||||||
|
<div ng-show="canEditMembers" class="side-controls">
|
||||||
|
<div class="hidden-sm hidden-xs">
|
||||||
|
<button class="btn btn-success"
|
||||||
|
id="showAddMember"
|
||||||
|
data-title="Add Team Member"
|
||||||
|
data-content-template="/static/directives/team-view-add.html"
|
||||||
|
data-placement="bottom-right"
|
||||||
|
bs-popover="bs-popover">
|
||||||
|
<i class="fa fa-plus"></i>
|
||||||
|
Add Team Member
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
<div class="resource-view" resource="membersResource" error-message="'No matching team found'">
|
<div class="resource-view" resource="membersResource" error-message="'No matching team found'">
|
||||||
<div class="description markdown-input" content="team.description" can-write="organization.is_admin"
|
<div class="description markdown-input" content="team.description" can-write="organization.is_admin"
|
||||||
content-changed="updateForDescription" field-title="'team description'"></div>
|
content-changed="updateForDescription" field-title="'team description'"></div>
|
||||||
|
|
||||||
<div class="panel panel-default">
|
<div class="empty-message" ng-if="!members.length">
|
||||||
<div class="panel-heading">Team Members
|
This team has no members
|
||||||
<i class="info-icon fa fa-info-circle" data-placement="left" data-content="Users that inherit all permissions delegated to this team"></i>
|
</div>
|
||||||
</div>
|
|
||||||
<div class="panel-body">
|
<div class="empty-message" ng-if="members.length && !(members | filter:search).length">
|
||||||
<table class="permissions">
|
No matching team members found
|
||||||
<tr ng-repeat="(name, member) in members">
|
</div>
|
||||||
<td class="user entity">
|
|
||||||
<span class="entity-reference" entity="member" namespace="organization.name"></span>
|
<table class="member-listing" style="margin-top: -20px" ng-show="members.length">
|
||||||
</td>
|
<!-- Members -->
|
||||||
<td>
|
<tr ng-if="(members | filter:search | filter: filterFunction(false, false)).length">
|
||||||
<span class="delete-ui" delete-title="'Remove User From Team'" button-title="'Remove'"
|
<td colspan="2"><div class="section-header">Team Members</div></td>
|
||||||
perform-delete="removeMember(member.name)" ng-if="canEditMembers"></span>
|
</tr>
|
||||||
</td>
|
|
||||||
</tr>
|
<tr ng-repeat="member in members | filter:search | filter: filterFunction(false, false) | orderBy: 'name'">
|
||||||
|
<td class="user entity">
|
||||||
<tr ng-show="canEditMembers">
|
<span class="entity-reference" entity="member" namespace="organization.name" show-gravatar="true" gravatar-size="32"></span>
|
||||||
<td colspan="3">
|
</td>
|
||||||
<div class="entity-search" style="width: 100%"
|
<td>
|
||||||
namespace="orgname" placeholder="'Add a registered user or robot...'"
|
<span class="delete-ui" delete-title="'Remove ' + member.name + ' from team'" button-title="'Remove'"
|
||||||
entity-selected="addNewMember(entity)"
|
perform-delete="removeMember(member.name)" ng-if="canEditMembers"></span>
|
||||||
current-entity="selectedMember"
|
</td>
|
||||||
auto-clear="true"
|
</tr>
|
||||||
allowed-entities="['user', 'robot']"></div>
|
|
||||||
</td>
|
<!-- Robots -->
|
||||||
</tr>
|
<tr ng-if="(members | filter:search | filter: filterFunction(false, true)).length">
|
||||||
</table>
|
<td colspan="2"><div class="section-header">Robot Accounts</div></td>
|
||||||
|
</tr>
|
||||||
|
|
||||||
|
<tr ng-repeat="member in members | filter:search | filter: filterFunction(false, true) | orderBy: 'name'">
|
||||||
|
<td class="user entity">
|
||||||
|
<span class="entity-reference" entity="member" namespace="organization.name"></span>
|
||||||
|
</td>
|
||||||
|
<td>
|
||||||
|
<span class="delete-ui" delete-title="'Remove ' + member.name + ' from team'" button-title="'Remove'"
|
||||||
|
perform-delete="removeMember(member.name)" ng-if="canEditMembers"></span>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
|
||||||
|
<!-- Invited -->
|
||||||
|
<tr ng-if="(members | filter:search | filter: filterFunction(true, false)).length">
|
||||||
|
<td colspan="2"><div class="section-header">Invited To Join</div></td>
|
||||||
|
</tr>
|
||||||
|
|
||||||
|
<tr ng-repeat="member in members | filter:search | filter: filterFunction(true, false) | orderBy: 'name'">
|
||||||
|
<td class="user entity">
|
||||||
|
<span ng-if="member.kind != 'invite'">
|
||||||
|
<span class="entity-reference" entity="member" namespace="organization.name" show-gravatar="true" gravatar-size="32"></span>
|
||||||
|
</span>
|
||||||
|
<span class="invite-listing" ng-if="member.kind == 'invite'">
|
||||||
|
<img class="gravatar"ng-src="//www.gravatar.com/avatar/{{ member.gravatar }}?s=32&d=identicon">
|
||||||
|
{{ member.email }}
|
||||||
|
</span>
|
||||||
|
</td>
|
||||||
|
<td>
|
||||||
|
<span class="delete-ui" delete-title="'Revoke invite to join team'" button-title="'Revoke'"
|
||||||
|
perform-delete="revokeInvite(member)" ng-if="canEditMembers"></span>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
</table>
|
||||||
|
|
||||||
|
<div ng-show="canEditMembers">
|
||||||
|
<div ng-if-media="'(max-width: 560px)'">
|
||||||
|
<div ng-include="'/static/directives/team-view-add.html'"></div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
@ -122,7 +122,7 @@
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="panel" ng-show="!updatingUser" >
|
<div class="panel" ng-show="!updatingUser" quay-show="Features.MAILING">
|
||||||
<div class="panel-title">Change e-mail address</div>
|
<div class="panel-title">Change e-mail address</div>
|
||||||
|
|
||||||
<div class="panel-body">
|
<div class="panel-body">
|
||||||
|
|
|
@ -8,17 +8,19 @@
|
||||||
<div class="container">
|
<div class="container">
|
||||||
<div class="row">
|
<div class="row">
|
||||||
<div class="col-md-12">
|
<div class="col-md-12">
|
||||||
<h2>There was an error logging in with {{ service_name }}.</h2>
|
<h2 style="margin-bottom: 20px;">There was an error logging in with {{ service_name }}.</h2>
|
||||||
|
|
||||||
{% if error_message %}
|
{% if error_message %}
|
||||||
<div class="alert alert-danger">{{ error_message }}</div>
|
<div class="alert alert-danger">{{ error_message }}</div>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
|
|
||||||
|
{% if user_creation %}
|
||||||
<div>
|
<div>
|
||||||
Please register using the <a ng-href="{{ service_url }}/signin" target="_self">registration form</a> to continue.
|
Please register using the <a ng-href="{{ service_url }}/signin" target="_self">registration form</a> to continue.
|
||||||
You will be able to connect your account to your Quay.io account
|
You will be able to connect your account to your Quay.io account
|
||||||
in the user settings.
|
in the user settings.
|
||||||
</div>
|
</div>
|
||||||
|
{% endif %}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|
Binary file not shown.
|
@ -1,14 +1,16 @@
|
||||||
import unittest
|
import unittest
|
||||||
import json
|
import json
|
||||||
|
import datetime
|
||||||
|
|
||||||
from urllib import urlencode
|
from urllib import urlencode
|
||||||
from urlparse import urlparse, urlunparse, parse_qs
|
from urlparse import urlparse, urlunparse, parse_qs
|
||||||
|
|
||||||
from app import app
|
from app import app
|
||||||
|
from data import model
|
||||||
from initdb import setup_database_for_testing, finished_database_for_testing
|
from initdb import setup_database_for_testing, finished_database_for_testing
|
||||||
from endpoints.api import api_bp, api
|
from endpoints.api import api_bp, api
|
||||||
|
|
||||||
from endpoints.api.team import TeamMember, TeamMemberList, OrganizationTeam
|
from endpoints.api.team import TeamMember, TeamMemberList, OrganizationTeam, TeamMemberInvite
|
||||||
from endpoints.api.tag import RepositoryTagImages, RepositoryTag
|
from endpoints.api.tag import RepositoryTagImages, RepositoryTag
|
||||||
from endpoints.api.search import FindRepositories, EntitySearch
|
from endpoints.api.search import FindRepositories, EntitySearch
|
||||||
from endpoints.api.image import RepositoryImageChanges, RepositoryImage, RepositoryImageList
|
from endpoints.api.image import RepositoryImageChanges, RepositoryImage, RepositoryImageList
|
||||||
|
@ -19,7 +21,7 @@ from endpoints.api.robot import (UserRobotList, OrgRobot, OrgRobotList, UserRobo
|
||||||
|
|
||||||
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, BuildTriggerFieldValues)
|
||||||
from endpoints.api.repoemail import RepositoryAuthorizedEmail
|
from endpoints.api.repoemail import RepositoryAuthorizedEmail
|
||||||
from endpoints.api.repositorynotification import RepositoryNotification, RepositoryNotificationList
|
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,
|
||||||
|
@ -40,7 +42,8 @@ from endpoints.api.repository import RepositoryList, RepositoryVisibility, Repos
|
||||||
from endpoints.api.permission import (RepositoryUserPermission, RepositoryTeamPermission,
|
from endpoints.api.permission import (RepositoryUserPermission, RepositoryTeamPermission,
|
||||||
RepositoryTeamPermissionList, RepositoryUserPermissionList)
|
RepositoryTeamPermissionList, RepositoryUserPermissionList)
|
||||||
|
|
||||||
from endpoints.api.superuser import SuperUserLogs, SuperUserList, SuperUserManagement
|
from endpoints.api.superuser import (SuperUserLogs, SuperUserList, SuperUserManagement,
|
||||||
|
SuperUserSendRecoveryEmail)
|
||||||
|
|
||||||
|
|
||||||
try:
|
try:
|
||||||
|
@ -75,7 +78,9 @@ class ApiTestCase(unittest.TestCase):
|
||||||
|
|
||||||
with client.session_transaction() as sess:
|
with client.session_transaction() as sess:
|
||||||
if auth_username:
|
if auth_username:
|
||||||
sess['user_id'] = auth_username
|
loaded = model.get_user(auth_username)
|
||||||
|
sess['user_id'] = loaded.id
|
||||||
|
sess['login_time'] = datetime.datetime.now()
|
||||||
sess[CSRF_TOKEN_KEY] = CSRF_TOKEN
|
sess[CSRF_TOKEN_KEY] = CSRF_TOKEN
|
||||||
|
|
||||||
# Restore the teardown functions
|
# Restore the teardown functions
|
||||||
|
@ -510,13 +515,13 @@ class TestUser(ApiTestCase):
|
||||||
self._run_test('PUT', 401, None, {})
|
self._run_test('PUT', 401, None, {})
|
||||||
|
|
||||||
def test_put_freshuser(self):
|
def test_put_freshuser(self):
|
||||||
self._run_test('PUT', 401, 'freshuser', {})
|
self._run_test('PUT', 200, 'freshuser', {})
|
||||||
|
|
||||||
def test_put_reader(self):
|
def test_put_reader(self):
|
||||||
self._run_test('PUT', 401, 'reader', {})
|
self._run_test('PUT', 200, 'reader', {})
|
||||||
|
|
||||||
def test_put_devtable(self):
|
def test_put_devtable(self):
|
||||||
self._run_test('PUT', 401, 'devtable', {})
|
self._run_test('PUT', 200, 'devtable', {})
|
||||||
|
|
||||||
def test_post_anonymous(self):
|
def test_post_anonymous(self):
|
||||||
self._run_test('POST', 400, None, {u'username': 'T946', u'password': '0SG4', u'email': 'MENT'})
|
self._run_test('POST', 400, None, {u'username': 'T946', u'password': '0SG4', u'email': 'MENT'})
|
||||||
|
@ -1061,6 +1066,62 @@ class TestBuildTriggerActivateSwo1BuynlargeOrgrepo(ApiTestCase):
|
||||||
def test_post_devtable(self):
|
def test_post_devtable(self):
|
||||||
self._run_test('POST', 404, 'devtable', {'config': {}})
|
self._run_test('POST', 404, 'devtable', {'config': {}})
|
||||||
|
|
||||||
|
class TestBuildTriggerFieldValuesSwo1PublicPublicrepo(ApiTestCase):
|
||||||
|
def setUp(self):
|
||||||
|
ApiTestCase.setUp(self)
|
||||||
|
self._set_url(BuildTriggerFieldValues, trigger_uuid="SWO1", repository="public/publicrepo",
|
||||||
|
field_name="test_field")
|
||||||
|
|
||||||
|
def test_get_anonymous(self):
|
||||||
|
self._run_test('GET', 401, None, {})
|
||||||
|
|
||||||
|
def test_get_freshuser(self):
|
||||||
|
self._run_test('GET', 403, 'freshuser', {})
|
||||||
|
|
||||||
|
def test_get_reader(self):
|
||||||
|
self._run_test('GET', 403, 'reader', {})
|
||||||
|
|
||||||
|
def test_get_devtable(self):
|
||||||
|
self._run_test('GET', 403, 'devtable', {})
|
||||||
|
|
||||||
|
|
||||||
|
class TestBuildTriggerFieldValuesSwo1DevtableShared(ApiTestCase):
|
||||||
|
def setUp(self):
|
||||||
|
ApiTestCase.setUp(self)
|
||||||
|
self._set_url(BuildTriggerFieldValues, trigger_uuid="SWO1", repository="devtable/shared",
|
||||||
|
field_name="test_field")
|
||||||
|
|
||||||
|
def test_get_anonymous(self):
|
||||||
|
self._run_test('GET', 401, None, {})
|
||||||
|
|
||||||
|
def test_get_freshuser(self):
|
||||||
|
self._run_test('GET', 403, 'freshuser', {})
|
||||||
|
|
||||||
|
def test_get_reader(self):
|
||||||
|
self._run_test('GET', 403, 'reader', {})
|
||||||
|
|
||||||
|
def test_get_devtable(self):
|
||||||
|
self._run_test('GET', 404, 'devtable', {'config': {}})
|
||||||
|
|
||||||
|
|
||||||
|
class TestBuildTriggerFieldValuesSwo1BuynlargeOrgrepo(ApiTestCase):
|
||||||
|
def setUp(self):
|
||||||
|
ApiTestCase.setUp(self)
|
||||||
|
self._set_url(BuildTriggerFieldValues, trigger_uuid="SWO1", repository="buynlarge/orgrepo",
|
||||||
|
field_name="test_field")
|
||||||
|
|
||||||
|
def test_get_anonymous(self):
|
||||||
|
self._run_test('GET', 401, None, {})
|
||||||
|
|
||||||
|
def test_get_freshuser(self):
|
||||||
|
self._run_test('GET', 403, 'freshuser', {})
|
||||||
|
|
||||||
|
def test_get_reader(self):
|
||||||
|
self._run_test('GET', 403, 'reader', {})
|
||||||
|
|
||||||
|
def test_get_devtable(self):
|
||||||
|
self._run_test('GET', 404, 'devtable', {'config': {}})
|
||||||
|
|
||||||
|
|
||||||
class TestBuildTriggerSources831cPublicPublicrepo(ApiTestCase):
|
class TestBuildTriggerSources831cPublicPublicrepo(ApiTestCase):
|
||||||
def setUp(self):
|
def setUp(self):
|
||||||
|
@ -1292,7 +1353,7 @@ class TestActivateBuildTrigger0byeDevtableShared(ApiTestCase):
|
||||||
self._run_test('POST', 403, 'reader', None)
|
self._run_test('POST', 403, 'reader', None)
|
||||||
|
|
||||||
def test_post_devtable(self):
|
def test_post_devtable(self):
|
||||||
self._run_test('POST', 404, 'devtable', None)
|
self._run_test('POST', 404, 'devtable', {})
|
||||||
|
|
||||||
|
|
||||||
class TestActivateBuildTrigger0byeBuynlargeOrgrepo(ApiTestCase):
|
class TestActivateBuildTrigger0byeBuynlargeOrgrepo(ApiTestCase):
|
||||||
|
@ -1310,7 +1371,7 @@ class TestActivateBuildTrigger0byeBuynlargeOrgrepo(ApiTestCase):
|
||||||
self._run_test('POST', 403, 'reader', None)
|
self._run_test('POST', 403, 'reader', None)
|
||||||
|
|
||||||
def test_post_devtable(self):
|
def test_post_devtable(self):
|
||||||
self._run_test('POST', 404, 'devtable', None)
|
self._run_test('POST', 404, 'devtable', {})
|
||||||
|
|
||||||
|
|
||||||
class TestBuildTriggerAnalyze0byePublicPublicrepo(ApiTestCase):
|
class TestBuildTriggerAnalyze0byePublicPublicrepo(ApiTestCase):
|
||||||
|
@ -3527,13 +3588,61 @@ class TestSuperUserLogs(ApiTestCase):
|
||||||
self._run_test('GET', 200, 'devtable', None)
|
self._run_test('GET', 200, 'devtable', None)
|
||||||
|
|
||||||
|
|
||||||
|
class TestSuperUserSendRecoveryEmail(ApiTestCase):
|
||||||
|
def setUp(self):
|
||||||
|
ApiTestCase.setUp(self)
|
||||||
|
self._set_url(SuperUserSendRecoveryEmail, username='someuser')
|
||||||
|
|
||||||
|
def test_post_anonymous(self):
|
||||||
|
self._run_test('POST', 401, None, None)
|
||||||
|
|
||||||
|
def test_post_freshuser(self):
|
||||||
|
self._run_test('POST', 403, 'freshuser', None)
|
||||||
|
|
||||||
|
def test_post_reader(self):
|
||||||
|
self._run_test('POST', 403, 'reader', None)
|
||||||
|
|
||||||
|
def test_post_devtable(self):
|
||||||
|
self._run_test('POST', 404, 'devtable', None)
|
||||||
|
|
||||||
|
|
||||||
|
class TestTeamMemberInvite(ApiTestCase):
|
||||||
|
def setUp(self):
|
||||||
|
ApiTestCase.setUp(self)
|
||||||
|
self._set_url(TeamMemberInvite, code='foobarbaz')
|
||||||
|
|
||||||
|
def test_put_anonymous(self):
|
||||||
|
self._run_test('PUT', 401, None, None)
|
||||||
|
|
||||||
|
def test_put_freshuser(self):
|
||||||
|
self._run_test('PUT', 400, 'freshuser', None)
|
||||||
|
|
||||||
|
def test_put_reader(self):
|
||||||
|
self._run_test('PUT', 400, 'reader', None)
|
||||||
|
|
||||||
|
def test_put_devtable(self):
|
||||||
|
self._run_test('PUT', 400, 'devtable', None)
|
||||||
|
|
||||||
|
def test_delete_anonymous(self):
|
||||||
|
self._run_test('DELETE', 401, None, None)
|
||||||
|
|
||||||
|
def test_delete_freshuser(self):
|
||||||
|
self._run_test('DELETE', 400, 'freshuser', None)
|
||||||
|
|
||||||
|
def test_delete_reader(self):
|
||||||
|
self._run_test('DELETE', 400, 'reader', None)
|
||||||
|
|
||||||
|
def test_delete_devtable(self):
|
||||||
|
self._run_test('DELETE', 400, 'devtable', None)
|
||||||
|
|
||||||
|
|
||||||
class TestSuperUserList(ApiTestCase):
|
class TestSuperUserList(ApiTestCase):
|
||||||
def setUp(self):
|
def setUp(self):
|
||||||
ApiTestCase.setUp(self)
|
ApiTestCase.setUp(self)
|
||||||
self._set_url(SuperUserList)
|
self._set_url(SuperUserList)
|
||||||
|
|
||||||
def test_get_anonymous(self):
|
def test_get_anonymous(self):
|
||||||
self._run_test('GET', 403, None, None)
|
self._run_test('GET', 401, None, None)
|
||||||
|
|
||||||
def test_get_freshuser(self):
|
def test_get_freshuser(self):
|
||||||
self._run_test('GET', 403, 'freshuser', None)
|
self._run_test('GET', 403, 'freshuser', None)
|
||||||
|
@ -3545,14 +3654,13 @@ class TestSuperUserList(ApiTestCase):
|
||||||
self._run_test('GET', 200, 'devtable', None)
|
self._run_test('GET', 200, 'devtable', None)
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
class TestSuperUserManagement(ApiTestCase):
|
class TestSuperUserManagement(ApiTestCase):
|
||||||
def setUp(self):
|
def setUp(self):
|
||||||
ApiTestCase.setUp(self)
|
ApiTestCase.setUp(self)
|
||||||
self._set_url(SuperUserManagement, username='freshuser')
|
self._set_url(SuperUserManagement, username='freshuser')
|
||||||
|
|
||||||
def test_get_anonymous(self):
|
def test_get_anonymous(self):
|
||||||
self._run_test('GET', 403, None, None)
|
self._run_test('GET', 401, None, None)
|
||||||
|
|
||||||
def test_get_freshuser(self):
|
def test_get_freshuser(self):
|
||||||
self._run_test('GET', 403, 'freshuser', None)
|
self._run_test('GET', 403, 'freshuser', None)
|
||||||
|
@ -3565,7 +3673,7 @@ class TestSuperUserManagement(ApiTestCase):
|
||||||
|
|
||||||
|
|
||||||
def test_put_anonymous(self):
|
def test_put_anonymous(self):
|
||||||
self._run_test('PUT', 403, None, {})
|
self._run_test('PUT', 401, None, {})
|
||||||
|
|
||||||
def test_put_freshuser(self):
|
def test_put_freshuser(self):
|
||||||
self._run_test('PUT', 403, 'freshuser', {})
|
self._run_test('PUT', 403, 'freshuser', {})
|
||||||
|
@ -3578,7 +3686,7 @@ class TestSuperUserManagement(ApiTestCase):
|
||||||
|
|
||||||
|
|
||||||
def test_delete_anonymous(self):
|
def test_delete_anonymous(self):
|
||||||
self._run_test('DELETE', 403, None, None)
|
self._run_test('DELETE', 401, None, None)
|
||||||
|
|
||||||
def test_delete_freshuser(self):
|
def test_delete_freshuser(self):
|
||||||
self._run_test('DELETE', 403, 'freshuser', None)
|
self._run_test('DELETE', 403, 'freshuser', None)
|
||||||
|
|
|
@ -1,3 +1,5 @@
|
||||||
|
# coding=utf-8
|
||||||
|
|
||||||
import unittest
|
import unittest
|
||||||
import json as py_json
|
import json as py_json
|
||||||
|
|
||||||
|
@ -11,7 +13,7 @@ from app import app
|
||||||
from initdb import setup_database_for_testing, finished_database_for_testing
|
from initdb import setup_database_for_testing, finished_database_for_testing
|
||||||
from data import model, database
|
from data import model, database
|
||||||
|
|
||||||
from endpoints.api.team import TeamMember, TeamMemberList, OrganizationTeam
|
from endpoints.api.team import TeamMember, TeamMemberList, TeamMemberInvite, OrganizationTeam
|
||||||
from endpoints.api.tag import RepositoryTagImages, RepositoryTag
|
from endpoints.api.tag import RepositoryTagImages, RepositoryTag
|
||||||
from endpoints.api.search import FindRepositories, EntitySearch
|
from endpoints.api.search import FindRepositories, EntitySearch
|
||||||
from endpoints.api.image import RepositoryImage, RepositoryImageList
|
from endpoints.api.image import RepositoryImage, RepositoryImageList
|
||||||
|
@ -20,7 +22,7 @@ from endpoints.api.robot import (UserRobotList, OrgRobot, OrgRobotList, UserRobo
|
||||||
RegenerateUserRobot, RegenerateOrgRobot)
|
RegenerateUserRobot, RegenerateOrgRobot)
|
||||||
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, BuildTriggerFieldValues)
|
||||||
from endpoints.api.repoemail import RepositoryAuthorizedEmail
|
from endpoints.api.repoemail import RepositoryAuthorizedEmail
|
||||||
from endpoints.api.repositorynotification import RepositoryNotification, RepositoryNotificationList
|
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,
|
||||||
|
@ -131,6 +133,10 @@ class ApiTestCase(unittest.TestCase):
|
||||||
|
|
||||||
def deleteResponse(self, resource_name, params={}, expected_code=204):
|
def deleteResponse(self, resource_name, params={}, expected_code=204):
|
||||||
rv = self.app.delete(self.url_for(resource_name, params))
|
rv = self.app.delete(self.url_for(resource_name, params))
|
||||||
|
|
||||||
|
if rv.status_code != expected_code:
|
||||||
|
print 'Mismatch data for resource DELETE %s: %s' % (resource_name, rv.data)
|
||||||
|
|
||||||
self.assertEquals(rv.status_code, expected_code)
|
self.assertEquals(rv.status_code, expected_code)
|
||||||
return rv.data
|
return rv.data
|
||||||
|
|
||||||
|
@ -162,6 +168,13 @@ class ApiTestCase(unittest.TestCase):
|
||||||
parsed = py_json.loads(data)
|
parsed = py_json.loads(data)
|
||||||
return parsed
|
return parsed
|
||||||
|
|
||||||
|
def assertInTeam(self, data, membername):
|
||||||
|
for memberData in data['members']:
|
||||||
|
if memberData['name'] == membername:
|
||||||
|
return
|
||||||
|
|
||||||
|
self.fail(membername + ' not found in team: ' + py_json.dumps(data))
|
||||||
|
|
||||||
def login(self, username, password='password'):
|
def login(self, username, password='password'):
|
||||||
return self.postJsonResponse(Signin, data=dict(username=username, password=password))
|
return self.postJsonResponse(Signin, data=dict(username=username, password=password))
|
||||||
|
|
||||||
|
@ -328,6 +341,12 @@ class TestChangeUserDetails(ApiTestCase):
|
||||||
data=dict(password='newpasswordiscool'))
|
data=dict(password='newpasswordiscool'))
|
||||||
self.login(READ_ACCESS_USER, password='newpasswordiscool')
|
self.login(READ_ACCESS_USER, password='newpasswordiscool')
|
||||||
|
|
||||||
|
def test_changepassword_unicode(self):
|
||||||
|
self.login(READ_ACCESS_USER)
|
||||||
|
self.putJsonResponse(User,
|
||||||
|
data=dict(password=u'someunicode北京市pass'))
|
||||||
|
self.login(READ_ACCESS_USER, password=u'someunicode北京市pass')
|
||||||
|
|
||||||
def test_changeeemail(self):
|
def test_changeeemail(self):
|
||||||
self.login(READ_ACCESS_USER)
|
self.login(READ_ACCESS_USER)
|
||||||
|
|
||||||
|
@ -375,10 +394,30 @@ class TestCreateNewUser(ApiTestCase):
|
||||||
self.assertEquals('Invalid username auserName: Username must match expression [a-z0-9_]+', json['error_description'])
|
self.assertEquals('Invalid username auserName: Username must match expression [a-z0-9_]+', json['error_description'])
|
||||||
|
|
||||||
def test_createuser(self):
|
def test_createuser(self):
|
||||||
data = self.postResponse(User,
|
data = self.postJsonResponse(User,
|
||||||
data=NEW_USER_DETAILS,
|
data=NEW_USER_DETAILS,
|
||||||
expected_code=201)
|
expected_code=200)
|
||||||
self.assertEquals('"Created"', data)
|
self.assertEquals(True, data['awaiting_verification'])
|
||||||
|
|
||||||
|
def test_createuser_withteaminvite(self):
|
||||||
|
inviter = model.get_user(ADMIN_ACCESS_USER)
|
||||||
|
team = model.get_organization_team(ORGANIZATION, 'owners')
|
||||||
|
invite = model.add_or_invite_to_team(inviter, team, None, 'foo@example.com')
|
||||||
|
|
||||||
|
details = {
|
||||||
|
'invite_code': invite.invite_token
|
||||||
|
}
|
||||||
|
details.update(NEW_USER_DETAILS);
|
||||||
|
|
||||||
|
data = self.postJsonResponse(User, data=details, expected_code=200)
|
||||||
|
self.assertEquals(True, data['awaiting_verification'])
|
||||||
|
|
||||||
|
# Make sure the user was added to the team.
|
||||||
|
self.login(ADMIN_ACCESS_USER)
|
||||||
|
json = self.getJsonResponse(TeamMemberList,
|
||||||
|
params=dict(orgname=ORGANIZATION,
|
||||||
|
teamname='owners'))
|
||||||
|
self.assertInTeam(json, NEW_USER_DETAILS['username'])
|
||||||
|
|
||||||
|
|
||||||
class TestSignout(ApiTestCase):
|
class TestSignout(ApiTestCase):
|
||||||
|
@ -741,16 +780,43 @@ class TestGetOrganizationTeamMembers(ApiTestCase):
|
||||||
params=dict(orgname=ORGANIZATION,
|
params=dict(orgname=ORGANIZATION,
|
||||||
teamname='readers'))
|
teamname='readers'))
|
||||||
|
|
||||||
assert READ_ACCESS_USER in json['members']
|
self.assertEquals(READ_ACCESS_USER, json['members'][1]['name'])
|
||||||
|
|
||||||
|
|
||||||
class TestUpdateOrganizationTeamMember(ApiTestCase):
|
class TestUpdateOrganizationTeamMember(ApiTestCase):
|
||||||
def test_addmember(self):
|
def test_addmember_alreadyteammember(self):
|
||||||
self.login(ADMIN_ACCESS_USER)
|
self.login(ADMIN_ACCESS_USER)
|
||||||
|
|
||||||
|
membername = READ_ACCESS_USER
|
||||||
|
self.putResponse(TeamMember,
|
||||||
|
params=dict(orgname=ORGANIZATION, teamname='readers',
|
||||||
|
membername=membername),
|
||||||
|
expected_code=400)
|
||||||
|
|
||||||
|
|
||||||
|
def test_addmember_orgmember(self):
|
||||||
|
self.login(ADMIN_ACCESS_USER)
|
||||||
|
|
||||||
|
membername = READ_ACCESS_USER
|
||||||
|
self.putJsonResponse(TeamMember,
|
||||||
|
params=dict(orgname=ORGANIZATION, teamname='owners',
|
||||||
|
membername=membername))
|
||||||
|
|
||||||
|
# Verify the user was added to the team.
|
||||||
|
json = self.getJsonResponse(TeamMemberList,
|
||||||
|
params=dict(orgname=ORGANIZATION,
|
||||||
|
teamname='owners'))
|
||||||
|
|
||||||
|
self.assertInTeam(json, membername)
|
||||||
|
|
||||||
|
|
||||||
|
def test_addmember_robot(self):
|
||||||
|
self.login(ADMIN_ACCESS_USER)
|
||||||
|
|
||||||
|
membername = ORGANIZATION + '+coolrobot'
|
||||||
self.putJsonResponse(TeamMember,
|
self.putJsonResponse(TeamMember,
|
||||||
params=dict(orgname=ORGANIZATION, teamname='readers',
|
params=dict(orgname=ORGANIZATION, teamname='readers',
|
||||||
membername=NO_ACCESS_USER))
|
membername=membername))
|
||||||
|
|
||||||
|
|
||||||
# Verify the user was added to the team.
|
# Verify the user was added to the team.
|
||||||
|
@ -758,10 +824,168 @@ class TestUpdateOrganizationTeamMember(ApiTestCase):
|
||||||
params=dict(orgname=ORGANIZATION,
|
params=dict(orgname=ORGANIZATION,
|
||||||
teamname='readers'))
|
teamname='readers'))
|
||||||
|
|
||||||
assert NO_ACCESS_USER in json['members']
|
self.assertInTeam(json, membername)
|
||||||
|
|
||||||
|
|
||||||
|
def test_addmember_invalidrobot(self):
|
||||||
|
self.login(ADMIN_ACCESS_USER)
|
||||||
|
|
||||||
|
membername = 'freshuser+anotherrobot'
|
||||||
|
self.putResponse(TeamMember,
|
||||||
|
params=dict(orgname=ORGANIZATION, teamname='readers',
|
||||||
|
membername=membername),
|
||||||
|
expected_code=400)
|
||||||
|
|
||||||
|
|
||||||
|
def test_addmember_nonorgmember(self):
|
||||||
|
self.login(ADMIN_ACCESS_USER)
|
||||||
|
|
||||||
|
membername = NO_ACCESS_USER
|
||||||
|
response = self.putJsonResponse(TeamMember,
|
||||||
|
params=dict(orgname=ORGANIZATION, teamname='owners',
|
||||||
|
membername=membername))
|
||||||
|
|
||||||
|
|
||||||
|
self.assertEquals(True, response['invited'])
|
||||||
|
|
||||||
|
# Make sure the user is not (yet) part of the team.
|
||||||
|
json = self.getJsonResponse(TeamMemberList,
|
||||||
|
params=dict(orgname=ORGANIZATION,
|
||||||
|
teamname='readers'))
|
||||||
|
|
||||||
|
for member in json['members']:
|
||||||
|
self.assertNotEqual(membername, member['name'])
|
||||||
|
|
||||||
|
|
||||||
|
class TestAcceptTeamMemberInvite(ApiTestCase):
|
||||||
|
def assertInTeam(self, data, membername):
|
||||||
|
for memberData in data['members']:
|
||||||
|
if memberData['name'] == membername:
|
||||||
|
return
|
||||||
|
|
||||||
|
self.fail(membername + ' not found in team: ' + json.dumps(data))
|
||||||
|
|
||||||
|
def test_accept(self):
|
||||||
|
self.login(ADMIN_ACCESS_USER)
|
||||||
|
|
||||||
|
# Create the invite.
|
||||||
|
membername = NO_ACCESS_USER
|
||||||
|
response = self.putJsonResponse(TeamMember,
|
||||||
|
params=dict(orgname=ORGANIZATION, teamname='owners',
|
||||||
|
membername=membername))
|
||||||
|
|
||||||
|
self.assertEquals(True, response['invited'])
|
||||||
|
|
||||||
|
# Login as the user.
|
||||||
|
self.login(membername)
|
||||||
|
|
||||||
|
# Accept the invite.
|
||||||
|
user = model.get_user(membername)
|
||||||
|
invites = list(model.lookup_team_invites(user))
|
||||||
|
self.assertEquals(1, len(invites))
|
||||||
|
|
||||||
|
self.putJsonResponse(TeamMemberInvite,
|
||||||
|
params=dict(code=invites[0].invite_token))
|
||||||
|
|
||||||
|
# Verify the user is now on the team.
|
||||||
|
json = self.getJsonResponse(TeamMemberList,
|
||||||
|
params=dict(orgname=ORGANIZATION,
|
||||||
|
teamname='owners'))
|
||||||
|
|
||||||
|
self.assertInTeam(json, membername)
|
||||||
|
|
||||||
|
# Verify the accept now fails.
|
||||||
|
self.putResponse(TeamMemberInvite,
|
||||||
|
params=dict(code=invites[0].invite_token),
|
||||||
|
expected_code=400)
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
class TestDeclineTeamMemberInvite(ApiTestCase):
|
||||||
|
def test_decline_wronguser(self):
|
||||||
|
self.login(ADMIN_ACCESS_USER)
|
||||||
|
|
||||||
|
# Create the invite.
|
||||||
|
membername = NO_ACCESS_USER
|
||||||
|
response = self.putJsonResponse(TeamMember,
|
||||||
|
params=dict(orgname=ORGANIZATION, teamname='owners',
|
||||||
|
membername=membername))
|
||||||
|
|
||||||
|
self.assertEquals(True, response['invited'])
|
||||||
|
|
||||||
|
# Try to decline the invite.
|
||||||
|
user = model.get_user(membername)
|
||||||
|
invites = list(model.lookup_team_invites(user))
|
||||||
|
self.assertEquals(1, len(invites))
|
||||||
|
|
||||||
|
self.deleteResponse(TeamMemberInvite,
|
||||||
|
params=dict(code=invites[0].invite_token),
|
||||||
|
expected_code=400)
|
||||||
|
|
||||||
|
|
||||||
|
def test_decline(self):
|
||||||
|
self.login(ADMIN_ACCESS_USER)
|
||||||
|
|
||||||
|
# Create the invite.
|
||||||
|
membername = NO_ACCESS_USER
|
||||||
|
response = self.putJsonResponse(TeamMember,
|
||||||
|
params=dict(orgname=ORGANIZATION, teamname='owners',
|
||||||
|
membername=membername))
|
||||||
|
|
||||||
|
self.assertEquals(True, response['invited'])
|
||||||
|
|
||||||
|
# Login as the user.
|
||||||
|
self.login(membername)
|
||||||
|
|
||||||
|
# Decline the invite.
|
||||||
|
user = model.get_user(membername)
|
||||||
|
invites = list(model.lookup_team_invites(user))
|
||||||
|
self.assertEquals(1, len(invites))
|
||||||
|
|
||||||
|
self.deleteResponse(TeamMemberInvite,
|
||||||
|
params=dict(code=invites[0].invite_token))
|
||||||
|
|
||||||
|
# Make sure the invite was deleted.
|
||||||
|
self.deleteResponse(TeamMemberInvite,
|
||||||
|
params=dict(code=invites[0].invite_token),
|
||||||
|
expected_code=400)
|
||||||
|
|
||||||
|
|
||||||
class TestDeleteOrganizationTeamMember(ApiTestCase):
|
class TestDeleteOrganizationTeamMember(ApiTestCase):
|
||||||
|
def test_deletememberinvite(self):
|
||||||
|
self.login(ADMIN_ACCESS_USER)
|
||||||
|
|
||||||
|
membername = NO_ACCESS_USER
|
||||||
|
response = self.putJsonResponse(TeamMember,
|
||||||
|
params=dict(orgname=ORGANIZATION, teamname='readers',
|
||||||
|
membername=membername))
|
||||||
|
|
||||||
|
|
||||||
|
self.assertEquals(True, response['invited'])
|
||||||
|
|
||||||
|
# Verify the invite was added.
|
||||||
|
json = self.getJsonResponse(TeamMemberList,
|
||||||
|
params=dict(orgname=ORGANIZATION,
|
||||||
|
teamname='readers',
|
||||||
|
includePending=True))
|
||||||
|
|
||||||
|
assert len(json['members']) == 3
|
||||||
|
|
||||||
|
# Delete the invite.
|
||||||
|
self.deleteResponse(TeamMember,
|
||||||
|
params=dict(orgname=ORGANIZATION, teamname='readers',
|
||||||
|
membername=membername))
|
||||||
|
|
||||||
|
|
||||||
|
# Verify the user was removed from the team.
|
||||||
|
json = self.getJsonResponse(TeamMemberList,
|
||||||
|
params=dict(orgname=ORGANIZATION,
|
||||||
|
teamname='readers',
|
||||||
|
includePending=True))
|
||||||
|
|
||||||
|
assert len(json['members']) == 2
|
||||||
|
|
||||||
|
|
||||||
def test_deletemember(self):
|
def test_deletemember(self):
|
||||||
self.login(ADMIN_ACCESS_USER)
|
self.login(ADMIN_ACCESS_USER)
|
||||||
|
|
||||||
|
@ -775,7 +999,7 @@ class TestDeleteOrganizationTeamMember(ApiTestCase):
|
||||||
params=dict(orgname=ORGANIZATION,
|
params=dict(orgname=ORGANIZATION,
|
||||||
teamname='readers'))
|
teamname='readers'))
|
||||||
|
|
||||||
assert not READ_ACCESS_USER in json['members']
|
assert len(json['members']) == 1
|
||||||
|
|
||||||
|
|
||||||
class TestCreateRepo(ApiTestCase):
|
class TestCreateRepo(ApiTestCase):
|
||||||
|
@ -1205,6 +1429,7 @@ class TestListAndGetImage(ApiTestCase):
|
||||||
params=dict(repository=ADMIN_ACCESS_USER + '/simple'))
|
params=dict(repository=ADMIN_ACCESS_USER + '/simple'))
|
||||||
|
|
||||||
assert len(json['images']) > 0
|
assert len(json['images']) > 0
|
||||||
|
|
||||||
for image in json['images']:
|
for image in json['images']:
|
||||||
assert 'id' in image
|
assert 'id' in image
|
||||||
assert 'tags' in image
|
assert 'tags' in image
|
||||||
|
@ -1212,7 +1437,6 @@ class TestListAndGetImage(ApiTestCase):
|
||||||
assert 'comment' in image
|
assert 'comment' in image
|
||||||
assert 'command' in image
|
assert 'command' in image
|
||||||
assert 'ancestors' in image
|
assert 'ancestors' in image
|
||||||
assert 'dbid' in image
|
|
||||||
assert 'size' in image
|
assert 'size' in image
|
||||||
|
|
||||||
ijson = self.getJsonResponse(RepositoryImage,
|
ijson = self.getJsonResponse(RepositoryImage,
|
||||||
|
@ -1810,7 +2034,7 @@ class FakeBuildTrigger(BuildTriggerBase):
|
||||||
config['active'] = False
|
config['active'] = False
|
||||||
return config
|
return config
|
||||||
|
|
||||||
def manual_start(self, auth_token, config):
|
def manual_start(self, auth_token, config, run_parameters=None):
|
||||||
return ('foo', ['bar'], 'build-name', 'subdir')
|
return ('foo', ['bar'], 'build-name', 'subdir')
|
||||||
|
|
||||||
def dockerfile_url(self, auth_token, config):
|
def dockerfile_url(self, auth_token, config):
|
||||||
|
@ -1822,6 +2046,12 @@ class FakeBuildTrigger(BuildTriggerBase):
|
||||||
|
|
||||||
return config['dockerfile']
|
return config['dockerfile']
|
||||||
|
|
||||||
|
def list_field_values(self, auth_token, config, field_name):
|
||||||
|
if field_name == 'test_field':
|
||||||
|
return [1, 2, 3]
|
||||||
|
|
||||||
|
return None
|
||||||
|
|
||||||
|
|
||||||
class TestBuildTriggers(ApiTestCase):
|
class TestBuildTriggers(ApiTestCase):
|
||||||
def test_list_build_triggers(self):
|
def test_list_build_triggers(self):
|
||||||
|
@ -1994,9 +2224,22 @@ class TestBuildTriggers(ApiTestCase):
|
||||||
data={'config': trigger_config},
|
data={'config': trigger_config},
|
||||||
expected_code=400)
|
expected_code=400)
|
||||||
|
|
||||||
|
# Retrieve values for a field.
|
||||||
|
result = self.getJsonResponse(BuildTriggerFieldValues,
|
||||||
|
params=dict(repository=ADMIN_ACCESS_USER + '/simple',
|
||||||
|
trigger_uuid=trigger.uuid, field_name="test_field"))
|
||||||
|
|
||||||
|
self.assertEquals(result['values'], [1, 2, 3])
|
||||||
|
|
||||||
|
self.getResponse(BuildTriggerFieldValues,
|
||||||
|
params=dict(repository=ADMIN_ACCESS_USER + '/simple',
|
||||||
|
trigger_uuid=trigger.uuid, field_name="another_field"),
|
||||||
|
expected_code = 404)
|
||||||
|
|
||||||
# Start a manual build.
|
# Start a manual build.
|
||||||
start_json = self.postJsonResponse(ActivateBuildTrigger,
|
start_json = self.postJsonResponse(ActivateBuildTrigger,
|
||||||
params=dict(repository=ADMIN_ACCESS_USER + '/simple', trigger_uuid=trigger.uuid),
|
params=dict(repository=ADMIN_ACCESS_USER + '/simple', trigger_uuid=trigger.uuid),
|
||||||
|
data=dict(),
|
||||||
expected_code=201)
|
expected_code=201)
|
||||||
|
|
||||||
assert 'id' in start_json
|
assert 'id' in start_json
|
||||||
|
@ -2061,6 +2304,7 @@ class TestBuildTriggers(ApiTestCase):
|
||||||
# Start a manual build.
|
# Start a manual build.
|
||||||
start_json = self.postJsonResponse(ActivateBuildTrigger,
|
start_json = self.postJsonResponse(ActivateBuildTrigger,
|
||||||
params=dict(repository=ADMIN_ACCESS_USER + '/simple', trigger_uuid=trigger.uuid),
|
params=dict(repository=ADMIN_ACCESS_USER + '/simple', trigger_uuid=trigger.uuid),
|
||||||
|
data=dict(),
|
||||||
expected_code=201)
|
expected_code=201)
|
||||||
|
|
||||||
assert 'id' in start_json
|
assert 'id' in start_json
|
||||||
|
@ -2120,7 +2364,7 @@ class TestSuperUserManagement(ApiTestCase):
|
||||||
|
|
||||||
json = self.getJsonResponse(SuperUserManagement, params=dict(username = 'freshuser'))
|
json = self.getJsonResponse(SuperUserManagement, params=dict(username = 'freshuser'))
|
||||||
self.assertEquals('freshuser', json['username'])
|
self.assertEquals('freshuser', json['username'])
|
||||||
self.assertEquals('no@thanks.com', json['email'])
|
self.assertEquals('jschorr+test@devtable.com', json['email'])
|
||||||
self.assertEquals(False, json['super_user'])
|
self.assertEquals(False, json['super_user'])
|
||||||
|
|
||||||
def test_delete_user(self):
|
def test_delete_user(self):
|
||||||
|
@ -2143,7 +2387,7 @@ class TestSuperUserManagement(ApiTestCase):
|
||||||
# Verify the user exists.
|
# Verify the user exists.
|
||||||
json = self.getJsonResponse(SuperUserManagement, params=dict(username = 'freshuser'))
|
json = self.getJsonResponse(SuperUserManagement, params=dict(username = 'freshuser'))
|
||||||
self.assertEquals('freshuser', json['username'])
|
self.assertEquals('freshuser', json['username'])
|
||||||
self.assertEquals('no@thanks.com', json['email'])
|
self.assertEquals('jschorr+test@devtable.com', json['email'])
|
||||||
|
|
||||||
# Update the user.
|
# Update the user.
|
||||||
self.putJsonResponse(SuperUserManagement, params=dict(username='freshuser'), data=dict(email='foo@bar.com'))
|
self.putJsonResponse(SuperUserManagement, params=dict(username='freshuser'), data=dict(email='foo@bar.com'))
|
||||||
|
|
|
@ -34,6 +34,7 @@ class TestConfig(DefaultConfig):
|
||||||
|
|
||||||
FEATURE_SUPER_USERS = True
|
FEATURE_SUPER_USERS = True
|
||||||
FEATURE_BILLING = True
|
FEATURE_BILLING = True
|
||||||
|
FEATURE_MAILING = True
|
||||||
SUPER_USERS = ['devtable']
|
SUPER_USERS = ['devtable']
|
||||||
|
|
||||||
LICENSE_USER_LIMIT = 500
|
LICENSE_USER_LIMIT = 500
|
||||||
|
|
|
@ -45,8 +45,8 @@ class TestBuildLogs(RedisBuildLogs):
|
||||||
'pull_completion': 0.0,
|
'pull_completion': 0.0,
|
||||||
}
|
}
|
||||||
|
|
||||||
def __init__(self, redis_host, namespace, repository, test_build_id, allow_delegate=True):
|
def __init__(self, redis_config, namespace, repository, test_build_id, allow_delegate=True):
|
||||||
super(TestBuildLogs, self).__init__(redis_host)
|
super(TestBuildLogs, self).__init__(redis_config)
|
||||||
self.namespace = namespace
|
self.namespace = namespace
|
||||||
self.repository = repository
|
self.repository = repository
|
||||||
self.test_build_id = test_build_id
|
self.test_build_id = test_build_id
|
||||||
|
|
|
@ -1,7 +1,7 @@
|
||||||
import logging
|
import logging
|
||||||
import json
|
import json
|
||||||
|
|
||||||
from data.database import Image, ImageStorage, Repository, configure
|
from data.database import Image, ImageStorage, Repository, User, configure
|
||||||
from data import model
|
from data import model
|
||||||
from app import app, storage as store
|
from app import app, storage as store
|
||||||
|
|
||||||
|
@ -16,17 +16,18 @@ logging.getLogger('boto').setLevel(logging.CRITICAL)
|
||||||
|
|
||||||
|
|
||||||
query = (Image
|
query = (Image
|
||||||
.select(Image, ImageStorage, Repository)
|
.select(Image, ImageStorage, Repository, User)
|
||||||
.join(ImageStorage)
|
.join(ImageStorage)
|
||||||
.switch(Image)
|
.switch(Image)
|
||||||
.join(Repository)
|
.join(Repository)
|
||||||
.where(ImageStorage.uploading == False))
|
.join(User)
|
||||||
|
.where(ImageStorage.uploading == False))
|
||||||
|
|
||||||
bad_count = 0
|
bad_count = 0
|
||||||
good_count = 0
|
good_count = 0
|
||||||
|
|
||||||
def resolve_or_create(repo, docker_image_id, new_ancestry):
|
def resolve_or_create(repo, docker_image_id, new_ancestry):
|
||||||
existing = model.get_repo_image(repo.namespace, repo.name, docker_image_id)
|
existing = model.get_repo_image(repo.namespace_user.username, repo.name, docker_image_id)
|
||||||
if existing:
|
if existing:
|
||||||
logger.debug('Found existing image: %s, %s', existing.id, docker_image_id)
|
logger.debug('Found existing image: %s, %s', existing.id, docker_image_id)
|
||||||
return existing
|
return existing
|
||||||
|
@ -45,7 +46,7 @@ def resolve_or_create(repo, docker_image_id, new_ancestry):
|
||||||
return created
|
return created
|
||||||
except ImageStorage.DoesNotExist:
|
except ImageStorage.DoesNotExist:
|
||||||
msg = 'No image available anywhere for storage: %s in namespace: %s'
|
msg = 'No image available anywhere for storage: %s in namespace: %s'
|
||||||
logger.error(msg, docker_image_id, repo.namespace)
|
logger.error(msg, docker_image_id, repo.namespace_user.username)
|
||||||
raise RuntimeError()
|
raise RuntimeError()
|
||||||
|
|
||||||
|
|
||||||
|
@ -62,20 +63,19 @@ def all_ancestors_exist(ancestors):
|
||||||
cant_fix = []
|
cant_fix = []
|
||||||
for img in query:
|
for img in query:
|
||||||
try:
|
try:
|
||||||
with_locations = model.get_repo_image(img.repository.namespace, img.repository.name,
|
with_locations = model.get_repo_image(img.repository.namespace_user.username,
|
||||||
img.docker_image_id)
|
img.repository.name, img.docker_image_id)
|
||||||
ancestry_storage = store.image_ancestry_path(img.storage.uuid)
|
ancestry_storage = store.image_ancestry_path(img.storage.uuid)
|
||||||
if store.exists(with_locations.storage.locations, ancestry_storage):
|
if store.exists(with_locations.storage.locations, ancestry_storage):
|
||||||
full_ancestry = json.loads(store.get_content(with_locations.storage.locations, ancestry_storage))[1:]
|
full_ancestry = json.loads(store.get_content(with_locations.storage.locations,
|
||||||
|
ancestry_storage))[1:]
|
||||||
full_ancestry.reverse()
|
full_ancestry.reverse()
|
||||||
|
|
||||||
ancestor_dbids = [int(anc_id)
|
ancestor_dbids = [int(anc_id) for anc_id in img.ancestors.split('/')[1:-1]]
|
||||||
for anc_id in img.ancestors.split('/')[1:-1]]
|
|
||||||
|
|
||||||
if len(full_ancestry) != len(ancestor_dbids) or not all_ancestors_exist(ancestor_dbids):
|
if len(full_ancestry) != len(ancestor_dbids) or not all_ancestors_exist(ancestor_dbids):
|
||||||
logger.error('Image has incomplete ancestry: %s, %s, %s, %s' %
|
logger.error('Image has incomplete ancestry: %s, %s, %s, %s', img.id, img.docker_image_id,
|
||||||
(img.id, img.docker_image_id, full_ancestry,
|
full_ancestry, ancestor_dbids)
|
||||||
ancestor_dbids))
|
|
||||||
|
|
||||||
fixed_ancestry = '/'
|
fixed_ancestry = '/'
|
||||||
for ancestor in full_ancestry:
|
for ancestor in full_ancestry:
|
||||||
|
@ -99,5 +99,5 @@ for img in query:
|
||||||
len(cant_fix))
|
len(cant_fix))
|
||||||
|
|
||||||
for cant in cant_fix:
|
for cant in cant_fix:
|
||||||
logger.error('Unable to fix %s in repo %s/%s', cant.id,
|
logger.error('Unable to fix %s in repo %s/%s', cant.id, cant.repository.namespace_user.username,
|
||||||
cant.repository.namespace, cant.repository.name)
|
cant.repository.name)
|
||||||
|
|
|
@ -1,22 +1,15 @@
|
||||||
from data.database import Image
|
from data.database import Image, ImageStorage
|
||||||
from app import app, storage as store
|
from peewee import JOIN_LEFT_OUTER, fn
|
||||||
|
from app import app
|
||||||
|
|
||||||
live_image_id_set = set()
|
orphaned = (ImageStorage
|
||||||
|
.select()
|
||||||
|
.where(ImageStorage.uploading == False)
|
||||||
|
.join(Image, JOIN_LEFT_OUTER)
|
||||||
|
.group_by(ImageStorage)
|
||||||
|
.having(fn.Count(Image.id) == 0))
|
||||||
|
|
||||||
for image in Image.select():
|
counter = 0
|
||||||
live_image_id_set.add(image.docker_image_id)
|
for orphan in orphaned:
|
||||||
|
counter += 1
|
||||||
storage_image_id_set = set()
|
print orphan.uuid
|
||||||
for customer in store.list_directory('images/'):
|
|
||||||
for repo in store.list_directory(customer):
|
|
||||||
for image in store.list_directory(repo):
|
|
||||||
storage_image_id_set.add(image.split('/')[-1])
|
|
||||||
|
|
||||||
orphans = storage_image_id_set.difference(live_image_id_set)
|
|
||||||
missing_image_data = live_image_id_set.difference(storage_image_id_set)
|
|
||||||
|
|
||||||
for orphan in orphans:
|
|
||||||
print "Orphan: %s" % orphan
|
|
||||||
|
|
||||||
for missing in missing_image_data:
|
|
||||||
print "Missing: %s" % missing
|
|
||||||
|
|
|
@ -9,7 +9,7 @@ with open('outfile.dot', 'w') as outfile:
|
||||||
outfile.write('digraph relationships {\n')
|
outfile.write('digraph relationships {\n')
|
||||||
|
|
||||||
for repo in Repository.select():
|
for repo in Repository.select():
|
||||||
ns = fix_ident(repo.namespace)
|
ns = fix_ident(repo.namespace_user.username)
|
||||||
outfile.write('%s_%s -> %s\n' % (ns, fix_ident(repo.name), ns))
|
outfile.write('%s_%s -> %s\n' % (ns, fix_ident(repo.name), ns))
|
||||||
|
|
||||||
teams_in_orgs = set()
|
teams_in_orgs = set()
|
||||||
|
|
75
tools/uncompressedsize.py
Normal file
75
tools/uncompressedsize.py
Normal file
|
@ -0,0 +1,75 @@
|
||||||
|
import logging
|
||||||
|
import zlib
|
||||||
|
|
||||||
|
from data import model
|
||||||
|
from data.database import ImageStorage
|
||||||
|
from app import app, storage as store
|
||||||
|
from data.database import db, db_random_func
|
||||||
|
from util.gzipstream import ZLIB_GZIP_WINDOW
|
||||||
|
|
||||||
|
|
||||||
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
|
||||||
|
CHUNK_SIZE = 5 * 1024 * 1024
|
||||||
|
|
||||||
|
|
||||||
|
def backfill_sizes_from_data():
|
||||||
|
while True:
|
||||||
|
# Load the record from the DB.
|
||||||
|
batch_ids = list(ImageStorage
|
||||||
|
.select(ImageStorage.uuid)
|
||||||
|
.where(ImageStorage.uncompressed_size >> None,
|
||||||
|
ImageStorage.uploading == False)
|
||||||
|
.limit(100)
|
||||||
|
.order_by(db_random_func()))
|
||||||
|
if len(batch_ids) == 0:
|
||||||
|
# We're done!
|
||||||
|
return
|
||||||
|
|
||||||
|
for record in batch_ids:
|
||||||
|
uuid = record.uuid
|
||||||
|
|
||||||
|
try:
|
||||||
|
with_locs = model.get_storage_by_uuid(uuid)
|
||||||
|
if with_locs.uncompressed_size is not None:
|
||||||
|
logger.debug('Somebody else already filled this in for us: %s', uuid)
|
||||||
|
continue
|
||||||
|
|
||||||
|
# Read the layer from backing storage and calculate the uncompressed size.
|
||||||
|
logger.debug('Loading data: %s (%s bytes)', uuid, with_locs.image_size)
|
||||||
|
decompressor = zlib.decompressobj(ZLIB_GZIP_WINDOW)
|
||||||
|
|
||||||
|
uncompressed_size = 0
|
||||||
|
with store.stream_read_file(with_locs.locations, store.image_layer_path(uuid)) as stream:
|
||||||
|
while True:
|
||||||
|
current_data = stream.read(CHUNK_SIZE)
|
||||||
|
if len(current_data) == 0:
|
||||||
|
break
|
||||||
|
|
||||||
|
uncompressed_size += len(decompressor.decompress(current_data))
|
||||||
|
|
||||||
|
# Write the size to the image storage. We do so under a transaction AFTER checking to
|
||||||
|
# make sure the image storage still exists and has not changed.
|
||||||
|
logger.debug('Writing entry: %s. Size: %s', uuid, uncompressed_size)
|
||||||
|
with app.config['DB_TRANSACTION_FACTORY'](db):
|
||||||
|
current_record = model.get_storage_by_uuid(uuid)
|
||||||
|
|
||||||
|
if not current_record.uploading and current_record.uncompressed_size == None:
|
||||||
|
current_record.uncompressed_size = uncompressed_size
|
||||||
|
current_record.save()
|
||||||
|
else:
|
||||||
|
logger.debug('Somebody else already filled this in for us, after we did the work: %s',
|
||||||
|
uuid)
|
||||||
|
|
||||||
|
except model.InvalidImageException:
|
||||||
|
logger.warning('Storage with uuid no longer exists: %s', uuid)
|
||||||
|
except MemoryError:
|
||||||
|
logger.warning('MemoryError on %s', uuid)
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
logging.basicConfig(level=logging.DEBUG)
|
||||||
|
logging.getLogger('boto').setLevel(logging.CRITICAL)
|
||||||
|
logging.getLogger('peewee').setLevel(logging.CRITICAL)
|
||||||
|
|
||||||
|
backfill_sizes_from_data()
|
28
util/gzipstream.py
Normal file
28
util/gzipstream.py
Normal file
|
@ -0,0 +1,28 @@
|
||||||
|
"""
|
||||||
|
Defines utility methods for working with gzip streams.
|
||||||
|
"""
|
||||||
|
|
||||||
|
import zlib
|
||||||
|
|
||||||
|
# Window size for decompressing GZIP streams.
|
||||||
|
# This results in ZLIB automatically detecting the GZIP headers.
|
||||||
|
# http://stackoverflow.com/questions/3122145/zlib-error-error-3-while-decompressing-incorrect-header-check/22310760#22310760
|
||||||
|
ZLIB_GZIP_WINDOW = zlib.MAX_WBITS | 32
|
||||||
|
|
||||||
|
class SizeInfo(object):
|
||||||
|
def __init__(self):
|
||||||
|
self.size = 0
|
||||||
|
|
||||||
|
def calculate_size_handler():
|
||||||
|
""" Returns an object and a SocketReader handler. The handler will gunzip the data it receives,
|
||||||
|
adding the size found to the object.
|
||||||
|
"""
|
||||||
|
|
||||||
|
size_info = SizeInfo()
|
||||||
|
|
||||||
|
decompressor = zlib.decompressobj(ZLIB_GZIP_WINDOW)
|
||||||
|
|
||||||
|
def fn(buf):
|
||||||
|
size_info.size += len(decompressor.decompress(buf))
|
||||||
|
|
||||||
|
return size_info, fn
|
|
@ -34,6 +34,27 @@ def parse_robot_username(robot_username):
|
||||||
return robot_username.split('+', 2)
|
return robot_username.split('+', 2)
|
||||||
|
|
||||||
|
|
||||||
|
def parse_urn(urn):
|
||||||
|
""" Parses a URN, returning a pair that contains a list of URN
|
||||||
|
namespace parts, followed by the URN's unique ID.
|
||||||
|
"""
|
||||||
|
if not urn.startswith('urn:'):
|
||||||
|
return None
|
||||||
|
|
||||||
|
parts = urn[len('urn:'):].split(':')
|
||||||
|
return (parts[0:len(parts) - 1], parts[len(parts) - 1])
|
||||||
|
|
||||||
|
|
||||||
|
def parse_single_urn(urn):
|
||||||
|
""" Parses a URN, returning a pair that contains the first
|
||||||
|
namespace part, followed by the URN's unique ID.
|
||||||
|
"""
|
||||||
|
result = parse_urn(urn)
|
||||||
|
if result is None or not len(result[0]):
|
||||||
|
return None
|
||||||
|
|
||||||
|
return (result[0][0], result[1])
|
||||||
|
|
||||||
uuid_generator = lambda: str(uuid4())
|
uuid_generator = lambda: str(uuid4())
|
||||||
|
|
||||||
|
|
||||||
|
|
|
@ -1,116 +1,158 @@
|
||||||
from flask.ext.mail import Message
|
from flask.ext.mail import Message
|
||||||
|
|
||||||
from app import mail, app, get_app_url
|
from app import mail, app, get_app_url
|
||||||
|
from jinja2 import Template, Environment, FileSystemLoader, contextfilter
|
||||||
|
from data import model
|
||||||
|
from util.gravatar import compute_hash
|
||||||
|
|
||||||
|
def user_reference(username):
|
||||||
|
user = model.get_user_or_org(username)
|
||||||
|
if not user:
|
||||||
|
return username
|
||||||
|
|
||||||
|
return """
|
||||||
|
<span>
|
||||||
|
<img src="http://www.gravatar.com/avatar/%s?s=16&d=identicon" style="vertical-align: middle; margin-left: 6px; margin-right: 4px;">
|
||||||
|
<b>%s</b>
|
||||||
|
</span>""" % (compute_hash(user.email), username)
|
||||||
|
|
||||||
|
|
||||||
CONFIRM_MESSAGE = """
|
def repository_reference(pair):
|
||||||
This email address was recently used to register the username '%s'
|
(namespace, repository) = pair
|
||||||
at <a href="%s">Quay.io</a>.<br>
|
|
||||||
<br>
|
owner = model.get_user(namespace)
|
||||||
To confirm this email address, please click the following link:<br>
|
if not owner:
|
||||||
<a href="%s/confirm?code=%s">%s/confirm?code=%s</a>
|
return "%s/%s" % (namespace, repository)
|
||||||
"""
|
|
||||||
|
return """
|
||||||
|
<span style="white-space: nowrap;">
|
||||||
|
<img src="http://www.gravatar.com/avatar/%s?s=16&d=identicon" style="vertical-align: middle; margin-left: 6px; margin-right: 4px;">
|
||||||
|
<a href="%s/repository/%s/%s">%s/%s</a>
|
||||||
|
</span>
|
||||||
|
""" % (compute_hash(owner.email), get_app_url(), namespace, repository, namespace, repository)
|
||||||
|
|
||||||
|
|
||||||
CHANGE_MESSAGE = """
|
def admin_reference(username):
|
||||||
This email address was recently asked to become the new e-mail address for username '%s'
|
user = model.get_user(username)
|
||||||
at <a href="%s">Quay.io</a>.<br>
|
if not user:
|
||||||
<br>
|
return 'account settings'
|
||||||
To confirm this email address, please click the following link:<br>
|
|
||||||
<a href="%s/confirm?code=%s">%s/confirm?code=%s</a>
|
if user.organization:
|
||||||
"""
|
return """
|
||||||
|
<a href="%s/organization/%s/admin">organization's admin setting</a>
|
||||||
|
""" % (get_app_url(), username)
|
||||||
|
else:
|
||||||
|
return """
|
||||||
|
<a href="%s/user/">account settings</a>
|
||||||
|
""" % (get_app_url())
|
||||||
|
|
||||||
|
|
||||||
RECOVERY_MESSAGE = """
|
template_loader = FileSystemLoader(searchpath="emails")
|
||||||
A user at <a href="%s">Quay.io</a> has attempted to recover their account
|
template_env = Environment(loader=template_loader)
|
||||||
using this email address.<br>
|
template_env.filters['user_reference'] = user_reference
|
||||||
<br>
|
template_env.filters['admin_reference'] = admin_reference
|
||||||
If you made this request, please click the following link to recover your account and
|
template_env.filters['repository_reference'] = repository_reference
|
||||||
change your password:
|
|
||||||
<a href="%s/recovery?code=%s">%s/recovery?code=%s</a><br>
|
|
||||||
<br>
|
|
||||||
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>
|
|
||||||
"""
|
|
||||||
|
|
||||||
|
|
||||||
SUBSCRIPTION_CHANGE = """
|
def send_email(recipient, subject, template_file, parameters):
|
||||||
Change: {0}<br>
|
app_title = app.config['REGISTRY_TITLE_SHORT']
|
||||||
Customer id: <a href="https://manage.stripe.com/customers/{1}">{1}</a><br>
|
app_url = get_app_url()
|
||||||
Customer email: <a href="mailto:{2}">{2}</a><br>
|
|
||||||
Quay user or org name: {3}<br>
|
def app_link_handler(url=None, title=None):
|
||||||
"""
|
real_url = app_url + '/' + url if url else app_url
|
||||||
|
if not title:
|
||||||
|
title = real_url if url else app_title
|
||||||
|
|
||||||
|
return '<a href="%s">%s</a>' % (real_url, title)
|
||||||
|
|
||||||
|
parameters.update({
|
||||||
|
'subject': subject,
|
||||||
|
'app_logo': 'https://quay.io/static/img/quay-logo.png', # TODO: make this pull from config
|
||||||
|
'app_url': app_url,
|
||||||
|
'app_title': app_title,
|
||||||
|
'app_link': app_link_handler
|
||||||
|
})
|
||||||
|
|
||||||
|
rendered_html = template_env.get_template(template_file + '.html').render(parameters)
|
||||||
|
|
||||||
|
msg = Message('[%s] %s' % (app_title, subject), sender='support@quay.io', recipients=[recipient])
|
||||||
|
msg.html = rendered_html
|
||||||
|
mail.send(msg)
|
||||||
|
|
||||||
|
|
||||||
PAYMENT_FAILED = """
|
def send_password_changed(username, email):
|
||||||
Hi {0},<br>
|
send_email(email, 'Account password changed', 'passwordchanged', {
|
||||||
<br>
|
'username': username
|
||||||
Your recent payment for Quay.io failed, which usually results in our payments processorcanceling
|
})
|
||||||
your subscription automatically. If you would like to continue to use Quay.io without interruption,
|
|
||||||
please add a new card to Quay.io and re-subscribe to your plan.<br>
|
|
||||||
<br>
|
|
||||||
You can find the card and subscription management features under your account settings.<br>
|
|
||||||
<br>
|
|
||||||
Thanks and have a great day!<br>
|
|
||||||
<br>
|
|
||||||
-Quay.io Support<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}'
|
|
||||||
|
|
||||||
|
def send_email_changed(username, old_email, new_email):
|
||||||
|
send_email(old_email, 'Account e-mail address changed', 'emailchanged', {
|
||||||
|
'username': username,
|
||||||
|
'new_email': new_email
|
||||||
|
})
|
||||||
|
|
||||||
def send_change_email(username, email, token):
|
def send_change_email(username, email, token):
|
||||||
msg = Message('Quay.io email change. Please confirm your email.',
|
send_email(email, 'E-mail address change requested', 'changeemail', {
|
||||||
sender='support@quay.io', # Why do I need this?
|
'username': username,
|
||||||
recipients=[email])
|
'token': token
|
||||||
msg.html = CHANGE_MESSAGE % (username, get_app_url(), get_app_url(), token, get_app_url(), token)
|
})
|
||||||
mail.send(msg)
|
|
||||||
|
|
||||||
|
|
||||||
def send_confirmation_email(username, email, token):
|
def send_confirmation_email(username, email, token):
|
||||||
msg = Message('Welcome to Quay.io! Please confirm your email.',
|
send_email(email, 'Please confirm your e-mail address', 'confirmemail', {
|
||||||
sender='support@quay.io', # Why do I need this?
|
'username': username,
|
||||||
recipients=[email])
|
'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):
|
def send_repo_authorization_email(namespace, repository, email, token):
|
||||||
msg = Message('Quay.io Notification: Please confirm your email.',
|
subject = 'Please verify your e-mail address for repository %s/%s' % (namespace, repository)
|
||||||
sender='support@quay.io', # Why do I need this?
|
send_email(email, subject, 'repoauthorizeemail', {
|
||||||
recipients=[email])
|
'namespace': namespace,
|
||||||
msg.html = AUTH_FORREPO_MESSAGE % (get_app_url(), get_app_url(), namespace, repository, namespace,
|
'repository': repository,
|
||||||
repository, get_app_url(), token, get_app_url(), token)
|
'token': token
|
||||||
mail.send(msg)
|
})
|
||||||
|
|
||||||
|
|
||||||
def send_recovery_email(email, token):
|
def send_recovery_email(email, token):
|
||||||
msg = Message('Quay.io account recovery.',
|
subject = 'Account recovery'
|
||||||
sender='support@quay.io', # Why do I need this?
|
send_email(email, subject, 'recovery', {
|
||||||
recipients=[email])
|
'email': email,
|
||||||
msg.html = RECOVERY_MESSAGE % (get_app_url(), get_app_url(), token, get_app_url(), token)
|
'token': token
|
||||||
mail.send(msg)
|
})
|
||||||
|
|
||||||
|
def send_payment_failed(email, username):
|
||||||
|
send_email(email, 'Subscription Payment Failure', 'paymentfailure', {
|
||||||
|
'username': username
|
||||||
|
})
|
||||||
|
|
||||||
|
def send_org_invite_email(member_name, member_email, orgname, team, adder, code):
|
||||||
|
send_email(member_email, 'Invitation to join team', 'teaminvite', {
|
||||||
|
'inviter': adder,
|
||||||
|
'token': code,
|
||||||
|
'organization': orgname,
|
||||||
|
'teamname': team
|
||||||
|
})
|
||||||
|
|
||||||
|
|
||||||
def send_invoice_email(email, contents):
|
def send_invoice_email(email, contents):
|
||||||
|
# Note: This completely generates the contents of the email, so we don't use the
|
||||||
|
# normal template here.
|
||||||
msg = Message('Quay.io payment received - Thank you!',
|
msg = Message('Quay.io payment received - Thank you!',
|
||||||
sender='support@quay.io', # Why do I need this?
|
sender='support@quay.io',
|
||||||
recipients=[email])
|
recipients=[email])
|
||||||
msg.html = contents
|
msg.html = contents
|
||||||
mail.send(msg)
|
mail.send(msg)
|
||||||
|
|
||||||
|
|
||||||
|
# INTERNAL EMAILS BELOW
|
||||||
|
|
||||||
def send_subscription_change(change_description, customer_id, customer_email, quay_username):
|
def send_subscription_change(change_description, customer_id, customer_email, quay_username):
|
||||||
|
SUBSCRIPTION_CHANGE_TITLE = 'Subscription Change - {0} {1}'
|
||||||
|
SUBSCRIPTION_CHANGE = """
|
||||||
|
Change: {0}<br>
|
||||||
|
Customer id: <a href="https://manage.stripe.com/customers/{1}">{1}</a><br>
|
||||||
|
Customer email: <a href="mailto:{2}">{2}</a><br>
|
||||||
|
Quay user or org name: {3}<br>
|
||||||
|
"""
|
||||||
|
|
||||||
title = SUBSCRIPTION_CHANGE_TITLE.format(quay_username, change_description)
|
title = SUBSCRIPTION_CHANGE_TITLE.format(quay_username, change_description)
|
||||||
msg = Message(title, sender='support@quay.io', recipients=['stripe@quay.io'])
|
msg = Message(title, sender='support@quay.io', recipients=['stripe@quay.io'])
|
||||||
msg.html = SUBSCRIPTION_CHANGE.format(change_description, customer_id, customer_email,
|
msg.html = SUBSCRIPTION_CHANGE.format(change_description, customer_id, customer_email,
|
||||||
|
@ -118,8 +160,3 @@ def send_subscription_change(change_description, customer_id, customer_email, qu
|
||||||
mail.send(msg)
|
mail.send(msg)
|
||||||
|
|
||||||
|
|
||||||
def send_payment_failed(customer_email, quay_username):
|
|
||||||
msg = Message('Quay.io Subscription Payment Failure', sender='support@quay.io',
|
|
||||||
recipients=[customer_email])
|
|
||||||
msg.html = PAYMENT_FAILED.format(quay_username)
|
|
||||||
mail.send(msg)
|
|
||||||
|
|
|
@ -7,11 +7,12 @@ from gzip import GzipFile
|
||||||
|
|
||||||
from data import model
|
from data import model
|
||||||
from data.archivedlogs import JSON_MIMETYPE
|
from data.archivedlogs import JSON_MIMETYPE
|
||||||
from data.database import RepositoryBuild
|
from data.database import RepositoryBuild, db_random_func
|
||||||
from app import build_logs, log_archive
|
from app import build_logs, log_archive
|
||||||
from util.streamingjsonencoder import StreamingJSONEncoder
|
from util.streamingjsonencoder import StreamingJSONEncoder
|
||||||
|
|
||||||
POLL_PERIOD_SECONDS = 30
|
POLL_PERIOD_SECONDS = 30
|
||||||
|
MEMORY_TEMPFILE_SIZE = 64 * 1024 # Large enough to handle approximately 99% of builds in memory
|
||||||
|
|
||||||
logger = logging.getLogger(__name__)
|
logger = logging.getLogger(__name__)
|
||||||
sched = BlockingScheduler()
|
sched = BlockingScheduler()
|
||||||
|
@ -22,7 +23,7 @@ def archive_redis_buildlogs():
|
||||||
avoid needing two-phase commit. """
|
avoid needing two-phase commit. """
|
||||||
try:
|
try:
|
||||||
# Get a random build to archive
|
# Get a random build to archive
|
||||||
to_archive = model.archivable_buildlogs_query().order_by(fn.Random()).get()
|
to_archive = model.archivable_buildlogs_query().order_by(db_random_func()).get()
|
||||||
logger.debug('Archiving: %s', to_archive.uuid)
|
logger.debug('Archiving: %s', to_archive.uuid)
|
||||||
|
|
||||||
length, entries = build_logs.get_log_entries(to_archive.uuid, 0)
|
length, entries = build_logs.get_log_entries(to_archive.uuid, 0)
|
||||||
|
@ -32,7 +33,7 @@ def archive_redis_buildlogs():
|
||||||
'logs': entries,
|
'logs': entries,
|
||||||
}
|
}
|
||||||
|
|
||||||
with SpooledTemporaryFile() as tempfile:
|
with SpooledTemporaryFile(MEMORY_TEMPFILE_SIZE) as tempfile:
|
||||||
with GzipFile('testarchive', fileobj=tempfile) as zipstream:
|
with GzipFile('testarchive', fileobj=tempfile) as zipstream:
|
||||||
for chunk in StreamingJSONEncoder().iterencode(to_encode):
|
for chunk in StreamingJSONEncoder().iterencode(to_encode):
|
||||||
zipstream.write(chunk)
|
zipstream.write(chunk)
|
||||||
|
|
|
@ -33,7 +33,8 @@ class DiffsWorker(Worker):
|
||||||
|
|
||||||
return True
|
return True
|
||||||
|
|
||||||
logging.config.fileConfig('conf/logging.conf', disable_existing_loggers=False)
|
if __name__ == "__main__":
|
||||||
|
logging.config.fileConfig('conf/logging.conf', disable_existing_loggers=False)
|
||||||
|
|
||||||
worker = DiffsWorker(image_diff_queue)
|
worker = DiffsWorker(image_diff_queue)
|
||||||
worker.start()
|
worker.start()
|
||||||
|
|
|
@ -38,6 +38,8 @@ TIMEOUT_PERIOD_MINUTES = 20
|
||||||
CACHE_EXPIRATION_PERIOD_HOURS = 24
|
CACHE_EXPIRATION_PERIOD_HOURS = 24
|
||||||
NO_TAGS = ['<none>:<none>']
|
NO_TAGS = ['<none>:<none>']
|
||||||
RESERVATION_TIME = (TIMEOUT_PERIOD_MINUTES + 5) * 60
|
RESERVATION_TIME = (TIMEOUT_PERIOD_MINUTES + 5) * 60
|
||||||
|
DOCKER_BASE_URL = None # Set this if you want to use a different docker URL/socket.
|
||||||
|
|
||||||
|
|
||||||
def matches_system_error(status_str):
|
def matches_system_error(status_str):
|
||||||
""" Returns true if the given status string matches a known system error in the
|
""" Returns true if the given status string matches a known system error in the
|
||||||
|
@ -128,8 +130,8 @@ class DockerfileBuildContext(object):
|
||||||
# Note: We have two different clients here because we (potentially) login
|
# Note: We have two different clients here because we (potentially) login
|
||||||
# with both, but with different credentials that we do not want shared between
|
# with both, but with different credentials that we do not want shared between
|
||||||
# the build and push operations.
|
# the build and push operations.
|
||||||
self._push_cl = StreamingDockerClient(timeout=1200)
|
self._push_cl = StreamingDockerClient(timeout=1200, base_url = DOCKER_BASE_URL)
|
||||||
self._build_cl = StreamingDockerClient(timeout=1200)
|
self._build_cl = StreamingDockerClient(timeout=1200, base_url = DOCKER_BASE_URL)
|
||||||
|
|
||||||
dockerfile_path = os.path.join(self._build_dir, dockerfile_subdir,
|
dockerfile_path = os.path.join(self._build_dir, dockerfile_subdir,
|
||||||
'Dockerfile')
|
'Dockerfile')
|
||||||
|
@ -221,6 +223,13 @@ class DockerfileBuildContext(object):
|
||||||
raise RuntimeError(message)
|
raise RuntimeError(message)
|
||||||
|
|
||||||
def pull(self):
|
def pull(self):
|
||||||
|
image_and_tag_tuple = self._parsed_dockerfile.get_image_and_tag()
|
||||||
|
if image_and_tag_tuple is None or image_and_tag_tuple[0] is None:
|
||||||
|
self._build_logger('Missing FROM command in Dockerfile', build_logs.ERROR)
|
||||||
|
raise JobException('Missing FROM command in Dockerfile')
|
||||||
|
|
||||||
|
image_and_tag = ':'.join(image_and_tag_tuple)
|
||||||
|
|
||||||
# Login with the specified credentials (if any).
|
# Login with the specified credentials (if any).
|
||||||
if self._pull_credentials:
|
if self._pull_credentials:
|
||||||
logger.debug('Logging in with pull credentials: %s@%s',
|
logger.debug('Logging in with pull credentials: %s@%s',
|
||||||
|
@ -236,13 +245,6 @@ class DockerfileBuildContext(object):
|
||||||
registry=self._pull_credentials['registry'], reauth=True)
|
registry=self._pull_credentials['registry'], reauth=True)
|
||||||
|
|
||||||
# Pull the image, in case it was updated since the last build
|
# Pull the image, in case it was updated since the last build
|
||||||
image_and_tag_tuple = self._parsed_dockerfile.get_image_and_tag()
|
|
||||||
if image_and_tag_tuple is None or image_and_tag_tuple[0] is None:
|
|
||||||
self._build_logger('Missing FROM command in Dockerfile', build_logs.ERROR)
|
|
||||||
raise JobException('Missing FROM command in Dockerfile')
|
|
||||||
|
|
||||||
image_and_tag = ':'.join(image_and_tag_tuple)
|
|
||||||
|
|
||||||
self._build_logger('Pulling base image: %s' % image_and_tag, log_data = {
|
self._build_logger('Pulling base image: %s' % image_and_tag, log_data = {
|
||||||
'phasestep': 'pull',
|
'phasestep': 'pull',
|
||||||
'repo_url': image_and_tag
|
'repo_url': image_and_tag
|
||||||
|
@ -478,9 +480,8 @@ class DockerfileBuildWorker(Worker):
|
||||||
|
|
||||||
def watchdog(self):
|
def watchdog(self):
|
||||||
logger.debug('Running build watchdog code.')
|
logger.debug('Running build watchdog code.')
|
||||||
|
|
||||||
try:
|
try:
|
||||||
docker_cl = Client()
|
docker_cl = Client(base_url = DOCKER_BASE_URL)
|
||||||
|
|
||||||
# Iterate the running containers and kill ones that have been running more than 20 minutes
|
# Iterate the running containers and kill ones that have been running more than 20 minutes
|
||||||
for container in docker_cl.containers():
|
for container in docker_cl.containers():
|
||||||
|
@ -519,7 +520,20 @@ class DockerfileBuildWorker(Worker):
|
||||||
log_appender = partial(build_logs.append_log_message,
|
log_appender = partial(build_logs.append_log_message,
|
||||||
repository_build.uuid)
|
repository_build.uuid)
|
||||||
|
|
||||||
log_appender('initializing', build_logs.PHASE)
|
# Lookup and save the version of docker being used.
|
||||||
|
docker_cl = Client(base_url = DOCKER_BASE_URL)
|
||||||
|
docker_version = docker_cl.version().get('Version', '')
|
||||||
|
dash = docker_version.find('-')
|
||||||
|
|
||||||
|
# Strip any -tutum or whatever off of the version.
|
||||||
|
if dash > 0:
|
||||||
|
docker_version = docker_version[:dash]
|
||||||
|
|
||||||
|
log_appender('initializing', build_logs.PHASE, log_data = {
|
||||||
|
'docker_version': docker_version
|
||||||
|
})
|
||||||
|
|
||||||
|
log_appender('Docker version: %s' % docker_version)
|
||||||
|
|
||||||
start_msg = ('Starting job with resource url: %s repo: %s' % (resource_url,
|
start_msg = ('Starting job with resource url: %s repo: %s' % (resource_url,
|
||||||
repo))
|
repo))
|
||||||
|
|
|
@ -8,6 +8,7 @@ from workers.worker import Worker
|
||||||
|
|
||||||
from endpoints.notificationmethod import NotificationMethod, InvalidNotificationMethodException
|
from endpoints.notificationmethod import NotificationMethod, InvalidNotificationMethodException
|
||||||
from endpoints.notificationevent import NotificationEvent, InvalidNotificationEventException
|
from endpoints.notificationevent import NotificationEvent, InvalidNotificationEventException
|
||||||
|
from workers.worker import JobException
|
||||||
|
|
||||||
from data import model
|
from data import model
|
||||||
|
|
||||||
|
@ -29,7 +30,7 @@ class NotificationWorker(Worker):
|
||||||
notification = model.get_repo_notification(repo_namespace, repo_name, notification_uuid)
|
notification = model.get_repo_notification(repo_namespace, repo_name, notification_uuid)
|
||||||
if not notification:
|
if not notification:
|
||||||
# Probably deleted.
|
# Probably deleted.
|
||||||
return True
|
return
|
||||||
|
|
||||||
event_name = notification.event.name
|
event_name = notification.event.name
|
||||||
method_name = notification.method.name
|
method_name = notification.method.name
|
||||||
|
@ -39,15 +40,17 @@ class NotificationWorker(Worker):
|
||||||
method_handler = NotificationMethod.get_method(method_name)
|
method_handler = NotificationMethod.get_method(method_name)
|
||||||
except InvalidNotificationMethodException as ex:
|
except InvalidNotificationMethodException as ex:
|
||||||
logger.exception('Cannot find notification method: %s' % ex.message)
|
logger.exception('Cannot find notification method: %s' % ex.message)
|
||||||
return False
|
raise JobException('Cannot find notification method: %s' % ex.message)
|
||||||
except InvalidNotificationEventException as ex:
|
except InvalidNotificationEventException as ex:
|
||||||
logger.exception('Cannot find notification method: %s' % ex.message)
|
logger.exception('Cannot find notification event: %s' % ex.message)
|
||||||
return False
|
raise JobException('Cannot find notification event: %s' % ex.message)
|
||||||
|
|
||||||
return method_handler.perform(notification, event_handler, job_details)
|
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,
|
if __name__ == "__main__":
|
||||||
reservation_seconds=3600)
|
logging.config.fileConfig('conf/logging.conf', disable_existing_loggers=False)
|
||||||
worker.start()
|
|
||||||
|
worker = NotificationWorker(notification_queue, poll_period_seconds=10, reservation_seconds=30,
|
||||||
|
retry_after_seconds=30)
|
||||||
|
worker.start()
|
||||||
|
|
|
@ -63,11 +63,12 @@ class WorkerStatusHandler(BaseHTTPRequestHandler):
|
||||||
|
|
||||||
class Worker(object):
|
class Worker(object):
|
||||||
def __init__(self, queue, poll_period_seconds=30, reservation_seconds=300,
|
def __init__(self, queue, poll_period_seconds=30, reservation_seconds=300,
|
||||||
watchdog_period_seconds=60):
|
watchdog_period_seconds=60, retry_after_seconds=300):
|
||||||
self._sched = BackgroundScheduler()
|
self._sched = BackgroundScheduler()
|
||||||
self._poll_period_seconds = poll_period_seconds
|
self._poll_period_seconds = poll_period_seconds
|
||||||
self._reservation_seconds = reservation_seconds
|
self._reservation_seconds = reservation_seconds
|
||||||
self._watchdog_period_seconds = watchdog_period_seconds
|
self._watchdog_period_seconds = watchdog_period_seconds
|
||||||
|
self._retry_after_seconds = retry_after_seconds
|
||||||
self._stop = Event()
|
self._stop = Event()
|
||||||
self._terminated = Event()
|
self._terminated = Event()
|
||||||
self._queue = queue
|
self._queue = queue
|
||||||
|
@ -103,7 +104,8 @@ class Worker(object):
|
||||||
try:
|
try:
|
||||||
self.watchdog()
|
self.watchdog()
|
||||||
except WorkerUnhealthyException as exc:
|
except WorkerUnhealthyException as exc:
|
||||||
logger.error('The worker has encountered an error via watchdog and will not take new jobs: %s' % exc.message)
|
logger.error('The worker has encountered an error via watchdog and will not take new jobs')
|
||||||
|
logger.error(exc.message)
|
||||||
self.mark_current_incomplete(restore_retry=True)
|
self.mark_current_incomplete(restore_retry=True)
|
||||||
self._stop.set()
|
self._stop.set()
|
||||||
|
|
||||||
|
@ -111,7 +113,7 @@ class Worker(object):
|
||||||
logger.debug('Getting work item from queue.')
|
logger.debug('Getting work item from queue.')
|
||||||
|
|
||||||
with self._current_item_lock:
|
with self._current_item_lock:
|
||||||
self.current_queue_item = self._queue.get()
|
self.current_queue_item = self._queue.get(processing_time=self._reservation_seconds)
|
||||||
|
|
||||||
while True:
|
while True:
|
||||||
# Retrieve the current item in the queue over which to operate. We do so under
|
# Retrieve the current item in the queue over which to operate. We do so under
|
||||||
|
@ -129,12 +131,14 @@ class Worker(object):
|
||||||
self.process_queue_item(job_details)
|
self.process_queue_item(job_details)
|
||||||
self.mark_current_complete()
|
self.mark_current_complete()
|
||||||
|
|
||||||
except JobException:
|
except JobException as jex:
|
||||||
logger.warning('An error occurred processing request: %s', current_queue_item.body)
|
logger.warning('An error occurred processing request: %s', current_queue_item.body)
|
||||||
|
logger.warning('Job exception: %s' % jex)
|
||||||
self.mark_current_incomplete(restore_retry=False)
|
self.mark_current_incomplete(restore_retry=False)
|
||||||
|
|
||||||
except WorkerUnhealthyException as exc:
|
except WorkerUnhealthyException as exc:
|
||||||
logger.error('The worker has encountered an error via the job and will not take new jobs: %s' % exc.message)
|
logger.error('The worker has encountered an error via the job and will not take new jobs')
|
||||||
|
logger.error(exc.message)
|
||||||
self.mark_current_incomplete(restore_retry=True)
|
self.mark_current_incomplete(restore_retry=True)
|
||||||
self._stop.set()
|
self._stop.set()
|
||||||
|
|
||||||
|
@ -190,7 +194,8 @@ class Worker(object):
|
||||||
def mark_current_incomplete(self, restore_retry=False):
|
def mark_current_incomplete(self, restore_retry=False):
|
||||||
with self._current_item_lock:
|
with self._current_item_lock:
|
||||||
if self.current_queue_item is not None:
|
if self.current_queue_item is not None:
|
||||||
self._queue.incomplete(self.current_queue_item, restore_retry=restore_retry)
|
self._queue.incomplete(self.current_queue_item, restore_retry=restore_retry,
|
||||||
|
retry_after=self._retry_after_seconds)
|
||||||
self.current_queue_item = None
|
self.current_queue_item = None
|
||||||
|
|
||||||
def mark_current_complete(self):
|
def mark_current_complete(self):
|
||||||
|
|
Reference in a new issue