2959 lines
103 KiB
Python
2959 lines
103 KiB
Python
import bcrypt
|
|
import logging
|
|
import dateutil.parser
|
|
import json
|
|
import time
|
|
|
|
from datetime import datetime, timedelta, date
|
|
from uuid import uuid4
|
|
|
|
from data.database import (User, Repository, Image, AccessToken, Role, RepositoryPermission,
|
|
Visibility, RepositoryTag, EmailConfirmation, FederatedLogin,
|
|
LoginService, RepositoryBuild, Team, TeamMember, TeamRole,
|
|
LogEntryKind, LogEntry, PermissionPrototype, ImageStorage,
|
|
BuildTriggerService, RepositoryBuildTrigger, NotificationKind,
|
|
Notification, ImageStorageLocation, ImageStoragePlacement,
|
|
ExternalNotificationEvent, ExternalNotificationMethod,
|
|
RepositoryNotification, RepositoryAuthorizedEmail, TeamMemberInvite,
|
|
DerivedImageStorage, ImageStorageTransformation, random_string_generator,
|
|
db, BUILD_PHASE, QuayUserField, ImageStorageSignature, QueueItem,
|
|
ImageStorageSignatureKind, validate_database_url, db_for_update,
|
|
AccessTokenKind, Star, get_epoch_timestamp, RepositoryActionCount)
|
|
from peewee import JOIN_LEFT_OUTER, fn, SQL, IntegrityError
|
|
from util.validation import (validate_username, validate_email, validate_password,
|
|
INVALID_PASSWORD_MESSAGE)
|
|
from util.names import format_robot_username, parse_robot_username
|
|
from util.backoff import exponential_backoff
|
|
|
|
|
|
EXPONENTIAL_BACKOFF_SCALE = timedelta(seconds=1)
|
|
PRESUMED_DEAD_BUILD_AGE = timedelta(days=15)
|
|
|
|
|
|
Namespace = User.alias()
|
|
|
|
|
|
logger = logging.getLogger(__name__)
|
|
|
|
|
|
class Config(object):
|
|
def __init__(self):
|
|
self.app_config = None
|
|
self.store = None
|
|
|
|
config = Config()
|
|
|
|
|
|
class DataModelException(Exception):
|
|
pass
|
|
|
|
|
|
class InvalidEmailAddressException(DataModelException):
|
|
pass
|
|
|
|
|
|
class InvalidUsernameException(DataModelException):
|
|
pass
|
|
|
|
|
|
class InvalidOrganizationException(DataModelException):
|
|
pass
|
|
|
|
|
|
class InvalidRobotException(DataModelException):
|
|
pass
|
|
|
|
|
|
class InvalidTeamException(DataModelException):
|
|
pass
|
|
|
|
|
|
class InvalidTeamMemberException(DataModelException):
|
|
pass
|
|
|
|
|
|
class InvalidPasswordException(DataModelException):
|
|
pass
|
|
|
|
|
|
class InvalidTokenException(DataModelException):
|
|
pass
|
|
|
|
|
|
class InvalidRepositoryBuildException(DataModelException):
|
|
pass
|
|
|
|
|
|
class InvalidNotificationException(DataModelException):
|
|
pass
|
|
|
|
|
|
class InvalidBuildTriggerException(DataModelException):
|
|
pass
|
|
|
|
|
|
class InvalidImageException(DataModelException):
|
|
pass
|
|
|
|
|
|
class TooManyUsersException(DataModelException):
|
|
pass
|
|
|
|
|
|
class UserAlreadyInTeam(DataModelException):
|
|
pass
|
|
|
|
|
|
class TooManyLoginAttemptsException(Exception):
|
|
def __init__(self, message, retry_after):
|
|
super(TooManyLoginAttemptsException, self).__init__(message)
|
|
self.retry_after = retry_after
|
|
|
|
|
|
def _get_repository(namespace_name, repository_name, for_update=False):
|
|
query = (Repository
|
|
.select(Repository, Namespace)
|
|
.join(Namespace, on=(Repository.namespace_user == Namespace.id))
|
|
.where(Namespace.username == namespace_name, Repository.name == repository_name))
|
|
if for_update:
|
|
query = db_for_update(query)
|
|
|
|
return query.get()
|
|
|
|
|
|
def hash_password(password, salt=None):
|
|
salt = salt or bcrypt.gensalt()
|
|
return bcrypt.hashpw(password.encode('utf-8'), salt)
|
|
|
|
|
|
def is_create_user_allowed():
|
|
return True
|
|
|
|
|
|
def create_user(username, password, email, auto_verify=False):
|
|
""" Creates a regular user, if allowed. """
|
|
if not validate_password(password):
|
|
raise InvalidPasswordException(INVALID_PASSWORD_MESSAGE)
|
|
|
|
if not is_create_user_allowed():
|
|
raise TooManyUsersException()
|
|
|
|
created = _create_user(username, email)
|
|
created.password_hash = hash_password(password)
|
|
created.verified = auto_verify
|
|
created.save()
|
|
|
|
return created
|
|
|
|
|
|
def _create_user(username, email):
|
|
if not validate_email(email):
|
|
raise InvalidEmailAddressException('Invalid email address: %s' % email)
|
|
|
|
(username_valid, username_issue) = validate_username(username)
|
|
if not username_valid:
|
|
raise InvalidUsernameException('Invalid username %s: %s' % (username, username_issue))
|
|
|
|
try:
|
|
existing = User.get((User.username == username) | (User.email == email))
|
|
|
|
logger.info('Existing user with same username or email.')
|
|
|
|
# A user already exists with either the same username or email
|
|
if existing.username == username:
|
|
raise InvalidUsernameException('Username has already been taken: %s' %
|
|
username)
|
|
raise InvalidEmailAddressException('Email has already been used: %s' %
|
|
email)
|
|
|
|
except User.DoesNotExist:
|
|
# This is actually the happy path
|
|
logger.debug('Email and username are unique!')
|
|
pass
|
|
|
|
try:
|
|
return User.create(username=username, email=email)
|
|
except Exception as ex:
|
|
raise DataModelException(ex.message)
|
|
|
|
|
|
def is_username_unique(test_username):
|
|
try:
|
|
User.get((User.username == test_username))
|
|
return False
|
|
except User.DoesNotExist:
|
|
return True
|
|
|
|
|
|
def create_organization(name, email, creating_user):
|
|
try:
|
|
# Create the org
|
|
new_org = _create_user(name, email)
|
|
new_org.organization = True
|
|
new_org.save()
|
|
|
|
# Create a team for the owners
|
|
owners_team = create_team('owners', new_org, 'admin')
|
|
|
|
# Add the user who created the org to the owners team
|
|
add_user_to_team(creating_user, owners_team)
|
|
|
|
return new_org
|
|
except InvalidUsernameException:
|
|
msg = ('Invalid organization name: %s Organization names must consist ' +
|
|
'solely of lower case letters, numbers, and underscores. ' +
|
|
'[a-z0-9_]') % name
|
|
raise InvalidOrganizationException(msg)
|
|
|
|
|
|
def create_robot(robot_shortname, parent):
|
|
(username_valid, username_issue) = validate_username(robot_shortname)
|
|
if not username_valid:
|
|
raise InvalidRobotException('The name for the robot \'%s\' is invalid: %s' %
|
|
(robot_shortname, username_issue))
|
|
|
|
username = format_robot_username(parent.username, robot_shortname)
|
|
|
|
try:
|
|
User.get(User.username == username)
|
|
|
|
msg = 'Existing robot with name: %s' % username
|
|
logger.info(msg)
|
|
raise InvalidRobotException(msg)
|
|
|
|
except User.DoesNotExist:
|
|
pass
|
|
|
|
try:
|
|
created = User.create(username=username, robot=True)
|
|
|
|
service = LoginService.get(name='quayrobot')
|
|
password = created.email
|
|
FederatedLogin.create(user=created, service=service,
|
|
service_ident=password)
|
|
|
|
return created, password
|
|
except Exception as ex:
|
|
raise DataModelException(ex.message)
|
|
|
|
def get_robot(robot_shortname, parent):
|
|
robot_username = format_robot_username(parent.username, robot_shortname)
|
|
robot = lookup_robot(robot_username)
|
|
|
|
if not robot:
|
|
msg = ('Could not find robot with username: %s' %
|
|
robot_username)
|
|
raise InvalidRobotException(msg)
|
|
|
|
service = LoginService.get(name='quayrobot')
|
|
login = FederatedLogin.get(FederatedLogin.user == robot, FederatedLogin.service == service)
|
|
|
|
return robot, login.service_ident
|
|
|
|
def lookup_robot(robot_username):
|
|
joined = User.select().join(FederatedLogin).join(LoginService)
|
|
found = list(joined.where(LoginService.name == 'quayrobot',
|
|
User.username == robot_username))
|
|
if not found or len(found) < 1 or not found[0].robot:
|
|
return None
|
|
|
|
return found[0]
|
|
|
|
def verify_robot(robot_username, password):
|
|
result = parse_robot_username(robot_username)
|
|
if result is None:
|
|
raise InvalidRobotException('%s is an invalid robot name' % robot_username)
|
|
|
|
# Find the matching robot.
|
|
query = (User.select()
|
|
.join(FederatedLogin)
|
|
.join(LoginService)
|
|
.where(FederatedLogin.service_ident == password,
|
|
LoginService.name == 'quayrobot',
|
|
User.username == robot_username))
|
|
|
|
try:
|
|
robot = query.get()
|
|
except User.DoesNotExist:
|
|
msg = ('Could not find robot with username: %s and supplied password.' %
|
|
robot_username)
|
|
raise InvalidRobotException(msg)
|
|
|
|
# Find the owner user and ensure it is not disabled.
|
|
try:
|
|
owner = User.get(User.username == result[0])
|
|
except User.DoesNotExist:
|
|
raise InvalidRobotException('Robot %s owner does not exist' % robot_username)
|
|
|
|
if not owner.enabled:
|
|
raise InvalidRobotException('This user has been disabled. Please contact your administrator.')
|
|
|
|
return robot
|
|
|
|
def regenerate_robot_token(robot_shortname, parent):
|
|
robot_username = format_robot_username(parent.username, robot_shortname)
|
|
|
|
robot = lookup_robot(robot_username)
|
|
if not robot:
|
|
raise InvalidRobotException('Could not find robot with username: %s' %
|
|
robot_username)
|
|
|
|
password = random_string_generator(length=64)()
|
|
robot.email = password
|
|
|
|
service = LoginService.get(name='quayrobot')
|
|
login = FederatedLogin.get(FederatedLogin.user == robot, FederatedLogin.service == service)
|
|
login.service_ident = password
|
|
|
|
login.save()
|
|
robot.save()
|
|
|
|
return robot, password
|
|
|
|
def delete_robot(robot_username):
|
|
try:
|
|
robot = User.get(username=robot_username, robot=True)
|
|
robot.delete_instance(recursive=True, delete_nullable=True)
|
|
|
|
except User.DoesNotExist:
|
|
raise InvalidRobotException('Could not find robot with username: %s' %
|
|
robot_username)
|
|
|
|
|
|
def _list_entity_robots(entity_name):
|
|
""" Return the list of robots for the specified entity. This MUST return a query, not a
|
|
materialized list so that callers can use db_for_update.
|
|
"""
|
|
return (User
|
|
.select()
|
|
.join(FederatedLogin)
|
|
.where(User.robot == True, User.username ** (entity_name + '+%')))
|
|
|
|
|
|
class _TupleWrapper(object):
|
|
def __init__(self, data, fields):
|
|
self._data = data
|
|
self._fields = fields
|
|
|
|
def get(self, field):
|
|
return self._data[self._fields.index(field.name + ':' + field.model_class.__name__)]
|
|
|
|
|
|
class TupleSelector(object):
|
|
""" Helper class for selecting tuples from a peewee query and easily accessing
|
|
them as if they were objects.
|
|
"""
|
|
def __init__(self, query, fields):
|
|
self._query = query.select(*fields).tuples()
|
|
self._fields = [field.name + ':' + field.model_class.__name__ for field in fields]
|
|
|
|
def __iter__(self):
|
|
return self._build_iterator()
|
|
|
|
def _build_iterator(self):
|
|
for tuple_data in self._query:
|
|
yield _TupleWrapper(tuple_data, self._fields)
|
|
|
|
|
|
|
|
def list_entity_robot_permission_teams(entity_name, include_permissions=False):
|
|
query = (_list_entity_robots(entity_name))
|
|
|
|
fields = [User.username, FederatedLogin.service_ident]
|
|
if include_permissions:
|
|
query = (query.join(RepositoryPermission, JOIN_LEFT_OUTER,
|
|
on=(RepositoryPermission.user == FederatedLogin.user))
|
|
.join(Repository, JOIN_LEFT_OUTER)
|
|
.switch(User)
|
|
.join(TeamMember, JOIN_LEFT_OUTER)
|
|
.join(Team, JOIN_LEFT_OUTER))
|
|
|
|
fields.append(Repository.name)
|
|
fields.append(Team.name)
|
|
|
|
return TupleSelector(query, fields)
|
|
|
|
|
|
def list_robot_permissions(robot_name):
|
|
return (RepositoryPermission.select(RepositoryPermission, User, Repository)
|
|
.join(Repository)
|
|
.join(Visibility)
|
|
.switch(RepositoryPermission)
|
|
.join(Role)
|
|
.switch(RepositoryPermission)
|
|
.join(User)
|
|
.where(User.username == robot_name, User.robot == True))
|
|
|
|
def convert_user_to_organization(user, admin_user):
|
|
# Change the user to an organization.
|
|
user.organization = True
|
|
|
|
# disable this account for login.
|
|
user.password_hash = None
|
|
user.save()
|
|
|
|
# Clear any federated auth pointing to this user
|
|
FederatedLogin.delete().where(FederatedLogin.user == user).execute()
|
|
|
|
# Create a team for the owners
|
|
owners_team = create_team('owners', user, 'admin')
|
|
|
|
# Add the user who will admin the org to the owners team
|
|
add_user_to_team(admin_user, owners_team)
|
|
|
|
return user
|
|
|
|
|
|
def create_team(name, org, team_role_name, description=''):
|
|
(username_valid, username_issue) = validate_username(name)
|
|
if not username_valid:
|
|
raise InvalidTeamException('Invalid team name %s: %s' % (name, username_issue))
|
|
|
|
if not org.organization:
|
|
raise InvalidOrganizationException('User with name %s is not an org.' %
|
|
org.username)
|
|
|
|
team_role = TeamRole.get(TeamRole.name == team_role_name)
|
|
return Team.create(name=name, organization=org, role=team_role,
|
|
description=description)
|
|
|
|
|
|
def __get_user_admin_teams(org_name, teamname, username):
|
|
Org = User.alias()
|
|
user_teams = Team.select().join(TeamMember).join(User)
|
|
with_org = user_teams.switch(Team).join(Org,
|
|
on=(Org.id == Team.organization))
|
|
with_role = with_org.switch(Team).join(TeamRole)
|
|
admin_teams = with_role.where(User.username == username,
|
|
Org.username == org_name,
|
|
TeamRole.name == 'admin')
|
|
return admin_teams
|
|
|
|
|
|
def remove_team(org_name, team_name, removed_by_username):
|
|
joined = Team.select(Team, TeamRole).join(User).switch(Team).join(TeamRole)
|
|
|
|
found = list(joined.where(User.organization == True,
|
|
User.username == org_name,
|
|
Team.name == team_name))
|
|
if not found:
|
|
raise InvalidTeamException('Team \'%s\' is not a team in org \'%s\'' %
|
|
(team_name, org_name))
|
|
|
|
team = found[0]
|
|
if team.role.name == 'admin':
|
|
admin_teams = list(__get_user_admin_teams(org_name, team_name,
|
|
removed_by_username))
|
|
|
|
if len(admin_teams) <= 1:
|
|
# The team we are trying to remove is the only admin team for this user
|
|
msg = ('Deleting team \'%s\' would remove all admin from user \'%s\'' %
|
|
(team_name, removed_by_username))
|
|
raise DataModelException(msg)
|
|
|
|
team.delete_instance(recursive=True, delete_nullable=True)
|
|
|
|
|
|
def add_or_invite_to_team(inviter, team, user=None, email=None, requires_invite=True):
|
|
# If the user is a member of the organization, then we simply add the
|
|
# user directly to the team. Otherwise, an invite is created for the user/email.
|
|
# We return None if the user was directly added and the invite object if the user was invited.
|
|
if user and requires_invite:
|
|
orgname = team.organization.username
|
|
|
|
# If the user is part of the organization (or a robot), then no invite is required.
|
|
if user.robot:
|
|
requires_invite = False
|
|
if not user.username.startswith(orgname + '+'):
|
|
raise InvalidTeamMemberException('Cannot add the specified robot to this team, ' +
|
|
'as it is not a member of the organization')
|
|
else:
|
|
Org = User.alias()
|
|
found = User.select(User.username)
|
|
found = found.where(User.username == user.username).join(TeamMember).join(Team)
|
|
found = found.join(Org, on=(Org.username == orgname)).limit(1)
|
|
requires_invite = not any(found)
|
|
|
|
# If we have a valid user and no invite is required, simply add the user to the team.
|
|
if user and not requires_invite:
|
|
add_user_to_team(user, team)
|
|
return None
|
|
|
|
email_address = email if not user else None
|
|
return TeamMemberInvite.create(user=user, email=email_address, team=team, inviter=inviter)
|
|
|
|
|
|
def add_user_to_team(user, team):
|
|
try:
|
|
return TeamMember.create(user=user, team=team)
|
|
except Exception:
|
|
raise UserAlreadyInTeam('User \'%s\' is already a member of team \'%s\'' %
|
|
(user.username, team.name))
|
|
|
|
|
|
def remove_user_from_team(org_name, team_name, username, removed_by_username):
|
|
Org = User.alias()
|
|
joined = TeamMember.select().join(User).switch(TeamMember).join(Team)
|
|
with_role = joined.join(TeamRole)
|
|
with_org = with_role.switch(Team).join(Org,
|
|
on=(Org.id == Team.organization))
|
|
found = list(with_org.where(User.username == username,
|
|
Org.username == org_name,
|
|
Team.name == team_name))
|
|
|
|
if not found:
|
|
raise DataModelException('User %s does not belong to team %s' %
|
|
(username, team_name))
|
|
|
|
if username == removed_by_username:
|
|
admin_team_query = __get_user_admin_teams(org_name, team_name, username)
|
|
admin_team_names = {team.name for team in admin_team_query}
|
|
if team_name in admin_team_names and len(admin_team_names) <= 1:
|
|
msg = 'User cannot remove themselves from their only admin team.'
|
|
raise DataModelException(msg)
|
|
|
|
user_in_team = found[0]
|
|
user_in_team.delete_instance()
|
|
|
|
|
|
def get_team_org_role(team):
|
|
return TeamRole.get(TeamRole.id == team.role.id)
|
|
|
|
|
|
def set_team_org_permission(team, team_role_name, set_by_username):
|
|
if team.role.name == 'admin' and team_role_name != 'admin':
|
|
# We need to make sure we're not removing the users only admin role
|
|
user_admin_teams = __get_user_admin_teams(team.organization.username,
|
|
team.name, set_by_username)
|
|
admin_team_set = {admin_team.name for admin_team in user_admin_teams}
|
|
if team.name in admin_team_set and len(admin_team_set) <= 1:
|
|
msg = (('Cannot remove admin from team \'%s\' because calling user ' +
|
|
'would no longer have admin on org \'%s\'') %
|
|
(team.name, team.organization.username))
|
|
raise DataModelException(msg)
|
|
|
|
new_role = TeamRole.get(TeamRole.name == team_role_name)
|
|
team.role = new_role
|
|
team.save()
|
|
return team
|
|
|
|
|
|
def create_federated_user(username, email, service_name, service_id,
|
|
set_password_notification, metadata={}):
|
|
if not is_create_user_allowed():
|
|
raise TooManyUsersException()
|
|
|
|
new_user = _create_user(username, email)
|
|
new_user.verified = True
|
|
new_user.save()
|
|
|
|
service = LoginService.get(LoginService.name == service_name)
|
|
FederatedLogin.create(user=new_user, service=service,
|
|
service_ident=service_id,
|
|
metadata_json=json.dumps(metadata))
|
|
|
|
if set_password_notification:
|
|
create_notification('password_required', new_user)
|
|
|
|
return new_user
|
|
|
|
|
|
def attach_federated_login(user, service_name, service_id, metadata={}):
|
|
service = LoginService.get(LoginService.name == service_name)
|
|
FederatedLogin.create(user=user, service=service, service_ident=service_id,
|
|
metadata_json=json.dumps(metadata))
|
|
return user
|
|
|
|
|
|
def verify_federated_login(service_name, service_id):
|
|
try:
|
|
found = (FederatedLogin
|
|
.select(FederatedLogin, User)
|
|
.join(LoginService)
|
|
.switch(FederatedLogin).join(User)
|
|
.where(FederatedLogin.service_ident == service_id, LoginService.name == service_name)
|
|
.get())
|
|
return found.user
|
|
except FederatedLogin.DoesNotExist:
|
|
return None
|
|
|
|
|
|
def list_federated_logins(user):
|
|
selected = FederatedLogin.select(FederatedLogin.service_ident,
|
|
LoginService.name, FederatedLogin.metadata_json)
|
|
joined = selected.join(LoginService)
|
|
return joined.where(LoginService.name != 'quayrobot',
|
|
FederatedLogin.user == user)
|
|
|
|
|
|
def lookup_federated_login(user, service_name):
|
|
try:
|
|
return list_federated_logins(user).where(LoginService.name == service_name).get()
|
|
except FederatedLogin.DoesNotExist:
|
|
return None
|
|
|
|
def create_confirm_email_code(user, new_email=None):
|
|
if new_email:
|
|
if not validate_email(new_email):
|
|
raise InvalidEmailAddressException('Invalid email address: %s' %
|
|
new_email)
|
|
|
|
code = EmailConfirmation.create(user=user, email_confirm=True,
|
|
new_email=new_email)
|
|
return code
|
|
|
|
|
|
def confirm_user_email(code):
|
|
try:
|
|
code = EmailConfirmation.get(EmailConfirmation.code == code,
|
|
EmailConfirmation.email_confirm == True)
|
|
except EmailConfirmation.DoesNotExist:
|
|
raise DataModelException('Invalid email confirmation code.')
|
|
|
|
user = code.user
|
|
user.verified = True
|
|
|
|
old_email = None
|
|
new_email = code.new_email
|
|
if new_email and new_email != old_email:
|
|
if find_user_by_email(new_email):
|
|
raise DataModelException('E-mail address already used.')
|
|
|
|
old_email = user.email
|
|
user.email = new_email
|
|
|
|
user.save()
|
|
|
|
code.delete_instance()
|
|
|
|
return user, new_email, old_email
|
|
|
|
|
|
def create_reset_password_email_code(email):
|
|
try:
|
|
user = User.get(User.email == email)
|
|
except User.DoesNotExist:
|
|
raise InvalidEmailAddressException('Email address was not found.');
|
|
|
|
if user.organization:
|
|
raise InvalidEmailAddressException('Organizations can not have passwords.')
|
|
|
|
code = EmailConfirmation.create(user=user, pw_reset=True)
|
|
return code
|
|
|
|
|
|
def validate_reset_code(code):
|
|
try:
|
|
code = EmailConfirmation.get(EmailConfirmation.code == code,
|
|
EmailConfirmation.pw_reset == True)
|
|
except EmailConfirmation.DoesNotExist:
|
|
return None
|
|
|
|
user = code.user
|
|
code.delete_instance()
|
|
|
|
return user
|
|
|
|
|
|
def find_user_by_email(email):
|
|
try:
|
|
return User.get(User.email == email)
|
|
except User.DoesNotExist:
|
|
return None
|
|
|
|
|
|
def get_nonrobot_user(username):
|
|
try:
|
|
return User.get(User.username == username, User.organization == False, User.robot == False)
|
|
except User.DoesNotExist:
|
|
return None
|
|
|
|
|
|
def get_user(username):
|
|
try:
|
|
return User.get(User.username == username, User.organization == False)
|
|
except User.DoesNotExist:
|
|
return None
|
|
|
|
|
|
def get_namespace_user(username):
|
|
try:
|
|
return User.get(User.username == username)
|
|
except User.DoesNotExist:
|
|
return None
|
|
|
|
|
|
def get_user_or_org(username):
|
|
try:
|
|
return User.get(User.username == username, User.robot == False)
|
|
except User.DoesNotExist:
|
|
return None
|
|
|
|
|
|
def get_user_by_id(user_db_id):
|
|
try:
|
|
return User.get(User.id == user_db_id, User.organization == False)
|
|
except User.DoesNotExist:
|
|
return None
|
|
|
|
|
|
def get_namespace_by_user_id(namespace_user_db_id):
|
|
try:
|
|
return User.get(User.id == namespace_user_db_id, User.robot == False).username
|
|
except User.DoesNotExist:
|
|
raise InvalidUsernameException('User with id does not exist: %s' % namespace_user_db_id)
|
|
|
|
|
|
def get_user_by_uuid(user_uuid):
|
|
try:
|
|
return User.get(User.uuid == user_uuid, User.organization == False)
|
|
except User.DoesNotExist:
|
|
return None
|
|
|
|
|
|
def get_user_or_org_by_customer_id(customer_id):
|
|
try:
|
|
return User.get(User.stripe_id == customer_id)
|
|
except User.DoesNotExist:
|
|
return None
|
|
|
|
def get_matching_user_namespaces(namespace_prefix, username, limit=10):
|
|
query = (Repository
|
|
.select()
|
|
.join(Namespace, on=(Repository.namespace_user == Namespace.id))
|
|
.switch(Repository)
|
|
.join(Visibility)
|
|
.switch(Repository)
|
|
.join(RepositoryPermission, JOIN_LEFT_OUTER)
|
|
.where(Namespace.username ** (namespace_prefix + '%'))
|
|
.group_by(Repository.namespace_user, Repository))
|
|
|
|
count = 0
|
|
namespaces = {}
|
|
for repo in _filter_to_repos_for_user(query, username):
|
|
if not repo.namespace_user.username in namespaces:
|
|
namespaces[repo.namespace_user.username] = repo.namespace_user
|
|
count = count + 1
|
|
if count >= limit:
|
|
break
|
|
|
|
return namespaces.values()
|
|
|
|
def get_matching_user_teams(team_prefix, user, limit=10):
|
|
query = (Team.select()
|
|
.join(User)
|
|
.switch(Team)
|
|
.join(TeamMember)
|
|
.where(TeamMember.user == user, Team.name ** (team_prefix + '%'))
|
|
.distinct(Team.id)
|
|
.limit(limit))
|
|
|
|
return query
|
|
|
|
|
|
def get_matching_robots(name_prefix, username, limit=10):
|
|
admined_orgs = (get_user_organizations(username)
|
|
.switch(Team)
|
|
.join(TeamRole)
|
|
.where(TeamRole.name == 'admin'))
|
|
|
|
prefix_checks = False
|
|
|
|
for org in admined_orgs:
|
|
prefix_checks = prefix_checks | (User.username ** (org.username + '+' + name_prefix + '%'))
|
|
|
|
prefix_checks = prefix_checks | (User.username ** (username + '+' + name_prefix + '%'))
|
|
|
|
return User.select().where(prefix_checks).limit(limit)
|
|
|
|
|
|
def get_matching_admined_teams(team_prefix, user, limit=10):
|
|
admined_orgs = (get_user_organizations(user.username)
|
|
.switch(Team)
|
|
.join(TeamRole)
|
|
.where(TeamRole.name == 'admin'))
|
|
|
|
query = (Team.select()
|
|
.join(User)
|
|
.switch(Team)
|
|
.join(TeamMember)
|
|
.where(Team.name ** (team_prefix + '%'), Team.organization << (admined_orgs))
|
|
.distinct(Team.id)
|
|
.limit(limit))
|
|
|
|
return query
|
|
|
|
|
|
def get_matching_teams(team_prefix, organization):
|
|
query = Team.select().where(Team.name ** (team_prefix + '%'),
|
|
Team.organization == organization)
|
|
return query.limit(10)
|
|
|
|
|
|
def get_matching_users(username_prefix, robot_namespace=None,
|
|
organization=None):
|
|
direct_user_query = (User.username ** (username_prefix + '%') &
|
|
(User.organization == False) & (User.robot == False))
|
|
|
|
if robot_namespace:
|
|
robot_prefix = format_robot_username(robot_namespace, username_prefix)
|
|
direct_user_query = (direct_user_query |
|
|
(User.username ** (robot_prefix + '%') &
|
|
(User.robot == True)))
|
|
|
|
query = (User
|
|
.select(User.username, User.email, User.robot)
|
|
.group_by(User.username, User.email, User.robot)
|
|
.where(direct_user_query))
|
|
|
|
if organization:
|
|
query = (query
|
|
.select(User.username, User.email, User.robot, fn.Sum(Team.id))
|
|
.join(TeamMember, JOIN_LEFT_OUTER)
|
|
.join(Team, JOIN_LEFT_OUTER, on=((Team.id == TeamMember.team) &
|
|
(Team.organization == organization))))
|
|
|
|
|
|
class MatchingUserResult(object):
|
|
def __init__(self, *args):
|
|
self.username = args[0]
|
|
self.email = args[1]
|
|
self.robot = args[2]
|
|
|
|
if organization:
|
|
self.is_org_member = (args[3] != None)
|
|
else:
|
|
self.is_org_member = None
|
|
|
|
|
|
return (MatchingUserResult(*args) for args in query.tuples().limit(10))
|
|
|
|
|
|
def verify_user(username_or_email, password):
|
|
# Make sure we didn't get any unicode for the username.
|
|
try:
|
|
str(username_or_email)
|
|
except ValueError:
|
|
return None
|
|
|
|
try:
|
|
fetched = User.get((User.username == username_or_email) |
|
|
(User.email == username_or_email))
|
|
except User.DoesNotExist:
|
|
return None
|
|
|
|
now = datetime.utcnow()
|
|
|
|
if fetched.invalid_login_attempts > 0:
|
|
can_retry_at = exponential_backoff(fetched.invalid_login_attempts, EXPONENTIAL_BACKOFF_SCALE,
|
|
fetched.last_invalid_login)
|
|
|
|
if can_retry_at > now:
|
|
retry_after = can_retry_at - now
|
|
raise TooManyLoginAttemptsException('Too many login attempts.', retry_after.total_seconds())
|
|
|
|
if (fetched.password_hash and
|
|
hash_password(password, fetched.password_hash) == fetched.password_hash):
|
|
if fetched.invalid_login_attempts > 0:
|
|
fetched.invalid_login_attempts = 0
|
|
fetched.save()
|
|
|
|
return fetched
|
|
|
|
fetched.invalid_login_attempts += 1
|
|
fetched.last_invalid_login = now
|
|
fetched.save()
|
|
|
|
# We weren't able to authorize the user
|
|
return None
|
|
|
|
|
|
def get_user_organizations(username):
|
|
UserAlias = User.alias()
|
|
all_teams = User.select().distinct().join(Team).join(TeamMember)
|
|
with_user = all_teams.join(UserAlias, on=(UserAlias.id == TeamMember.user))
|
|
return with_user.where(User.organization == True,
|
|
UserAlias.username == username)
|
|
|
|
|
|
def get_organization(name):
|
|
try:
|
|
return User.get(username=name, organization=True)
|
|
except User.DoesNotExist:
|
|
raise InvalidOrganizationException('Organization does not exist: %s' %
|
|
name)
|
|
|
|
|
|
def get_organization_team(orgname, teamname):
|
|
joined = Team.select().join(User)
|
|
query = joined.where(Team.name == teamname, User.organization == True,
|
|
User.username == orgname).limit(1)
|
|
result = list(query)
|
|
if not result:
|
|
raise InvalidTeamException('Team does not exist: %s/%s', orgname,
|
|
teamname)
|
|
|
|
return result[0]
|
|
|
|
|
|
def get_organization_members_with_teams(organization, membername = None):
|
|
joined = TeamMember.select().annotate(Team).annotate(User)
|
|
query = joined.where(Team.organization == organization)
|
|
if membername:
|
|
query = query.where(User.username == membername)
|
|
return query
|
|
|
|
def get_organization_team_members(teamid):
|
|
joined = User.select().join(TeamMember).join(Team)
|
|
query = joined.where(Team.id == teamid)
|
|
return query
|
|
|
|
def get_organization_team_member_invites(teamid):
|
|
joined = TeamMemberInvite.select().join(Team).join(User)
|
|
query = joined.where(Team.id == teamid)
|
|
return query
|
|
|
|
def get_organization_member_set(orgname):
|
|
Org = User.alias()
|
|
org_users = (User.select(User.username)
|
|
.join(TeamMember)
|
|
.join(Team)
|
|
.join(Org, on=(Org.id == Team.organization))
|
|
.where(Org.username == orgname)
|
|
.distinct())
|
|
return {user.username for user in org_users}
|
|
|
|
|
|
def get_teams_within_org(organization):
|
|
return Team.select().where(Team.organization == organization)
|
|
|
|
|
|
def get_user_teams_within_org(username, organization):
|
|
joined = Team.select().join(TeamMember).join(User)
|
|
return joined.where(Team.organization == organization,
|
|
User.username == username)
|
|
|
|
|
|
def get_visible_repository_count(username=None, include_public=True,
|
|
namespace=None):
|
|
query = _visible_repository_query(username=username,
|
|
include_public=include_public,
|
|
namespace=namespace)
|
|
return query.count()
|
|
|
|
|
|
def get_visible_repositories(username=None, include_public=True, page=None,
|
|
limit=None, sort=False, namespace=None, namespace_only=False):
|
|
query = _visible_repository_query(username=username, include_public=include_public, page=page,
|
|
limit=limit, namespace=namespace,
|
|
select_models=[Repository, Namespace, Visibility])
|
|
|
|
if sort:
|
|
query = query.order_by(Repository.id.desc())
|
|
|
|
if limit:
|
|
query = query.limit(limit)
|
|
|
|
if namespace and namespace_only:
|
|
query = query.where(Namespace.username == namespace)
|
|
|
|
return query
|
|
|
|
|
|
def _visible_repository_query(username=None, include_public=True, limit=None,
|
|
page=None, namespace=None, select_models=[]):
|
|
query = (Repository
|
|
.select(*select_models) # MySQL/RDS complains is there are selected models for counts.
|
|
.distinct()
|
|
.join(Visibility)
|
|
.switch(Repository)
|
|
.join(Namespace, on=(Repository.namespace_user == Namespace.id))
|
|
.switch(Repository)
|
|
.join(RepositoryPermission, JOIN_LEFT_OUTER))
|
|
|
|
query = _filter_to_repos_for_user(query, username, namespace, include_public)
|
|
if page:
|
|
query = query.paginate(page, limit)
|
|
elif limit:
|
|
query = query.limit(limit)
|
|
|
|
return query
|
|
|
|
|
|
def _filter_to_repos_for_user(query, username=None, namespace=None, include_public=True):
|
|
if not include_public and not username:
|
|
return Repository.select().where(Repository.id == '-1')
|
|
|
|
where_clause = None
|
|
if username:
|
|
UserThroughTeam = User.alias()
|
|
Org = User.alias()
|
|
AdminTeam = Team.alias()
|
|
AdminTeamMember = TeamMember.alias()
|
|
AdminUser = User.alias()
|
|
|
|
query = (query
|
|
.switch(RepositoryPermission)
|
|
.join(User, JOIN_LEFT_OUTER)
|
|
.switch(RepositoryPermission)
|
|
.join(Team, JOIN_LEFT_OUTER)
|
|
.join(TeamMember, JOIN_LEFT_OUTER)
|
|
.join(UserThroughTeam, JOIN_LEFT_OUTER, on=(UserThroughTeam.id == TeamMember.user))
|
|
.switch(Repository)
|
|
.join(Org, JOIN_LEFT_OUTER, on=(Repository.namespace_user == Org.id))
|
|
.join(AdminTeam, JOIN_LEFT_OUTER, on=(Org.id == AdminTeam.organization))
|
|
.join(TeamRole, JOIN_LEFT_OUTER, on=(AdminTeam.role == TeamRole.id))
|
|
.switch(AdminTeam)
|
|
.join(AdminTeamMember, JOIN_LEFT_OUTER, on=(AdminTeam.id == AdminTeamMember.team))
|
|
.join(AdminUser, JOIN_LEFT_OUTER, on=(AdminTeamMember.user == AdminUser.id)))
|
|
|
|
where_clause = ((User.username == username) | (UserThroughTeam.username == username) |
|
|
((AdminUser.username == username) & (TeamRole.name == 'admin')))
|
|
|
|
if namespace:
|
|
where_clause = where_clause & (Namespace.username == namespace)
|
|
|
|
# TODO(jschorr, jake): Figure out why the old join on Visibility was so darn slow and
|
|
# remove this hack.
|
|
if include_public:
|
|
new_clause = (Repository.visibility == _get_public_repo_visibility())
|
|
if where_clause:
|
|
where_clause = where_clause | new_clause
|
|
else:
|
|
where_clause = new_clause
|
|
|
|
return query.where(where_clause)
|
|
|
|
|
|
_public_repo_visibility_cache = None
|
|
def _get_public_repo_visibility():
|
|
global _public_repo_visibility_cache
|
|
|
|
if not _public_repo_visibility_cache:
|
|
_public_repo_visibility_cache = Visibility.get(name='public')
|
|
|
|
return _public_repo_visibility_cache
|
|
|
|
|
|
def get_sorted_matching_repositories(prefix, only_public, checker, limit=10):
|
|
""" Returns repositories matching the given prefix string and passing the given checker
|
|
function.
|
|
"""
|
|
last_week = datetime.now() - timedelta(weeks=1)
|
|
results = []
|
|
existing_ids = []
|
|
|
|
def get_search_results(search_clause, with_count=False):
|
|
if len(results) >= limit:
|
|
return
|
|
|
|
select_items = [Repository, Namespace]
|
|
if with_count:
|
|
select_items.append(fn.Sum(RepositoryActionCount.count).alias('count'))
|
|
|
|
query = (Repository.select(*select_items)
|
|
.join(Namespace, JOIN_LEFT_OUTER, on=(Namespace.id == Repository.namespace_user))
|
|
.switch(Repository)
|
|
.where(search_clause)
|
|
.group_by(Repository, Namespace))
|
|
|
|
if only_public:
|
|
query = query.where(Repository.visibility == _get_public_repo_visibility())
|
|
|
|
if existing_ids:
|
|
query = query.where(~(Repository.id << existing_ids))
|
|
|
|
if with_count:
|
|
query = (query.switch(Repository)
|
|
.join(RepositoryActionCount)
|
|
.where(RepositoryActionCount.date >= last_week)
|
|
.order_by(fn.Sum(RepositoryActionCount.count).desc()))
|
|
|
|
for result in query:
|
|
if len(results) >= limit:
|
|
return results
|
|
|
|
# Note: We compare IDs here, instead of objects, because calling .visibility on the
|
|
# Repository will kick off a new SQL query to retrieve that visibility enum value. We don't
|
|
# join the visibility table in SQL, as well, because it is ungodly slow in MySQL :-/
|
|
result.is_public = result.visibility_id == _get_public_repo_visibility().id
|
|
result.count = result.count if with_count else 0
|
|
|
|
if not checker(result):
|
|
continue
|
|
|
|
results.append(result)
|
|
existing_ids.append(result.id)
|
|
|
|
# For performance reasons, we conduct the repo name and repo namespace searches on their
|
|
# own. This also affords us the ability to give higher precedence to repository names matching
|
|
# over namespaces, which is semantically correct.
|
|
get_search_results(Repository.name ** (prefix + '%'), with_count=True)
|
|
get_search_results(Repository.name ** (prefix + '%'), with_count=False)
|
|
|
|
get_search_results(Namespace.username ** (prefix + '%'), with_count=True)
|
|
get_search_results(Namespace.username ** (prefix + '%'), with_count=False)
|
|
|
|
return results
|
|
|
|
|
|
def get_matching_repositories(repo_term, username=None, limit=10, include_public=True):
|
|
namespace_term = repo_term
|
|
name_term = repo_term
|
|
|
|
visible = get_visible_repositories(username, include_public=include_public)
|
|
|
|
search_clauses = (Repository.name ** ('%' + name_term + '%') |
|
|
Namespace.username ** ('%' + namespace_term + '%'))
|
|
|
|
# Handle the case where the user has already entered a namespace path.
|
|
if repo_term.find('/') > 0:
|
|
parts = repo_term.split('/', 1)
|
|
namespace_term = '/'.join(parts[:-1])
|
|
name_term = parts[-1]
|
|
|
|
search_clauses = (Repository.name ** ('%' + name_term + '%') &
|
|
Namespace.username ** ('%' + namespace_term + '%'))
|
|
|
|
return visible.where(search_clauses).limit(limit)
|
|
|
|
|
|
def change_password(user, new_password):
|
|
if not validate_password(new_password):
|
|
raise InvalidPasswordException(INVALID_PASSWORD_MESSAGE)
|
|
|
|
pw_hash = hash_password(new_password)
|
|
user.invalid_login_attempts = 0
|
|
user.password_hash = pw_hash
|
|
user.uuid = str(uuid4())
|
|
user.save()
|
|
|
|
# Remove any password required notifications for the user.
|
|
delete_notifications_by_kind(user, 'password_required')
|
|
|
|
|
|
def change_username(user_id, new_username):
|
|
(username_valid, username_issue) = validate_username(new_username)
|
|
if not username_valid:
|
|
raise InvalidUsernameException('Invalid username %s: %s' % (new_username, username_issue))
|
|
|
|
with config.app_config['DB_TRANSACTION_FACTORY'](db):
|
|
# Reload the user for update
|
|
user = db_for_update(User.select().where(User.id == user_id)).get()
|
|
|
|
# Rename the robots
|
|
for robot in db_for_update(_list_entity_robots(user.username)):
|
|
_, robot_shortname = parse_robot_username(robot.username)
|
|
new_robot_name = format_robot_username(new_username, robot_shortname)
|
|
robot.username = new_robot_name
|
|
robot.save()
|
|
|
|
# Rename the user
|
|
user.username = new_username
|
|
user.save()
|
|
return user
|
|
|
|
|
|
def change_invoice_email(user, invoice_email):
|
|
user.invoice_email = invoice_email
|
|
user.save()
|
|
|
|
|
|
def change_user_tag_expiration(user, tag_expiration_s):
|
|
user.removed_tag_expiration_s = tag_expiration_s
|
|
user.save()
|
|
|
|
|
|
def update_email(user, new_email, auto_verify=False):
|
|
try:
|
|
user.email = new_email
|
|
user.verified = auto_verify
|
|
user.save()
|
|
except IntegrityError:
|
|
raise DataModelException('E-mail address already used')
|
|
|
|
|
|
def get_all_user_permissions(user):
|
|
UserThroughTeam = User.alias()
|
|
|
|
base_query = (RepositoryPermission
|
|
.select(RepositoryPermission, Role, Repository, Namespace)
|
|
.join(Role)
|
|
.switch(RepositoryPermission)
|
|
.join(Repository)
|
|
.join(Namespace, on=(Repository.namespace_user == Namespace.id))
|
|
.switch(RepositoryPermission))
|
|
|
|
direct = (base_query
|
|
.clone()
|
|
.join(User)
|
|
.where(User.id == user))
|
|
|
|
team = (base_query
|
|
.clone()
|
|
.join(Team)
|
|
.join(TeamMember)
|
|
.join(UserThroughTeam, on=(UserThroughTeam.id == TeamMember.user))
|
|
.where(UserThroughTeam.id == user))
|
|
|
|
return direct | team
|
|
|
|
|
|
def delete_prototype_permission(org, uid):
|
|
found = get_prototype_permission(org, uid)
|
|
if not found:
|
|
return None
|
|
|
|
found.delete_instance()
|
|
return found
|
|
|
|
|
|
def get_prototype_permission(org, uid):
|
|
try:
|
|
return PermissionPrototype.get(PermissionPrototype.org == org,
|
|
PermissionPrototype.uuid == uid)
|
|
except PermissionPrototype.DoesNotExist:
|
|
return None
|
|
|
|
|
|
def get_prototype_permissions(org):
|
|
ActivatingUser = User.alias()
|
|
DelegateUser = User.alias()
|
|
query = (PermissionPrototype
|
|
.select()
|
|
.where(PermissionPrototype.org == org)
|
|
.join(ActivatingUser, JOIN_LEFT_OUTER,
|
|
on=(ActivatingUser.id == PermissionPrototype.activating_user))
|
|
.join(DelegateUser, JOIN_LEFT_OUTER,
|
|
on=(DelegateUser.id == PermissionPrototype.delegate_user))
|
|
.join(Team, JOIN_LEFT_OUTER,
|
|
on=(Team.id == PermissionPrototype.delegate_team))
|
|
.join(Role, JOIN_LEFT_OUTER, on=(Role.id == PermissionPrototype.role)))
|
|
return query
|
|
|
|
|
|
def update_prototype_permission(org, uid, role_name):
|
|
found = get_prototype_permission(org, uid)
|
|
if not found:
|
|
return None
|
|
|
|
new_role = Role.get(Role.name == role_name)
|
|
found.role = new_role
|
|
found.save()
|
|
return found
|
|
|
|
|
|
def add_prototype_permission(org, role_name, activating_user,
|
|
delegate_user=None, delegate_team=None):
|
|
new_role = Role.get(Role.name == role_name)
|
|
return PermissionPrototype.create(org=org, role=new_role,
|
|
activating_user=activating_user,
|
|
delegate_user=delegate_user, delegate_team=delegate_team)
|
|
|
|
|
|
def get_org_wide_permissions(user):
|
|
Org = User.alias()
|
|
team_with_role = Team.select(Team, Org, TeamRole).join(TeamRole)
|
|
with_org = team_with_role.switch(Team).join(Org, on=(Team.organization ==
|
|
Org.id))
|
|
with_user = with_org.switch(Team).join(TeamMember).join(User)
|
|
return with_user.where(User.id == user, Org.organization == True)
|
|
|
|
|
|
def get_all_repo_teams(namespace_name, repository_name):
|
|
return (RepositoryPermission.select(Team.name, Role.name, RepositoryPermission)
|
|
.join(Team)
|
|
.switch(RepositoryPermission)
|
|
.join(Role)
|
|
.switch(RepositoryPermission)
|
|
.join(Repository)
|
|
.join(Namespace, on=(Repository.namespace_user == Namespace.id))
|
|
.where(Namespace.username == namespace_name, Repository.name == repository_name))
|
|
|
|
|
|
def get_all_repo_users(namespace_name, repository_name):
|
|
return (RepositoryPermission.select(User.username, User.email, User.robot, Role.name,
|
|
RepositoryPermission)
|
|
.join(User)
|
|
.switch(RepositoryPermission)
|
|
.join(Role)
|
|
.switch(RepositoryPermission)
|
|
.join(Repository)
|
|
.join(Namespace, on=(Repository.namespace_user == Namespace.id))
|
|
.where(Namespace.username == namespace_name, Repository.name == repository_name))
|
|
|
|
|
|
def get_all_repo_users_transitive_via_teams(namespace_name, repository_name):
|
|
return (User
|
|
.select()
|
|
.distinct()
|
|
.join(TeamMember)
|
|
.join(Team)
|
|
.join(RepositoryPermission)
|
|
.join(Repository)
|
|
.join(Namespace, on=(Repository.namespace_user == Namespace.id))
|
|
.where(Namespace.username == namespace_name, Repository.name == repository_name))
|
|
|
|
|
|
def get_all_repo_users_transitive(namespace_name, repository_name):
|
|
# Load the users found via teams and directly via permissions.
|
|
via_teams = get_all_repo_users_transitive_via_teams(namespace_name, repository_name)
|
|
directly = [perm.user for perm in get_all_repo_users(namespace_name, repository_name)]
|
|
|
|
# Filter duplicates.
|
|
user_set = set()
|
|
|
|
def check_add(u):
|
|
if u.username in user_set:
|
|
return False
|
|
|
|
user_set.add(u.username)
|
|
return True
|
|
|
|
return [user for user in list(directly) + list(via_teams) if check_add(user)]
|
|
|
|
|
|
def get_repository_for_resource(resource_key):
|
|
try:
|
|
return (Repository
|
|
.select(Repository, Namespace)
|
|
.join(Namespace, on=(Repository.namespace_user == Namespace.id))
|
|
.switch(Repository)
|
|
.join(RepositoryBuild)
|
|
.where(RepositoryBuild.resource_key == resource_key)
|
|
.get())
|
|
except Repository.DoesNotExist:
|
|
return None
|
|
|
|
|
|
def lookup_repository(repo_id):
|
|
try:
|
|
return Repository.get(Repository.id == repo_id)
|
|
except Repository.DoesNotExist:
|
|
return None
|
|
|
|
|
|
def get_repository(namespace_name, repository_name):
|
|
try:
|
|
return _get_repository(namespace_name, repository_name)
|
|
except Repository.DoesNotExist:
|
|
return None
|
|
|
|
|
|
def get_image(repo, dockerfile_id):
|
|
try:
|
|
return Image.get(Image.docker_image_id == dockerfile_id, Image.repository == repo)
|
|
except Image.DoesNotExist:
|
|
return None
|
|
|
|
|
|
def find_child_image(repo, parent_image, command):
|
|
try:
|
|
return (Image.select()
|
|
.join(ImageStorage)
|
|
.switch(Image)
|
|
.where(Image.ancestors % '%/' + parent_image.id + '/%',
|
|
ImageStorage.command == command)
|
|
.order_by(ImageStorage.created.desc())
|
|
.get())
|
|
except Image.DoesNotExist:
|
|
return None
|
|
|
|
|
|
def get_repo_image(namespace_name, repository_name, docker_image_id):
|
|
def limit_to_image_id(query):
|
|
return query.where(Image.docker_image_id == docker_image_id).limit(1)
|
|
|
|
query = _get_repository_images(namespace_name, repository_name, limit_to_image_id)
|
|
try:
|
|
return query.get()
|
|
except Image.DoesNotExist:
|
|
return None
|
|
|
|
|
|
def get_repo_image_extended(namespace_name, repository_name, docker_image_id):
|
|
def limit_to_image_id(query):
|
|
return query.where(Image.docker_image_id == docker_image_id).limit(1)
|
|
|
|
images = _get_repository_images_base(namespace_name, repository_name, limit_to_image_id)
|
|
if not images:
|
|
return None
|
|
|
|
return images[0]
|
|
|
|
def is_repository_public(repository):
|
|
return repository.visibility == _get_public_repo_visibility()
|
|
|
|
def repository_is_public(namespace_name, repository_name):
|
|
try:
|
|
(Repository
|
|
.select()
|
|
.join(Namespace, on=(Repository.namespace_user == Namespace.id))
|
|
.switch(Repository)
|
|
.join(Visibility)
|
|
.where(Namespace.username == namespace_name, Repository.name == repository_name,
|
|
Visibility.name == 'public')
|
|
.get())
|
|
return True
|
|
except Repository.DoesNotExist:
|
|
return False
|
|
|
|
|
|
def set_repository_visibility(repo, visibility):
|
|
visibility_obj = Visibility.get(name=visibility)
|
|
if not visibility_obj:
|
|
return
|
|
|
|
repo.visibility = visibility_obj
|
|
repo.save()
|
|
|
|
|
|
def __apply_default_permissions(repo, proto_query, name_property,
|
|
create_permission_func):
|
|
final_protos = {}
|
|
for proto in proto_query:
|
|
applies_to = proto.delegate_team or proto.delegate_user
|
|
name = getattr(applies_to, name_property)
|
|
# We will skip the proto if it is pre-empted by a more important proto
|
|
if name in final_protos and proto.activating_user is None:
|
|
continue
|
|
|
|
# By this point, it is either a user specific proto, or there is no
|
|
# proto yet, so we can safely assume it applies
|
|
final_protos[name] = (applies_to, proto.role)
|
|
|
|
for delegate, role in final_protos.values():
|
|
create_permission_func(delegate, repo, role)
|
|
|
|
|
|
def create_repository(namespace, name, creating_user, visibility='private'):
|
|
private = Visibility.get(name=visibility)
|
|
namespace_user = User.get(username=namespace)
|
|
repo = Repository.create(name=name, visibility=private, namespace_user=namespace_user)
|
|
admin = Role.get(name='admin')
|
|
|
|
if creating_user and not creating_user.organization:
|
|
RepositoryPermission.create(user=creating_user, repository=repo,
|
|
role=admin)
|
|
|
|
if creating_user.username != namespace:
|
|
# Permission prototypes only work for orgs
|
|
org = get_organization(namespace)
|
|
user_clause = ((PermissionPrototype.activating_user == creating_user) |
|
|
(PermissionPrototype.activating_user >> None))
|
|
|
|
team_protos = (PermissionPrototype
|
|
.select()
|
|
.where(PermissionPrototype.org == org, user_clause,
|
|
PermissionPrototype.delegate_user >> None))
|
|
|
|
def create_team_permission(team, repo, role):
|
|
RepositoryPermission.create(team=team, repository=repo, role=role)
|
|
|
|
__apply_default_permissions(repo, team_protos, 'name',
|
|
create_team_permission)
|
|
|
|
user_protos = (PermissionPrototype
|
|
.select()
|
|
.where(PermissionPrototype.org == org, user_clause,
|
|
PermissionPrototype.delegate_team >> None))
|
|
|
|
def create_user_permission(user, repo, role):
|
|
# The creating user always gets admin anyway
|
|
if user.username == creating_user.username:
|
|
return
|
|
|
|
RepositoryPermission.create(user=user, repository=repo, role=role)
|
|
|
|
__apply_default_permissions(repo, user_protos, 'username',
|
|
create_user_permission)
|
|
|
|
return repo
|
|
|
|
|
|
def __translate_ancestry(old_ancestry, translations, repository, username, preferred_location):
|
|
if old_ancestry == '/':
|
|
return '/'
|
|
|
|
def translate_id(old_id, docker_image_id):
|
|
logger.debug('Translating id: %s', old_id)
|
|
if old_id not in translations:
|
|
image_in_repo = find_create_or_link_image(docker_image_id, repository, username,
|
|
translations, preferred_location)
|
|
translations[old_id] = image_in_repo.id
|
|
return translations[old_id]
|
|
|
|
# Select all the ancestor Docker IDs in a single query.
|
|
old_ids = [int(id_str) for id_str in old_ancestry.split('/')[1:-1]]
|
|
query = Image.select(Image.id, Image.docker_image_id).where(Image.id << old_ids)
|
|
old_images = {i.id: i.docker_image_id for i in query}
|
|
|
|
# Translate the old images into new ones.
|
|
new_ids = [str(translate_id(old_id, old_images[old_id])) for old_id in old_ids]
|
|
return '/%s/' % '/'.join(new_ids)
|
|
|
|
|
|
def _create_storage(location_name):
|
|
storage = ImageStorage.create()
|
|
location = ImageStorageLocation.get(name=location_name)
|
|
ImageStoragePlacement.create(location=location, storage=storage)
|
|
storage.locations = {location_name}
|
|
return storage
|
|
|
|
|
|
def _find_or_link_image(existing_image, repository, username, translations, preferred_location):
|
|
# TODO(jake): This call is currently recursively done under a single transaction. Can we make
|
|
# it instead be done under a set of transactions?
|
|
with config.app_config['DB_TRANSACTION_FACTORY'](db):
|
|
# Check for an existing image, under the transaction, to make sure it doesn't already exist.
|
|
repo_image = get_repo_image(repository.namespace_user.username, repository.name,
|
|
existing_image.docker_image_id)
|
|
if repo_image:
|
|
return repo_image
|
|
|
|
# Make sure the existing base image still exists.
|
|
try:
|
|
to_copy = Image.select().join(ImageStorage).where(Image.id == existing_image.id).get()
|
|
|
|
msg = 'Linking image to existing storage with docker id: %s and uuid: %s'
|
|
logger.debug(msg, existing_image.docker_image_id, to_copy.storage.uuid)
|
|
|
|
new_image_ancestry = __translate_ancestry(to_copy.ancestors, translations, repository,
|
|
username, preferred_location)
|
|
|
|
storage = to_copy.storage
|
|
storage.locations = {placement.location.name
|
|
for placement in storage.imagestorageplacement_set}
|
|
|
|
new_image = Image.create(docker_image_id=existing_image.docker_image_id,
|
|
repository=repository, storage=storage,
|
|
ancestors=new_image_ancestry)
|
|
|
|
logger.debug('Storing translation %s -> %s', existing_image.id, new_image.id)
|
|
translations[existing_image.id] = new_image.id
|
|
return new_image
|
|
except Image.DoesNotExist:
|
|
return None
|
|
|
|
|
|
def find_create_or_link_image(docker_image_id, repository, username, translations,
|
|
preferred_location):
|
|
|
|
# First check for the image existing in the repository. If found, we simply return it.
|
|
repo_image = get_repo_image(repository.namespace_user.username, repository.name,
|
|
docker_image_id)
|
|
if repo_image:
|
|
return repo_image
|
|
|
|
# We next check to see if there is an existing storage the new image can link to.
|
|
existing_image_query = (Image
|
|
.select(Image, ImageStorage)
|
|
.distinct()
|
|
.join(ImageStorage)
|
|
.switch(Image)
|
|
.join(Repository)
|
|
.join(RepositoryPermission, JOIN_LEFT_OUTER)
|
|
.switch(Repository)
|
|
.join(Namespace, on=(Repository.namespace_user == Namespace.id))
|
|
.where(ImageStorage.uploading == False))
|
|
|
|
existing_image_query = (_filter_to_repos_for_user(existing_image_query, username)
|
|
.where(Image.docker_image_id == docker_image_id))
|
|
|
|
# If there is an existing image, we try to translate its ancestry and copy its storage.
|
|
new_image = None
|
|
try:
|
|
logger.debug('Looking up existing image for ID: %s', docker_image_id)
|
|
existing_image = existing_image_query.get()
|
|
|
|
logger.debug('Existing image %s found for ID: %s', existing_image.id, docker_image_id)
|
|
new_image = _find_or_link_image(existing_image, repository, username, translations,
|
|
preferred_location)
|
|
if new_image:
|
|
return new_image
|
|
except Image.DoesNotExist:
|
|
logger.debug('No existing image found for ID: %s', docker_image_id)
|
|
pass
|
|
|
|
# Otherwise, create a new storage directly.
|
|
with config.app_config['DB_TRANSACTION_FACTORY'](db):
|
|
# Final check for an existing image, under the transaction.
|
|
repo_image = get_repo_image(repository.namespace_user.username, repository.name,
|
|
docker_image_id)
|
|
if repo_image:
|
|
return repo_image
|
|
|
|
logger.debug('Creating new storage for docker id: %s', docker_image_id)
|
|
storage = _create_storage(preferred_location)
|
|
|
|
return Image.create(docker_image_id=docker_image_id,
|
|
repository=repository, storage=storage,
|
|
ancestors='/')
|
|
|
|
|
|
def find_or_create_storage_signature(storage, signature_kind):
|
|
found = lookup_storage_signature(storage, signature_kind)
|
|
if found is None:
|
|
kind = ImageStorageSignatureKind.get(name=signature_kind)
|
|
found = ImageStorageSignature.create(storage=storage, kind=kind)
|
|
|
|
return found
|
|
|
|
|
|
def lookup_storage_signature(storage, signature_kind):
|
|
kind = ImageStorageSignatureKind.get(name=signature_kind)
|
|
try:
|
|
return (ImageStorageSignature
|
|
.select()
|
|
.where(ImageStorageSignature.storage == storage,
|
|
ImageStorageSignature.kind == kind)
|
|
.get())
|
|
except ImageStorageSignature.DoesNotExist:
|
|
return None
|
|
|
|
|
|
def find_derived_storage(source, transformation_name):
|
|
try:
|
|
found = (ImageStorage
|
|
.select(ImageStorage, DerivedImageStorage)
|
|
.join(DerivedImageStorage, on=(ImageStorage.id == DerivedImageStorage.derivative))
|
|
.join(ImageStorageTransformation)
|
|
.where(DerivedImageStorage.source == source,
|
|
ImageStorageTransformation.name == transformation_name)
|
|
.get())
|
|
|
|
found.locations = {placement.location.name for placement in found.imagestorageplacement_set}
|
|
return found
|
|
except ImageStorage.DoesNotExist:
|
|
return None
|
|
|
|
|
|
def find_or_create_derived_storage(source, transformation_name, preferred_location):
|
|
existing = find_derived_storage(source, transformation_name)
|
|
if existing is not None:
|
|
return existing
|
|
|
|
logger.debug('Creating storage dervied from source: %s', source.uuid)
|
|
trans = ImageStorageTransformation.get(name=transformation_name)
|
|
new_storage = _create_storage(preferred_location)
|
|
DerivedImageStorage.create(source=source, derivative=new_storage, transformation=trans)
|
|
return new_storage
|
|
|
|
|
|
def delete_derived_storage_by_uuid(storage_uuid):
|
|
try:
|
|
image_storage = get_storage_by_uuid(storage_uuid)
|
|
except InvalidImageException:
|
|
return
|
|
|
|
try:
|
|
DerivedImageStorage.get(derivative=image_storage)
|
|
except DerivedImageStorage.DoesNotExist:
|
|
return
|
|
|
|
image_storage.delete_instance(recursive=True)
|
|
|
|
|
|
def get_storage_by_uuid(storage_uuid):
|
|
placements = list(ImageStoragePlacement
|
|
.select(ImageStoragePlacement, ImageStorage, ImageStorageLocation)
|
|
.join(ImageStorageLocation)
|
|
.switch(ImageStoragePlacement)
|
|
.join(ImageStorage)
|
|
.where(ImageStorage.uuid == storage_uuid))
|
|
|
|
if not placements:
|
|
raise InvalidImageException('No storage found with uuid: %s', storage_uuid)
|
|
|
|
found = placements[0].storage
|
|
found.locations = {placement.location.name for placement in placements}
|
|
|
|
return found
|
|
|
|
|
|
def set_image_size(docker_image_id, namespace_name, repository_name, image_size, uncompressed_size):
|
|
try:
|
|
image = (Image
|
|
.select(Image, ImageStorage)
|
|
.join(Repository)
|
|
.join(Namespace, on=(Repository.namespace_user == Namespace.id))
|
|
.switch(Image)
|
|
.join(ImageStorage, JOIN_LEFT_OUTER)
|
|
.where(Repository.name == repository_name, Namespace.username == namespace_name,
|
|
Image.docker_image_id == docker_image_id)
|
|
.get())
|
|
|
|
except Image.DoesNotExist:
|
|
raise DataModelException('No image with specified id and repository')
|
|
|
|
image.storage.image_size = image_size
|
|
image.storage.uncompressed_size = uncompressed_size
|
|
|
|
ancestors = image.ancestors.split('/')[1:-1]
|
|
if ancestors:
|
|
try:
|
|
# TODO(jschorr): Switch to this faster route once we have full ancestor aggregate_size
|
|
# parent_image = Image.get(Image.id == ancestors[-1])
|
|
# total_size = image_size + parent_image.storage.aggregate_size
|
|
total_size = (ImageStorage.select(fn.Sum(ImageStorage.image_size))
|
|
.join(Image)
|
|
.where(Image.id << ancestors)
|
|
.scalar()) + image_size
|
|
|
|
image.storage.aggregate_size = total_size
|
|
except Image.DoesNotExist:
|
|
pass
|
|
else:
|
|
image.storage.aggregate_size = image_size
|
|
|
|
image.storage.save()
|
|
|
|
return image
|
|
|
|
|
|
def set_image_metadata(docker_image_id, namespace_name, repository_name, created_date_str, comment,
|
|
command, parent=None):
|
|
with config.app_config['DB_TRANSACTION_FACTORY'](db):
|
|
query = (Image
|
|
.select(Image, ImageStorage)
|
|
.join(Repository)
|
|
.join(Namespace, on=(Repository.namespace_user == Namespace.id))
|
|
.switch(Image)
|
|
.join(ImageStorage)
|
|
.where(Repository.name == repository_name, Namespace.username == namespace_name,
|
|
Image.docker_image_id == docker_image_id))
|
|
|
|
try:
|
|
fetched = db_for_update(query).get()
|
|
except Image.DoesNotExist:
|
|
raise DataModelException('No image with specified id and repository')
|
|
|
|
# We cleanup any old checksum in case it's a retry after a fail
|
|
fetched.storage.checksum = None
|
|
fetched.storage.created = datetime.now()
|
|
|
|
if created_date_str is not None:
|
|
try:
|
|
fetched.storage.created = dateutil.parser.parse(created_date_str).replace(tzinfo=None)
|
|
except:
|
|
# parse raises different exceptions, so we cannot use a specific kind of handler here.
|
|
pass
|
|
|
|
fetched.storage.comment = comment
|
|
fetched.storage.command = command
|
|
|
|
if parent:
|
|
fetched.ancestors = '%s%s/' % (parent.ancestors, parent.id)
|
|
|
|
fetched.save()
|
|
fetched.storage.save()
|
|
return fetched
|
|
|
|
def _get_repository_images(namespace_name, repository_name, query_modifier):
|
|
query = (Image
|
|
.select()
|
|
.join(Repository)
|
|
.join(Namespace, on=(Repository.namespace_user == Namespace.id))
|
|
.where(Repository.name == repository_name, Namespace.username == namespace_name))
|
|
|
|
query = query_modifier(query)
|
|
return query
|
|
|
|
def _get_repository_images_base(namespace_name, repository_name, query_modifier):
|
|
query = (ImageStoragePlacement
|
|
.select(ImageStoragePlacement, Image, ImageStorage, ImageStorageLocation)
|
|
.join(ImageStorageLocation)
|
|
.switch(ImageStoragePlacement)
|
|
.join(ImageStorage, JOIN_LEFT_OUTER)
|
|
.join(Image)
|
|
.join(Repository)
|
|
.join(Namespace, on=(Repository.namespace_user == Namespace.id))
|
|
.where(Repository.name == repository_name, Namespace.username == namespace_name))
|
|
|
|
query = query_modifier(query)
|
|
|
|
location_list = list(query)
|
|
|
|
images = {}
|
|
for location in location_list:
|
|
# Make sure we're always retrieving the same image object.
|
|
image = location.storage.image
|
|
|
|
# Set the storage to the one we got from the location, to prevent another query
|
|
image.storage = location.storage
|
|
|
|
if not image.id in images:
|
|
images[image.id] = image
|
|
image.storage.locations = set()
|
|
else:
|
|
image = images[image.id]
|
|
|
|
# Add the location to the image's locations set.
|
|
image.storage.locations.add(location.location.name)
|
|
|
|
return images.values()
|
|
|
|
|
|
def lookup_repository_images(namespace_name, repository_name, docker_image_ids):
|
|
return (Image
|
|
.select()
|
|
.join(Repository)
|
|
.join(Namespace, on=(Repository.namespace_user == Namespace.id))
|
|
.where(Repository.name == repository_name, Namespace.username == namespace_name,
|
|
Image.docker_image_id << docker_image_ids))
|
|
|
|
def get_matching_repository_images(namespace_name, repository_name, docker_image_ids):
|
|
def modify_query(q):
|
|
return q.where(Image.docker_image_id << docker_image_ids)
|
|
|
|
return _get_repository_images_base(namespace_name, repository_name, modify_query)
|
|
|
|
|
|
def get_repository_images_without_placements(repository, with_ancestor=None):
|
|
query = (Image
|
|
.select(Image, ImageStorage)
|
|
.join(ImageStorage)
|
|
.where(Image.repository == repository))
|
|
|
|
if with_ancestor:
|
|
ancestors_string = '%s%s/' % (with_ancestor.ancestors, with_ancestor.id)
|
|
query = query.where((Image.ancestors ** (ancestors_string + '%')) |
|
|
(Image.id == with_ancestor.id))
|
|
|
|
return query
|
|
|
|
|
|
def get_repository_images(namespace_name, repository_name):
|
|
return _get_repository_images_base(namespace_name, repository_name, lambda q: q)
|
|
|
|
|
|
def _tag_alive(query, now_ts=None):
|
|
if now_ts is None:
|
|
now_ts = get_epoch_timestamp()
|
|
return query.where((RepositoryTag.lifetime_end_ts >> None) |
|
|
(RepositoryTag.lifetime_end_ts > now_ts))
|
|
|
|
|
|
def list_repository_tag_history(repository, limit=100, specific_tag=None):
|
|
query = (RepositoryTag
|
|
.select(RepositoryTag, Image)
|
|
.join(Image)
|
|
.where(RepositoryTag.repository == repository)
|
|
.where(RepositoryTag.hidden == False)
|
|
.order_by(RepositoryTag.lifetime_start_ts.desc())
|
|
.limit(limit))
|
|
|
|
if specific_tag:
|
|
query = query.where(RepositoryTag.name == specific_tag)
|
|
|
|
return query
|
|
|
|
|
|
def list_repository_tags(namespace_name, repository_name, include_hidden=False,
|
|
include_storage=False):
|
|
|
|
toSelect = (RepositoryTag, Image)
|
|
if include_storage:
|
|
toSelect = (RepositoryTag, Image, ImageStorage)
|
|
|
|
query = _tag_alive(RepositoryTag
|
|
.select(*toSelect)
|
|
.join(Repository)
|
|
.join(Namespace, on=(Repository.namespace_user == Namespace.id))
|
|
.switch(RepositoryTag)
|
|
.join(Image)
|
|
.where(Repository.name == repository_name,
|
|
Namespace.username == namespace_name))
|
|
|
|
if not include_hidden:
|
|
query = query.where(RepositoryTag.hidden == False)
|
|
|
|
if include_storage:
|
|
query = query.switch(Image).join(ImageStorage)
|
|
|
|
return query
|
|
|
|
|
|
def _garbage_collect_tags(namespace_name, repository_name):
|
|
# We do this without using a join to prevent holding read locks on the repository table
|
|
repo = _get_repository(namespace_name, repository_name)
|
|
expired_time = get_epoch_timestamp() - repo.namespace_user.removed_tag_expiration_s
|
|
|
|
tags_to_delete = list(RepositoryTag
|
|
.select(RepositoryTag.id)
|
|
.where(RepositoryTag.repository == repo,
|
|
~(RepositoryTag.lifetime_end_ts >> None),
|
|
(RepositoryTag.lifetime_end_ts <= expired_time))
|
|
.order_by(RepositoryTag.id))
|
|
if len(tags_to_delete) > 0:
|
|
(RepositoryTag
|
|
.delete()
|
|
.where(RepositoryTag.id << tags_to_delete)
|
|
.execute())
|
|
|
|
|
|
def garbage_collect_repository(namespace_name, repository_name):
|
|
storage_id_whitelist = {}
|
|
|
|
_garbage_collect_tags(namespace_name, repository_name)
|
|
|
|
with config.app_config['DB_TRANSACTION_FACTORY'](db):
|
|
# TODO (jake): We could probably select this and all the images in a single query using
|
|
# a different kind of join.
|
|
|
|
# Get a list of all images used by tags in the repository
|
|
tag_query = (RepositoryTag
|
|
.select(RepositoryTag, Image, ImageStorage)
|
|
.join(Image)
|
|
.join(ImageStorage, JOIN_LEFT_OUTER)
|
|
.switch(RepositoryTag)
|
|
.join(Repository)
|
|
.join(Namespace, on=(Repository.namespace_user == Namespace.id))
|
|
.where(Repository.name == repository_name, Namespace.username == namespace_name))
|
|
|
|
referenced_anscestors = set()
|
|
for tag in tag_query:
|
|
# The anscestor list is in the format '/1/2/3/', extract just the ids
|
|
anscestor_id_strings = tag.image.ancestors.split('/')[1:-1]
|
|
ancestor_list = [int(img_id_str) for img_id_str in anscestor_id_strings]
|
|
referenced_anscestors = referenced_anscestors.union(set(ancestor_list))
|
|
referenced_anscestors.add(tag.image.id)
|
|
|
|
all_repo_images = _get_repository_images(namespace_name, repository_name, lambda q: q)
|
|
all_images = {int(img.id): img for img in all_repo_images}
|
|
to_remove = set(all_images.keys()).difference(referenced_anscestors)
|
|
|
|
if len(to_remove) > 0:
|
|
logger.info('Cleaning up unreferenced images: %s', to_remove)
|
|
storage_id_whitelist = {all_images[to_remove_id].storage.id for to_remove_id in to_remove}
|
|
Image.delete().where(Image.id << list(to_remove)).execute()
|
|
|
|
if len(to_remove) > 0:
|
|
logger.info('Garbage collecting storage for images: %s', to_remove)
|
|
_garbage_collect_storage(storage_id_whitelist)
|
|
|
|
|
|
def _garbage_collect_storage(storage_id_whitelist):
|
|
if len(storage_id_whitelist) == 0:
|
|
return
|
|
|
|
def placements_query_to_paths_set(placements_query):
|
|
return {(placement.location.name, config.store.image_path(placement.storage.uuid))
|
|
for placement in placements_query}
|
|
|
|
def orphaned_storage_query(select_base_query, candidates, group_by):
|
|
return (select_base_query
|
|
.switch(ImageStorage)
|
|
.join(Image, JOIN_LEFT_OUTER)
|
|
.switch(ImageStorage)
|
|
.join(DerivedImageStorage, JOIN_LEFT_OUTER,
|
|
on=(ImageStorage.id == DerivedImageStorage.derivative))
|
|
.where(ImageStorage.id << list(candidates))
|
|
.group_by(*group_by)
|
|
.having((fn.Count(Image.id) == 0) & (fn.Count(DerivedImageStorage.id) == 0)))
|
|
|
|
# Note: We remove the derived image storage in its own transaction as a way to reduce the
|
|
# time that the transaction holds on the database indicies. This could result in a derived
|
|
# image storage being deleted for an image storage which is later reused during this time,
|
|
# but since these are caches anyway, it isn't terrible and worth the tradeoff (for now).
|
|
logger.debug('Garbage collecting derived storage from candidates: %s', storage_id_whitelist)
|
|
with config.app_config['DB_TRANSACTION_FACTORY'](db):
|
|
# Find out which derived storages will be removed, and add them to the whitelist
|
|
# The comma after ImageStorage.id is VERY important, it makes it a tuple, which is a sequence
|
|
orphaned_from_candidates = list(orphaned_storage_query(ImageStorage.select(ImageStorage.id),
|
|
storage_id_whitelist,
|
|
(ImageStorage.id,)))
|
|
|
|
if len(orphaned_from_candidates) > 0:
|
|
derived_to_remove = (ImageStorage
|
|
.select(ImageStorage.id)
|
|
.join(DerivedImageStorage,
|
|
on=(ImageStorage.id == DerivedImageStorage.derivative))
|
|
.where(DerivedImageStorage.source << orphaned_from_candidates))
|
|
storage_id_whitelist.update({derived.id for derived in derived_to_remove})
|
|
|
|
# Remove the dervived image storages with sources of orphaned storages
|
|
(DerivedImageStorage
|
|
.delete()
|
|
.where(DerivedImageStorage.source << orphaned_from_candidates)
|
|
.execute())
|
|
|
|
# Note: Both of these deletes must occur in the same transaction (unfortunately) because a
|
|
# storage without any placement is invalid, and a placement cannot exist without a storage.
|
|
# TODO(jake): We might want to allow for null storages on placements, which would allow us to
|
|
# delete the storages, then delete the placements in a non-transaction.
|
|
logger.debug('Garbage collecting storages from candidates: %s', storage_id_whitelist)
|
|
with config.app_config['DB_TRANSACTION_FACTORY'](db):
|
|
# Track all of the data that should be removed from blob storage
|
|
placements_to_remove = list(orphaned_storage_query(ImageStoragePlacement
|
|
.select(ImageStoragePlacement,
|
|
ImageStorage,
|
|
ImageStorageLocation)
|
|
.join(ImageStorageLocation)
|
|
.switch(ImageStoragePlacement)
|
|
.join(ImageStorage),
|
|
storage_id_whitelist,
|
|
(ImageStorage, ImageStoragePlacement,
|
|
ImageStorageLocation)))
|
|
|
|
paths_to_remove = placements_query_to_paths_set(placements_to_remove)
|
|
|
|
# Remove the placements for orphaned storages
|
|
if len(placements_to_remove) > 0:
|
|
placement_ids_to_remove = [placement.id for placement in placements_to_remove]
|
|
placements_removed = (ImageStoragePlacement
|
|
.delete()
|
|
.where(ImageStoragePlacement.id << placement_ids_to_remove)
|
|
.execute())
|
|
logger.debug('Removed %s image storage placements', placements_removed)
|
|
|
|
# Remove all orphaned storages
|
|
# The comma after ImageStorage.id is VERY important, it makes it a tuple, which is a sequence
|
|
orphaned_storages = list(orphaned_storage_query(ImageStorage.select(ImageStorage.id),
|
|
storage_id_whitelist,
|
|
(ImageStorage.id,)).alias('osq'))
|
|
if len(orphaned_storages) > 0:
|
|
storages_removed = (ImageStorage
|
|
.delete()
|
|
.where(ImageStorage.id << orphaned_storages)
|
|
.execute())
|
|
logger.debug('Removed %s image storage records', storages_removed)
|
|
|
|
# We are going to make the conscious decision to not delete image storage blobs inside
|
|
# transactions.
|
|
# This may end up producing garbage in s3, trading off for higher availability in the database.
|
|
for location_name, image_path in paths_to_remove:
|
|
logger.debug('Removing %s from %s', image_path, location_name)
|
|
config.store.remove({location_name}, image_path)
|
|
|
|
|
|
def get_tag_image(namespace_name, repository_name, tag_name):
|
|
def limit_to_tag(query):
|
|
return _tag_alive(query
|
|
.switch(Image)
|
|
.join(RepositoryTag)
|
|
.where(RepositoryTag.name == tag_name))
|
|
|
|
images = _get_repository_images_base(namespace_name, repository_name, limit_to_tag)
|
|
if not images:
|
|
raise DataModelException('Unable to find image for tag.')
|
|
else:
|
|
return images[0]
|
|
|
|
def get_image_by_id(namespace_name, repository_name, docker_image_id):
|
|
image = get_repo_image_extended(namespace_name, repository_name, docker_image_id)
|
|
if not image:
|
|
raise DataModelException('Unable to find image \'%s\' for repo \'%s/%s\'' %
|
|
(docker_image_id, namespace_name, repository_name))
|
|
return image
|
|
|
|
|
|
def get_parent_images(namespace_name, repository_name, image_obj):
|
|
""" Returns a list of parent Image objects in chronilogical order. """
|
|
parents = image_obj.ancestors
|
|
parent_db_ids = parents.strip('/').split('/')
|
|
|
|
if parent_db_ids == ['']:
|
|
return []
|
|
|
|
def filter_to_parents(query):
|
|
return query.where(Image.id << parent_db_ids)
|
|
|
|
parents = _get_repository_images_base(namespace_name, repository_name, filter_to_parents)
|
|
|
|
id_to_image = {unicode(image.id): image for image in parents}
|
|
|
|
return [id_to_image[parent_id] for parent_id in parent_db_ids]
|
|
|
|
|
|
def create_or_update_tag(namespace_name, repository_name, tag_name,
|
|
tag_docker_image_id, reversion=False):
|
|
try:
|
|
repo = _get_repository(namespace_name, repository_name)
|
|
except Repository.DoesNotExist:
|
|
raise DataModelException('Invalid repository %s/%s' % (namespace_name, repository_name))
|
|
|
|
now_ts = get_epoch_timestamp()
|
|
|
|
with config.app_config['DB_TRANSACTION_FACTORY'](db):
|
|
try:
|
|
tag = db_for_update(_tag_alive(RepositoryTag
|
|
.select()
|
|
.where(RepositoryTag.repository == repo,
|
|
RepositoryTag.name == tag_name), now_ts)).get()
|
|
tag.lifetime_end_ts = now_ts
|
|
tag.save()
|
|
except RepositoryTag.DoesNotExist:
|
|
pass
|
|
|
|
try:
|
|
image = Image.get(Image.docker_image_id == tag_docker_image_id, Image.repository == repo)
|
|
except Image.DoesNotExist:
|
|
raise DataModelException('Invalid image with id: %s' % tag_docker_image_id)
|
|
|
|
return RepositoryTag.create(repository=repo, image=image, name=tag_name,
|
|
lifetime_start_ts=now_ts, reversion=reversion)
|
|
|
|
def delete_tag(namespace_name, repository_name, tag_name):
|
|
now_ts = get_epoch_timestamp()
|
|
with config.app_config['DB_TRANSACTION_FACTORY'](db):
|
|
try:
|
|
query = _tag_alive(RepositoryTag
|
|
.select(RepositoryTag, Repository)
|
|
.join(Repository)
|
|
.join(Namespace, on=(Repository.namespace_user == Namespace.id))
|
|
.where(Repository.name == repository_name,
|
|
Namespace.username == namespace_name,
|
|
RepositoryTag.name == tag_name), now_ts)
|
|
found = db_for_update(query).get()
|
|
except RepositoryTag.DoesNotExist:
|
|
msg = ('Invalid repository tag \'%s\' on repository \'%s/%s\'' %
|
|
(tag_name, namespace_name, repository_name))
|
|
raise DataModelException(msg)
|
|
|
|
found.lifetime_end_ts = now_ts
|
|
found.save()
|
|
|
|
|
|
def create_temporary_hidden_tag(repo, image, expiration_s):
|
|
""" Create a tag with a defined timeline, that will not appear in the UI or CLI. Returns the name
|
|
of the temporary tag. """
|
|
now_ts = get_epoch_timestamp()
|
|
expire_ts = now_ts + expiration_s
|
|
tag_name = str(uuid4())
|
|
RepositoryTag.create(repository=repo, image=image, name=tag_name, lifetime_start_ts=now_ts,
|
|
lifetime_end_ts=expire_ts, hidden=True)
|
|
return tag_name
|
|
|
|
|
|
def purge_all_repository_tags(namespace_name, repository_name):
|
|
""" Immediately purge all repository tags without respecting the lifeline procedure """
|
|
try:
|
|
repo = _get_repository(namespace_name, repository_name)
|
|
except Repository.DoesNotExist:
|
|
raise DataModelException('Invalid repository \'%s/%s\'' %
|
|
(namespace_name, repository_name))
|
|
RepositoryTag.delete().where(RepositoryTag.repository == repo.id).execute()
|
|
|
|
|
|
def __entity_permission_repo_query(entity_id, entity_table, entity_id_property, namespace_name,
|
|
repository_name):
|
|
""" This method works for both users and teams. """
|
|
|
|
return (RepositoryPermission
|
|
.select(entity_table, Repository, Namespace, Role, RepositoryPermission)
|
|
.join(entity_table)
|
|
.switch(RepositoryPermission)
|
|
.join(Role)
|
|
.switch(RepositoryPermission)
|
|
.join(Repository)
|
|
.join(Namespace, on=(Repository.namespace_user == Namespace.id))
|
|
.where(Repository.name == repository_name, Namespace.username == namespace_name,
|
|
entity_id_property == entity_id))
|
|
|
|
|
|
def get_user_reponame_permission(username, namespace_name, repository_name):
|
|
fetched = list(__entity_permission_repo_query(username, User, User.username,
|
|
namespace_name,
|
|
repository_name))
|
|
if not fetched:
|
|
raise DataModelException('User does not have permission for repo.')
|
|
|
|
return fetched[0]
|
|
|
|
|
|
def get_team_reponame_permission(team_name, namespace_name, repository_name):
|
|
fetched = list(__entity_permission_repo_query(team_name, Team, Team.name,
|
|
namespace_name,
|
|
repository_name))
|
|
if not fetched:
|
|
raise DataModelException('Team does not have permission for repo.')
|
|
|
|
return fetched[0]
|
|
|
|
|
|
def delete_user_permission(username, namespace_name, repository_name):
|
|
if username == namespace_name:
|
|
raise DataModelException('Namespace owner must always be admin.')
|
|
|
|
fetched = list(__entity_permission_repo_query(username, User, User.username,
|
|
namespace_name,
|
|
repository_name))
|
|
if not fetched:
|
|
raise DataModelException('User does not have permission for repo.')
|
|
|
|
fetched[0].delete_instance()
|
|
|
|
|
|
def delete_team_permission(team_name, namespace_name, repository_name):
|
|
fetched = list(__entity_permission_repo_query(team_name, Team, Team.name,
|
|
namespace_name,
|
|
repository_name))
|
|
if not fetched:
|
|
raise DataModelException('Team does not have permission for repo.')
|
|
|
|
fetched[0].delete_instance()
|
|
|
|
|
|
def __set_entity_repo_permission(entity, permission_entity_property,
|
|
namespace_name, repository_name, role_name):
|
|
repo = _get_repository(namespace_name, repository_name)
|
|
new_role = Role.get(Role.name == role_name)
|
|
|
|
# Fetch any existing permission for this entity on the repo
|
|
try:
|
|
entity_attr = getattr(RepositoryPermission, permission_entity_property)
|
|
perm = RepositoryPermission.get(entity_attr == entity,
|
|
RepositoryPermission.repository == repo)
|
|
perm.role = new_role
|
|
perm.save()
|
|
return perm
|
|
except RepositoryPermission.DoesNotExist:
|
|
set_entity_kwargs = {permission_entity_property: entity}
|
|
new_perm = RepositoryPermission.create(repository=repo, role=new_role,
|
|
**set_entity_kwargs)
|
|
return new_perm
|
|
|
|
|
|
def set_user_repo_permission(username, namespace_name, repository_name,
|
|
role_name):
|
|
if username == namespace_name:
|
|
raise DataModelException('Namespace owner must always be admin.')
|
|
|
|
try:
|
|
user = User.get(User.username == username)
|
|
except User.DoesNotExist:
|
|
raise InvalidUsernameException('Invalid username: %s' % username)
|
|
return __set_entity_repo_permission(user, 'user', namespace_name,
|
|
repository_name, role_name)
|
|
|
|
|
|
def set_team_repo_permission(team_name, namespace_name, repository_name,
|
|
role_name):
|
|
team = list(Team.select().join(User).where(Team.name == team_name,
|
|
User.username == namespace_name))
|
|
if not team:
|
|
raise DataModelException('No team \'%s\' in organization \'%s\'.' %
|
|
(team_name, namespace_name))
|
|
|
|
return __set_entity_repo_permission(team[0], 'team', namespace_name,
|
|
repository_name, role_name)
|
|
|
|
|
|
def purge_repository(namespace_name, repository_name):
|
|
# Delete all tags to allow gc to reclaim storage
|
|
purge_all_repository_tags(namespace_name, repository_name)
|
|
|
|
# Gc to remove the images and storage
|
|
garbage_collect_repository(namespace_name, repository_name)
|
|
|
|
# Delete the rest of the repository metadata
|
|
fetched = _get_repository(namespace_name, repository_name)
|
|
fetched.delete_instance(recursive=True, delete_nullable=True)
|
|
|
|
|
|
def get_private_repo_count(username):
|
|
return (Repository
|
|
.select()
|
|
.join(Visibility)
|
|
.switch(Repository)
|
|
.join(Namespace, on=(Repository.namespace_user == Namespace.id))
|
|
.where(Namespace.username == username, Visibility.name == 'private')
|
|
.count())
|
|
|
|
|
|
def create_access_token(repository, role, kind=None, friendly_name=None):
|
|
role = Role.get(Role.name == role)
|
|
kind_ref = None
|
|
if kind is not None:
|
|
kind_ref = AccessTokenKind.get(AccessTokenKind.name == kind)
|
|
|
|
new_token = AccessToken.create(repository=repository, temporary=True,
|
|
role=role, kind=kind_ref, friendly_name=friendly_name)
|
|
return new_token
|
|
|
|
|
|
def create_delegate_token(namespace_name, repository_name, friendly_name,
|
|
role='read'):
|
|
read_only = Role.get(name=role)
|
|
repo = _get_repository(namespace_name, repository_name)
|
|
new_token = AccessToken.create(repository=repo, role=read_only,
|
|
friendly_name=friendly_name, temporary=False)
|
|
return new_token
|
|
|
|
|
|
def get_repository_delegate_tokens(namespace_name, repository_name):
|
|
return (AccessToken
|
|
.select(AccessToken, Role)
|
|
.join(Repository)
|
|
.join(Namespace, on=(Repository.namespace_user == Namespace.id))
|
|
.switch(AccessToken)
|
|
.join(Role)
|
|
.switch(AccessToken)
|
|
.join(RepositoryBuildTrigger, JOIN_LEFT_OUTER)
|
|
.where(Repository.name == repository_name, Namespace.username == namespace_name,
|
|
AccessToken.temporary == False, RepositoryBuildTrigger.uuid >> None))
|
|
|
|
|
|
def get_repo_delegate_token(namespace_name, repository_name, code):
|
|
repo_query = get_repository_delegate_tokens(namespace_name, repository_name)
|
|
|
|
try:
|
|
return repo_query.where(AccessToken.code == code).get()
|
|
except AccessToken.DoesNotExist:
|
|
raise InvalidTokenException('Unable to find token with code: %s' % code)
|
|
|
|
|
|
def set_repo_delegate_token_role(namespace_name, repository_name, code, role):
|
|
token = get_repo_delegate_token(namespace_name, repository_name, code)
|
|
|
|
if role != 'read' and role != 'write':
|
|
raise DataModelException('Invalid role for delegate token: %s' % role)
|
|
|
|
new_role = Role.get(Role.name == role)
|
|
token.role = new_role
|
|
token.save()
|
|
|
|
return token
|
|
|
|
|
|
def delete_delegate_token(namespace_name, repository_name, code):
|
|
token = get_repo_delegate_token(namespace_name, repository_name, code)
|
|
token.delete_instance(recursive=True)
|
|
return token
|
|
|
|
|
|
def load_token_data(code):
|
|
""" Load the permissions for any token by code. """
|
|
try:
|
|
return (AccessToken
|
|
.select(AccessToken, Repository, Namespace, Role)
|
|
.join(Role)
|
|
.switch(AccessToken)
|
|
.join(Repository)
|
|
.join(Namespace, on=(Repository.namespace_user == Namespace.id))
|
|
.where(AccessToken.code == code)
|
|
.get())
|
|
|
|
except AccessToken.DoesNotExist:
|
|
raise InvalidTokenException('Invalid delegate token code: %s' % code)
|
|
|
|
|
|
def _get_build_base_query():
|
|
return (RepositoryBuild
|
|
.select(RepositoryBuild, RepositoryBuildTrigger, BuildTriggerService, Repository,
|
|
Namespace, User)
|
|
.join(Repository)
|
|
.join(Namespace, on=(Repository.namespace_user == Namespace.id))
|
|
.switch(RepositoryBuild)
|
|
.join(User, JOIN_LEFT_OUTER)
|
|
.switch(RepositoryBuild)
|
|
.join(RepositoryBuildTrigger, JOIN_LEFT_OUTER)
|
|
.join(BuildTriggerService, JOIN_LEFT_OUTER)
|
|
.order_by(RepositoryBuild.started.desc()))
|
|
|
|
|
|
def get_repository_build(build_uuid):
|
|
try:
|
|
return _get_build_base_query().where(RepositoryBuild.uuid == build_uuid).get()
|
|
|
|
except RepositoryBuild.DoesNotExist:
|
|
msg = 'Unable to locate a build by id: %s' % build_uuid
|
|
raise InvalidRepositoryBuildException(msg)
|
|
|
|
|
|
def list_repository_builds(namespace_name, repository_name, limit,
|
|
include_inactive=True, since=None):
|
|
query = (_get_build_base_query()
|
|
.where(Repository.name == repository_name, Namespace.username == namespace_name)
|
|
.limit(limit))
|
|
|
|
if since is not None:
|
|
query = query.where(RepositoryBuild.started >= since)
|
|
|
|
if not include_inactive:
|
|
query = query.where(RepositoryBuild.phase != 'error',
|
|
RepositoryBuild.phase != 'complete')
|
|
|
|
return query
|
|
|
|
|
|
def get_recent_repository_build(namespace_name, repository_name):
|
|
query = list_repository_builds(namespace_name, repository_name, 1)
|
|
try:
|
|
return query.get()
|
|
except RepositoryBuild.DoesNotExist:
|
|
return None
|
|
|
|
|
|
def create_repository_build(repo, access_token, job_config_obj, dockerfile_id,
|
|
display_name, trigger=None, pull_robot_name=None):
|
|
pull_robot = None
|
|
if pull_robot_name:
|
|
pull_robot = lookup_robot(pull_robot_name)
|
|
|
|
return RepositoryBuild.create(repository=repo, access_token=access_token,
|
|
job_config=json.dumps(job_config_obj),
|
|
display_name=display_name, trigger=trigger,
|
|
resource_key=dockerfile_id,
|
|
pull_robot=pull_robot)
|
|
|
|
|
|
def get_pull_robot_name(trigger):
|
|
if not trigger.pull_robot:
|
|
return None
|
|
|
|
return trigger.pull_robot.username
|
|
|
|
|
|
def get_pull_credentials(robotname):
|
|
robot = lookup_robot(robotname)
|
|
if not robot:
|
|
return None
|
|
|
|
try:
|
|
login_info = FederatedLogin.get(user=robot)
|
|
except FederatedLogin.DoesNotExist:
|
|
return None
|
|
|
|
return {
|
|
'username': robot.username,
|
|
'password': login_info.service_ident,
|
|
'registry': '%s://%s/v1/' % (config.app_config['PREFERRED_URL_SCHEME'],
|
|
config.app_config['SERVER_HOSTNAME']),
|
|
}
|
|
|
|
|
|
def create_repo_notification(repo, event_name, method_name, config):
|
|
event = ExternalNotificationEvent.get(ExternalNotificationEvent.name == event_name)
|
|
method = ExternalNotificationMethod.get(ExternalNotificationMethod.name == method_name)
|
|
|
|
return RepositoryNotification.create(repository=repo, event=event, method=method,
|
|
config_json=json.dumps(config))
|
|
|
|
|
|
def get_repo_notification(uuid):
|
|
try:
|
|
return (RepositoryNotification
|
|
.select(RepositoryNotification, Repository, Namespace)
|
|
.join(Repository)
|
|
.join(Namespace, on=(Repository.namespace_user == Namespace.id))
|
|
.where(RepositoryNotification.uuid == uuid)
|
|
.get())
|
|
except RepositoryNotification.DoesNotExist:
|
|
raise InvalidNotificationException('No repository notification found with id: %s' % uuid)
|
|
|
|
|
|
def delete_repo_notification(namespace_name, repository_name, uuid):
|
|
found = get_repo_notification(uuid)
|
|
if (found.repository.namespace_user.username != namespace_name or
|
|
found.repository.name != repository_name):
|
|
raise InvalidNotificationException('No repository notifiation found with id: %s' % uuid)
|
|
found.delete_instance()
|
|
return found
|
|
|
|
|
|
def list_repo_notifications(namespace_name, repository_name, event_name=None):
|
|
query = (RepositoryNotification
|
|
.select(RepositoryNotification, Repository, Namespace)
|
|
.join(Repository)
|
|
.join(Namespace, on=(Repository.namespace_user == Namespace.id))
|
|
.where(Namespace.username == namespace_name, Repository.name == repository_name))
|
|
|
|
if event_name:
|
|
query = (query
|
|
.switch(RepositoryNotification)
|
|
.join(ExternalNotificationEvent)
|
|
.where(ExternalNotificationEvent.name == event_name))
|
|
|
|
return query
|
|
|
|
|
|
def list_logs(start_time, end_time, performer=None, repository=None, namespace=None):
|
|
Performer = User.alias()
|
|
joined = (LogEntry.select(LogEntry, LogEntryKind, User, Performer)
|
|
.join(User)
|
|
.switch(LogEntry)
|
|
.join(Performer, JOIN_LEFT_OUTER,
|
|
on=(LogEntry.performer == Performer.id).alias('performer'))
|
|
.switch(LogEntry)
|
|
.join(LogEntryKind)
|
|
.switch(LogEntry))
|
|
|
|
if repository:
|
|
joined = joined.where(LogEntry.repository == repository)
|
|
|
|
if performer:
|
|
joined = joined.where(LogEntry.performer == performer)
|
|
|
|
if namespace:
|
|
joined = joined.where(User.username == namespace)
|
|
|
|
return list(joined.where(
|
|
LogEntry.datetime >= start_time,
|
|
LogEntry.datetime < end_time).order_by(LogEntry.datetime.desc()))
|
|
|
|
|
|
def log_action(kind_name, user_or_organization_name, performer=None,
|
|
repository=None, access_token=None, ip=None, metadata={},
|
|
timestamp=None):
|
|
if not timestamp:
|
|
timestamp = datetime.today()
|
|
|
|
kind = LogEntryKind.get(LogEntryKind.name == kind_name)
|
|
account = User.get(User.username == user_or_organization_name)
|
|
LogEntry.create(kind=kind, account=account, performer=performer,
|
|
repository=repository, ip=ip, metadata_json=json.dumps(metadata),
|
|
datetime=timestamp)
|
|
|
|
|
|
def update_build_trigger(trigger, config, auth_token=None):
|
|
trigger.config = json.dumps(config or {})
|
|
if auth_token is not None:
|
|
trigger.auth_token = auth_token
|
|
trigger.save()
|
|
|
|
|
|
def create_build_trigger(repo, service_name, auth_token, user, pull_robot=None, config=None):
|
|
config = config or {}
|
|
service = BuildTriggerService.get(name=service_name)
|
|
trigger = RepositoryBuildTrigger.create(repository=repo, service=service,
|
|
auth_token=auth_token,
|
|
connected_user=user,
|
|
pull_robot=pull_robot,
|
|
config=json.dumps(config))
|
|
return trigger
|
|
|
|
|
|
def get_build_trigger(trigger_uuid):
|
|
try:
|
|
return (RepositoryBuildTrigger
|
|
.select(RepositoryBuildTrigger, BuildTriggerService, Repository, Namespace)
|
|
.join(BuildTriggerService)
|
|
.switch(RepositoryBuildTrigger)
|
|
.join(Repository)
|
|
.join(Namespace, on=(Repository.namespace_user == Namespace.id))
|
|
.switch(RepositoryBuildTrigger)
|
|
.join(User)
|
|
.where(RepositoryBuildTrigger.uuid == trigger_uuid)
|
|
.get())
|
|
except RepositoryBuildTrigger.DoesNotExist:
|
|
msg = 'No build trigger with uuid: %s' % trigger_uuid
|
|
raise InvalidBuildTriggerException(msg)
|
|
|
|
|
|
def list_build_triggers(namespace_name, repository_name):
|
|
return (RepositoryBuildTrigger
|
|
.select(RepositoryBuildTrigger, BuildTriggerService, Repository)
|
|
.join(BuildTriggerService)
|
|
.switch(RepositoryBuildTrigger)
|
|
.join(Repository)
|
|
.join(Namespace, on=(Repository.namespace_user == Namespace.id))
|
|
.where(Namespace.username == namespace_name, Repository.name == repository_name))
|
|
|
|
|
|
def list_trigger_builds(namespace_name, repository_name, trigger_uuid,
|
|
limit):
|
|
return (list_repository_builds(namespace_name, repository_name, limit)
|
|
.where(RepositoryBuildTrigger.uuid == trigger_uuid))
|
|
|
|
|
|
def create_notification(kind_name, target, metadata={}):
|
|
kind_ref = NotificationKind.get(name=kind_name)
|
|
notification = Notification.create(kind=kind_ref, target=target,
|
|
metadata_json=json.dumps(metadata))
|
|
return notification
|
|
|
|
|
|
def create_unique_notification(kind_name, target, metadata={}):
|
|
with config.app_config['DB_TRANSACTION_FACTORY'](db):
|
|
if list_notifications(target, kind_name, limit=1).count() == 0:
|
|
create_notification(kind_name, target, metadata)
|
|
|
|
|
|
def lookup_notification(user, uuid):
|
|
results = list(list_notifications(user, id_filter=uuid, include_dismissed=True, limit=1))
|
|
if not results:
|
|
return None
|
|
|
|
return results[0]
|
|
|
|
|
|
def list_notifications(user, kind_name=None, id_filter=None, include_dismissed=False,
|
|
page=None, limit=None):
|
|
Org = User.alias()
|
|
AdminTeam = Team.alias()
|
|
AdminTeamMember = TeamMember.alias()
|
|
AdminUser = User.alias()
|
|
|
|
query = (Notification.select()
|
|
.join(User)
|
|
|
|
.switch(Notification)
|
|
.join(Org, JOIN_LEFT_OUTER, on=(Org.id == Notification.target))
|
|
.join(AdminTeam, JOIN_LEFT_OUTER, on=(Org.id ==
|
|
AdminTeam.organization))
|
|
.join(TeamRole, JOIN_LEFT_OUTER, on=(AdminTeam.role == TeamRole.id))
|
|
.switch(AdminTeam)
|
|
.join(AdminTeamMember, JOIN_LEFT_OUTER, on=(AdminTeam.id ==
|
|
AdminTeamMember.team))
|
|
.join(AdminUser, JOIN_LEFT_OUTER, on=(AdminTeamMember.user ==
|
|
AdminUser.id))
|
|
.where((Notification.target == user) |
|
|
((AdminUser.id == user) & (TeamRole.name == 'admin')))
|
|
.order_by(Notification.created)
|
|
.desc())
|
|
|
|
if not include_dismissed:
|
|
query = query.switch(Notification).where(Notification.dismissed == False)
|
|
|
|
if kind_name:
|
|
query = (query
|
|
.switch(Notification)
|
|
.join(NotificationKind)
|
|
.where(NotificationKind.name == kind_name))
|
|
|
|
if id_filter:
|
|
query = (query
|
|
.switch(Notification)
|
|
.where(Notification.uuid == id_filter))
|
|
|
|
if page:
|
|
query = query.paginate(page, limit)
|
|
elif limit:
|
|
query = query.limit(limit)
|
|
|
|
return query
|
|
|
|
|
|
def delete_all_notifications_by_kind(kind_name):
|
|
kind_ref = NotificationKind.get(name=kind_name)
|
|
(Notification.delete()
|
|
.where(Notification.kind == kind_ref)
|
|
.execute())
|
|
|
|
|
|
def delete_notifications_by_kind(target, kind_name):
|
|
kind_ref = NotificationKind.get(name=kind_name)
|
|
Notification.delete().where(Notification.target == target,
|
|
Notification.kind == kind_ref).execute()
|
|
|
|
|
|
def delete_matching_notifications(target, kind_name, **kwargs):
|
|
kind_ref = NotificationKind.get(name=kind_name)
|
|
|
|
# Load all notifications for the user with the given kind.
|
|
notifications = Notification.select().where(
|
|
Notification.target == target,
|
|
Notification.kind == kind_ref)
|
|
|
|
# For each, match the metadata to the specified values.
|
|
for notification in notifications:
|
|
matches = True
|
|
try:
|
|
metadata = json.loads(notification.metadata_json)
|
|
except:
|
|
continue
|
|
|
|
for (key, value) in kwargs.iteritems():
|
|
if not key in metadata or metadata[key] != value:
|
|
matches = False
|
|
break
|
|
|
|
if not matches:
|
|
continue
|
|
|
|
notification.delete_instance()
|
|
|
|
|
|
def get_organizations():
|
|
return User.select().where(User.organization == True, User.robot == False)
|
|
|
|
|
|
def get_active_users():
|
|
return User.select().where(User.organization == False, User.robot == False)
|
|
|
|
|
|
def get_active_user_count():
|
|
return get_active_users().count()
|
|
|
|
|
|
def detach_external_login(user, service_name):
|
|
try:
|
|
service = LoginService.get(name = service_name)
|
|
except LoginService.DoesNotExist:
|
|
return
|
|
|
|
FederatedLogin.delete().where(FederatedLogin.user == user,
|
|
FederatedLogin.service == service).execute()
|
|
|
|
|
|
def delete_user(user):
|
|
user.delete_instance(recursive=True, delete_nullable=True)
|
|
|
|
# TODO: also delete any repository data associated
|
|
|
|
|
|
def check_health(app_config):
|
|
# Attempt to connect to the database first. If the DB is not responding,
|
|
# using the validate_database_url will timeout quickly, as opposed to
|
|
# making a normal connect which will just hang (thus breaking the health
|
|
# check).
|
|
try:
|
|
validate_database_url(app_config['DB_URI'], connect_timeout=3)
|
|
except Exception:
|
|
logger.exception('Could not connect to the database')
|
|
return False
|
|
|
|
# We will connect to the db, check that it contains some log entry kinds
|
|
try:
|
|
return bool(list(LogEntryKind.select().limit(1)))
|
|
except:
|
|
return False
|
|
|
|
|
|
def get_email_authorized_for_repo(namespace, repository, email):
|
|
try:
|
|
return (RepositoryAuthorizedEmail
|
|
.select(RepositoryAuthorizedEmail, Repository, Namespace)
|
|
.join(Repository)
|
|
.join(Namespace, on=(Repository.namespace_user == Namespace.id))
|
|
.where(Namespace.username == namespace, Repository.name == repository,
|
|
RepositoryAuthorizedEmail.email == email)
|
|
.get())
|
|
except RepositoryAuthorizedEmail.DoesNotExist:
|
|
return None
|
|
|
|
|
|
def create_email_authorization_for_repo(namespace_name, repository_name, email):
|
|
try:
|
|
repo = _get_repository(namespace_name, repository_name)
|
|
except Repository.DoesNotExist:
|
|
raise DataModelException('Invalid repository %s/%s' %
|
|
(namespace_name, repository_name))
|
|
|
|
return RepositoryAuthorizedEmail.create(repository=repo, email=email, confirmed=False)
|
|
|
|
|
|
def confirm_email_authorization_for_repo(code):
|
|
try:
|
|
found = (RepositoryAuthorizedEmail
|
|
.select(RepositoryAuthorizedEmail, Repository, Namespace)
|
|
.join(Repository)
|
|
.join(Namespace, on=(Repository.namespace_user == Namespace.id))
|
|
.where(RepositoryAuthorizedEmail.code == code)
|
|
.get())
|
|
except RepositoryAuthorizedEmail.DoesNotExist:
|
|
raise DataModelException('Invalid confirmation code.')
|
|
|
|
found.confirmed = True
|
|
found.save()
|
|
|
|
return found
|
|
|
|
|
|
def delete_team_email_invite(team, email):
|
|
found = TeamMemberInvite.get(TeamMemberInvite.email == email, TeamMemberInvite.team == team)
|
|
found.delete_instance()
|
|
|
|
def delete_team_user_invite(team, user):
|
|
try:
|
|
found = TeamMemberInvite.get(TeamMemberInvite.user == user, TeamMemberInvite.team == team)
|
|
except TeamMemberInvite.DoesNotExist:
|
|
return False
|
|
|
|
found.delete_instance()
|
|
return True
|
|
|
|
def lookup_team_invites(user):
|
|
return TeamMemberInvite.select().where(TeamMemberInvite.user == user)
|
|
|
|
def lookup_team_invite(code, user=None):
|
|
# Lookup the invite code.
|
|
try:
|
|
found = TeamMemberInvite.get(TeamMemberInvite.invite_token == code)
|
|
except TeamMemberInvite.DoesNotExist:
|
|
raise DataModelException('Invalid confirmation code.')
|
|
|
|
if user and found.user != user:
|
|
raise DataModelException('Invalid confirmation code.')
|
|
|
|
return found
|
|
|
|
def delete_team_invite(code, user=None):
|
|
found = lookup_team_invite(code, user)
|
|
|
|
team = found.team
|
|
inviter = found.inviter
|
|
|
|
found.delete_instance()
|
|
|
|
return (team, inviter)
|
|
|
|
|
|
def confirm_team_invite(code, user):
|
|
found = lookup_team_invite(code)
|
|
|
|
# If the invite is for a specific user, we have to confirm that here.
|
|
if found.user is not None and found.user != user:
|
|
message = """This invite is intended for user "%s".
|
|
Please login to that account and try again.""" % found.user.username
|
|
raise DataModelException(message)
|
|
|
|
# Add the user to the team.
|
|
try:
|
|
add_user_to_team(user, found.team)
|
|
except UserAlreadyInTeam:
|
|
# Ignore.
|
|
pass
|
|
|
|
# Delete the invite and return the team.
|
|
team = found.team
|
|
inviter = found.inviter
|
|
found.delete_instance()
|
|
return (team, inviter)
|
|
|
|
def cancel_repository_build(build, work_queue):
|
|
with config.app_config['DB_TRANSACTION_FACTORY'](db):
|
|
# Reload the build for update.
|
|
try:
|
|
build = db_for_update(RepositoryBuild.select().where(RepositoryBuild.id == build.id)).get()
|
|
except RepositoryBuild.DoesNotExist:
|
|
return False
|
|
|
|
if build.phase != BUILD_PHASE.WAITING or not build.queue_id:
|
|
return False
|
|
|
|
# Try to cancel the queue item.
|
|
if not work_queue.cancel(build.queue_id):
|
|
return False
|
|
|
|
# Delete the build row.
|
|
build.delete_instance()
|
|
return True
|
|
|
|
def _get_repository_events(repository, time_delta, time_delta_earlier, clause):
|
|
""" Returns a pair representing the count of the number of events for the given
|
|
repository in each of the specified time deltas. The date ranges are calculated by
|
|
taking the current time today and subtracting the time delta given. Since
|
|
we want to grab *two* ranges, we restrict the second range to be greater
|
|
than the first (i.e. referring to an earlier time), so we can conduct the
|
|
lookup in a single query. The clause is used to further filter the kind of
|
|
events being found.
|
|
"""
|
|
since = date.today() - time_delta
|
|
since_earlier = date.today() - time_delta_earlier
|
|
|
|
if since_earlier >= since:
|
|
raise ValueError('time_delta_earlier must be greater than time_delta')
|
|
|
|
# This uses a CASE WHEN inner clause to further filter the count.
|
|
formatted = since.strftime('%Y-%m-%d')
|
|
case_query = 'CASE WHEN datetime >= \'%s\' THEN 1 ELSE 0 END' % formatted
|
|
|
|
result = (LogEntry.select(fn.Sum(SQL(case_query)), fn.Count(SQL('*')))
|
|
.where(LogEntry.repository == repository)
|
|
.where(clause)
|
|
.where(LogEntry.datetime >= since_earlier)
|
|
.tuples()
|
|
.get())
|
|
|
|
return (int(result[0]) if result[0] else 0, int(result[1]) if result[1] else 0)
|
|
|
|
|
|
def get_repository_pushes(repository, time_delta, time_delta_earlier):
|
|
push_repo = LogEntryKind.get(name='push_repo')
|
|
clauses = (LogEntry.kind == push_repo)
|
|
return _get_repository_events(repository, time_delta, time_delta_earlier, clauses)
|
|
|
|
|
|
def get_repository_pulls(repository, time_delta, time_delta_earlier):
|
|
repo_pull = LogEntryKind.get(name='pull_repo')
|
|
repo_verb = LogEntryKind.get(name='repo_verb')
|
|
clauses = ((LogEntry.kind == repo_pull) | (LogEntry.kind == repo_verb))
|
|
return _get_repository_events(repository, time_delta, time_delta_earlier, clauses)
|
|
|
|
|
|
def get_repository_usage():
|
|
one_month_ago = date.today() - timedelta(weeks=4)
|
|
repo_pull = LogEntryKind.get(name='pull_repo')
|
|
repo_verb = LogEntryKind.get(name='repo_verb')
|
|
return (LogEntry.select(LogEntry.ip, LogEntry.repository)
|
|
.where((LogEntry.kind == repo_pull) | (LogEntry.kind == repo_verb))
|
|
.where(~(LogEntry.repository >> None))
|
|
.where(LogEntry.datetime >= one_month_ago)
|
|
.group_by(LogEntry.ip, LogEntry.repository)
|
|
.count())
|
|
|
|
def archivable_buildlogs_query():
|
|
presumed_dead_date = datetime.utcnow() - PRESUMED_DEAD_BUILD_AGE
|
|
return (RepositoryBuild.select()
|
|
.where((RepositoryBuild.phase == BUILD_PHASE.COMPLETE) |
|
|
(RepositoryBuild.phase == BUILD_PHASE.ERROR) |
|
|
(RepositoryBuild.started < presumed_dead_date), RepositoryBuild.logs_archived == False))
|
|
|
|
|
|
def star_repository(user, repository):
|
|
""" Stars a repository. """
|
|
star = Star.create(user=user.id, repository=repository.id)
|
|
star.save()
|
|
|
|
|
|
def unstar_repository(user, repository):
|
|
""" Unstars a repository. """
|
|
try:
|
|
star = (Star
|
|
.delete()
|
|
.where(Star.repository == repository.id, Star.user == user.id)
|
|
.execute())
|
|
except Star.DoesNotExist:
|
|
raise DataModelException('Star not found.')
|
|
|
|
|
|
def get_user_starred_repositories(user, limit=None, page=None):
|
|
""" Retrieves all of the repositories a user has starred. """
|
|
query = (Repository
|
|
.select(Repository, User, Visibility)
|
|
.join(Star)
|
|
.switch(Repository)
|
|
.join(User)
|
|
.switch(Repository)
|
|
.join(Visibility)
|
|
.where(Star.user == user))
|
|
|
|
if page and limit:
|
|
query = query.paginate(page, limit)
|
|
elif limit:
|
|
query = query.limit(limit)
|
|
|
|
return query
|
|
|
|
|
|
def repository_is_starred(user, repository):
|
|
""" Determines whether a user has starred a repository or not. """
|
|
try:
|
|
(Star
|
|
.select()
|
|
.where(Star.repository == repository.id, Star.user == user.id)
|
|
.get())
|
|
return True
|
|
except Star.DoesNotExist:
|
|
return False
|
|
|
|
|
|
def revert_tag(repository, tag_name, docker_image_id):
|
|
""" Reverts a tag to a specific image ID. """
|
|
# Verify that the image ID already existed under this repository under the
|
|
# tag.
|
|
try:
|
|
(RepositoryTag.select()
|
|
.join(Image)
|
|
.where(RepositoryTag.repository == repository)
|
|
.where(RepositoryTag.name == tag_name)
|
|
.where(Image.docker_image_id == docker_image_id)
|
|
.get())
|
|
except RepositoryTag.DoesNotExist:
|
|
raise DataModelException('Cannot revert to unknown or invalid image')
|
|
|
|
return create_or_update_tag(repository.namespace_user.username, repository.name,
|
|
tag_name, docker_image_id, reversion=True)
|