Merge pull request #3071 from quay/joseph.schorr/QUAY-932/namespace-robot-deletion
Fix issue where we didn't delete robots immediately under a namespace
This commit is contained in:
commit
7345ff855c
5 changed files with 132 additions and 10 deletions
23
data/migrations/versions/5b7503aada1b_cleanup_old_robots.py
Normal file
23
data/migrations/versions/5b7503aada1b_cleanup_old_robots.py
Normal file
|
@ -0,0 +1,23 @@
|
||||||
|
"""Cleanup old robots
|
||||||
|
|
||||||
|
Revision ID: 5b7503aada1b
|
||||||
|
Revises: 224ce4c72c2f
|
||||||
|
Create Date: 2018-05-09 17:18:52.230504
|
||||||
|
|
||||||
|
"""
|
||||||
|
|
||||||
|
# revision identifiers, used by Alembic.
|
||||||
|
revision = '5b7503aada1b'
|
||||||
|
down_revision = '224ce4c72c2f'
|
||||||
|
|
||||||
|
from alembic import op
|
||||||
|
import sqlalchemy as sa
|
||||||
|
|
||||||
|
from util.migrate.cleanup_old_robots import cleanup_old_robots
|
||||||
|
|
||||||
|
def upgrade(tables):
|
||||||
|
cleanup_old_robots()
|
||||||
|
|
||||||
|
def downgrade(tables):
|
||||||
|
# Nothing to do.
|
||||||
|
pass
|
|
@ -7,6 +7,8 @@ from mock import patch
|
||||||
from data.database import EmailConfirmation, User, DeletedNamespace
|
from data.database import EmailConfirmation, User, DeletedNamespace
|
||||||
from data.model.user import create_user_noverify, validate_reset_code, get_active_users
|
from data.model.user import create_user_noverify, validate_reset_code, get_active_users
|
||||||
from data.model.user import mark_namespace_for_deletion, delete_namespace_via_marker
|
from data.model.user import mark_namespace_for_deletion, delete_namespace_via_marker
|
||||||
|
from data.model.user import create_robot, lookup_robot, list_namespace_robots
|
||||||
|
from data.model.user import InvalidRobotException
|
||||||
from util.timedeltastring import convert_to_timedelta
|
from util.timedeltastring import convert_to_timedelta
|
||||||
from data.queue import WorkQueue
|
from data.queue import WorkQueue
|
||||||
from test.fixtures import *
|
from test.fixtures import *
|
||||||
|
@ -48,12 +50,30 @@ def test_mark_namespace_for_deletion(initialized_db):
|
||||||
# Create a user and then mark it for deletion.
|
# Create a user and then mark it for deletion.
|
||||||
user = create_user_noverify('foobar', 'foo@example.com', email_required=False)
|
user = create_user_noverify('foobar', 'foo@example.com', email_required=False)
|
||||||
|
|
||||||
|
# Add some robots.
|
||||||
|
create_robot('foo', user)
|
||||||
|
create_robot('bar', user)
|
||||||
|
|
||||||
|
assert lookup_robot('foobar+foo') is not None
|
||||||
|
assert lookup_robot('foobar+bar') is not None
|
||||||
|
assert len(list(list_namespace_robots('foobar'))) == 2
|
||||||
|
|
||||||
# Mark the user for deletion.
|
# Mark the user for deletion.
|
||||||
queue = WorkQueue('testgcnamespace', create_transaction)
|
queue = WorkQueue('testgcnamespace', create_transaction)
|
||||||
mark_namespace_for_deletion(user, [], queue)
|
mark_namespace_for_deletion(user, [], queue)
|
||||||
|
|
||||||
# Ensure the older user is still in the DB.
|
# Ensure the older user is still in the DB.
|
||||||
assert User.get(id=user.id).username != 'foobar'
|
older_user = User.get(id=user.id)
|
||||||
|
assert older_user.username != 'foobar'
|
||||||
|
|
||||||
|
# Ensure the robots are deleted.
|
||||||
|
with pytest.raises(InvalidRobotException):
|
||||||
|
assert lookup_robot('foobar+foo')
|
||||||
|
|
||||||
|
with pytest.raises(InvalidRobotException):
|
||||||
|
assert lookup_robot('foobar+bar')
|
||||||
|
|
||||||
|
assert len(list(list_namespace_robots(older_user.username))) == 0
|
||||||
|
|
||||||
# Ensure we can create a user with the same namespace again.
|
# Ensure we can create a user with the same namespace again.
|
||||||
new_user = create_user_noverify('foobar', 'foo@example.com', email_required=False)
|
new_user = create_user_noverify('foobar', 'foo@example.com', email_required=False)
|
||||||
|
|
|
@ -68,7 +68,6 @@ def create_user_noverify(username, email, email_required=True, prompts=tuple(),
|
||||||
|
|
||||||
try:
|
try:
|
||||||
existing = User.get((User.username == username) | (User.email == email))
|
existing = User.get((User.username == username) | (User.email == email))
|
||||||
|
|
||||||
logger.info('Existing user with same username or email.')
|
logger.info('Existing user with same username or email.')
|
||||||
|
|
||||||
# A user already exists with either the same username or email
|
# A user already exists with either the same username or email
|
||||||
|
@ -77,11 +76,11 @@ def create_user_noverify(username, email, email_required=True, prompts=tuple(),
|
||||||
username)
|
username)
|
||||||
raise InvalidEmailAddressException('Email has already been used: %s' %
|
raise InvalidEmailAddressException('Email has already been used: %s' %
|
||||||
email)
|
email)
|
||||||
|
|
||||||
except User.DoesNotExist:
|
except User.DoesNotExist:
|
||||||
# This is actually the happy path
|
# This is actually the happy path
|
||||||
logger.debug('Email and username are unique!')
|
logger.debug('Email and username are unique!')
|
||||||
|
|
||||||
|
# Create the user.
|
||||||
try:
|
try:
|
||||||
default_expr_s = _convert_to_s(config.app_config['DEFAULT_TAG_EXPIRATION'])
|
default_expr_s = _convert_to_s(config.app_config['DEFAULT_TAG_EXPIRATION'])
|
||||||
default_max_builds = config.app_config.get('DEFAULT_NAMESPACE_MAXIMUM_BUILD_COUNT')
|
default_max_builds = config.app_config.get('DEFAULT_NAMESPACE_MAXIMUM_BUILD_COUNT')
|
||||||
|
@ -970,10 +969,12 @@ def delete_user(user, queues):
|
||||||
def _delete_user_linked_data(user):
|
def _delete_user_linked_data(user):
|
||||||
if user.organization:
|
if user.organization:
|
||||||
# Delete the organization's teams.
|
# Delete the organization's teams.
|
||||||
|
with db_transaction():
|
||||||
for team in Team.select().where(Team.organization == user):
|
for team in Team.select().where(Team.organization == user):
|
||||||
team.delete_instance(recursive=True)
|
team.delete_instance(recursive=True)
|
||||||
|
|
||||||
# Delete any OAuth approvals and tokens associated with the user.
|
# Delete any OAuth approvals and tokens associated with the user.
|
||||||
|
with db_transaction():
|
||||||
for app in OAuthApplication.select().where(OAuthApplication.organization == user):
|
for app in OAuthApplication.select().where(OAuthApplication.organization == user):
|
||||||
app.delete_instance(recursive=True)
|
app.delete_instance(recursive=True)
|
||||||
else:
|
else:
|
||||||
|
@ -981,10 +982,17 @@ def _delete_user_linked_data(user):
|
||||||
TeamMember.delete().where(TeamMember.user == user).execute()
|
TeamMember.delete().where(TeamMember.user == user).execute()
|
||||||
|
|
||||||
# Delete any repository buildtriggers where the user is the connected user.
|
# Delete any repository buildtriggers where the user is the connected user.
|
||||||
|
with db_transaction():
|
||||||
triggers = RepositoryBuildTrigger.select().where(RepositoryBuildTrigger.connected_user == user)
|
triggers = RepositoryBuildTrigger.select().where(RepositoryBuildTrigger.connected_user == user)
|
||||||
for trigger in triggers:
|
for trigger in triggers:
|
||||||
trigger.delete_instance(recursive=True, delete_nullable=False)
|
trigger.delete_instance(recursive=True, delete_nullable=False)
|
||||||
|
|
||||||
|
# Delete any robots owned by this user.
|
||||||
|
with db_transaction():
|
||||||
|
robots = list(list_namespace_robots(user.username))
|
||||||
|
for robot in robots:
|
||||||
|
robot.delete_instance(recursive=True, delete_nullable=True)
|
||||||
|
|
||||||
# Null out any service key approvals. We technically lose information here, but its better than
|
# Null out any service key approvals. We technically lose information here, but its better than
|
||||||
# falling and only occurs if a superuser is being deleted.
|
# falling and only occurs if a superuser is being deleted.
|
||||||
ServiceKeyApproval.update(approver=None).where(ServiceKeyApproval.approver == user).execute()
|
ServiceKeyApproval.update(approver=None).where(ServiceKeyApproval.approver == user).execute()
|
||||||
|
|
28
util/migrate/cleanup_old_robots.py
Normal file
28
util/migrate/cleanup_old_robots.py
Normal file
|
@ -0,0 +1,28 @@
|
||||||
|
from data.database import User
|
||||||
|
from util.names import parse_robot_username
|
||||||
|
|
||||||
|
def cleanup_old_robots(page_size=50):
|
||||||
|
""" Deletes any robots that live under namespaces that no longer exist. """
|
||||||
|
# Collect the robot accounts to delete.
|
||||||
|
page_number = 1
|
||||||
|
to_delete = []
|
||||||
|
|
||||||
|
while True:
|
||||||
|
found_bots = False
|
||||||
|
for robot in list(User.select().where(User.robot == True).paginate(page_number, page_size)):
|
||||||
|
found_bots = True
|
||||||
|
namespace, _ = parse_robot_username(robot.username)
|
||||||
|
try:
|
||||||
|
User.get(username=namespace)
|
||||||
|
except User.DoesNotExist:
|
||||||
|
# Save the robot account for deletion.
|
||||||
|
to_delete.append(robot)
|
||||||
|
|
||||||
|
if not found_bots:
|
||||||
|
break
|
||||||
|
|
||||||
|
page_number = page_number + 1
|
||||||
|
|
||||||
|
# Cleanup any robot accounts whose corresponding namespace doesn't exist.
|
||||||
|
for robot in to_delete:
|
||||||
|
robot.delete_instance(recursive=True, delete_nullable=True)
|
43
util/migrate/test/test_cleanup_old_robots.py
Normal file
43
util/migrate/test/test_cleanup_old_robots.py
Normal file
|
@ -0,0 +1,43 @@
|
||||||
|
import pytest
|
||||||
|
|
||||||
|
from data.database import User
|
||||||
|
from util.migrate.cleanup_old_robots import cleanup_old_robots
|
||||||
|
|
||||||
|
from test.fixtures import *
|
||||||
|
|
||||||
|
def test_cleanup_old_robots(initialized_db):
|
||||||
|
before_robot_count = User.select().where(User.robot == True).count()
|
||||||
|
before_user_count = User.select().count()
|
||||||
|
|
||||||
|
# Run the cleanup once, and ensure it does nothing.
|
||||||
|
cleanup_old_robots()
|
||||||
|
|
||||||
|
after_robot_count = User.select().where(User.robot == True).count()
|
||||||
|
after_user_count = User.select().count()
|
||||||
|
|
||||||
|
assert before_robot_count == after_robot_count
|
||||||
|
assert before_user_count == after_user_count
|
||||||
|
|
||||||
|
# Create some orphan robots.
|
||||||
|
created = set()
|
||||||
|
for index in range(0, 50):
|
||||||
|
created.add('doesnotexist+a%s' % index)
|
||||||
|
created.add('anothernamespace+b%s' % index)
|
||||||
|
|
||||||
|
User.create(username='doesnotexist+a%s' % index, robot=True)
|
||||||
|
User.create(username='anothernamespace+b%s' % index, robot=True)
|
||||||
|
|
||||||
|
before_robot_count = User.select().where(User.robot == True).count()
|
||||||
|
before_user_count = User.select().count()
|
||||||
|
|
||||||
|
cleanup_old_robots(page_size=10)
|
||||||
|
|
||||||
|
after_robot_count = User.select().where(User.robot == True).count()
|
||||||
|
after_user_count = User.select().count()
|
||||||
|
|
||||||
|
assert before_robot_count == after_robot_count + len(created)
|
||||||
|
assert before_user_count == after_user_count + len(created)
|
||||||
|
|
||||||
|
for name in created:
|
||||||
|
with pytest.raises(User.DoesNotExist):
|
||||||
|
User.get(username=name)
|
Reference in a new issue