First stab at time machine using fixed two week expiration policy.
This commit is contained in:
parent
0f3d87466e
commit
90c0a9c1e0
6 changed files with 78 additions and 43 deletions
|
@ -185,5 +185,8 @@ class DefaultConfig(object):
|
||||||
LOG_ARCHIVE_LOCATION = 'local_us'
|
LOG_ARCHIVE_LOCATION = 'local_us'
|
||||||
LOG_ARCHIVE_PATH = 'logarchive/'
|
LOG_ARCHIVE_PATH = 'logarchive/'
|
||||||
|
|
||||||
|
# Number of revisions to keep expired tags
|
||||||
|
TIME_MACHINE_DELTA_SECONDS = 14 * 24 * 60 * 60
|
||||||
|
|
||||||
# For enterprise:
|
# For enterprise:
|
||||||
MAXIMUM_REPOSITORY_USAGE = 20
|
MAXIMUM_REPOSITORY_USAGE = 20
|
||||||
|
|
|
@ -456,12 +456,14 @@ class RepositoryTag(BaseModel):
|
||||||
name = CharField()
|
name = CharField()
|
||||||
image = ForeignKeyField(Image)
|
image = ForeignKeyField(Image)
|
||||||
repository = ForeignKeyField(Repository)
|
repository = ForeignKeyField(Repository)
|
||||||
|
lifetime_start = DateTimeField(default=datetime.utcnow)
|
||||||
|
lifetime_end = DateTimeField(null=True)
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
database = db
|
database = db
|
||||||
read_slaves = (read_slave,)
|
read_slaves = (read_slave,)
|
||||||
indexes = (
|
indexes = (
|
||||||
(('repository', 'name'), True),
|
(('repository', 'name'), False),
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
|
|
@ -106,12 +106,15 @@ class TooManyLoginAttemptsException(Exception):
|
||||||
self.retry_after = retry_after
|
self.retry_after = retry_after
|
||||||
|
|
||||||
|
|
||||||
def _get_repository(namespace_name, repository_name):
|
def _get_repository(namespace_name, repository_name, for_update=False):
|
||||||
return (Repository
|
query = (Repository
|
||||||
.select(Repository, Namespace)
|
.select(Repository, Namespace)
|
||||||
.join(Namespace, on=(Repository.namespace_user == Namespace.id))
|
.join(Namespace, on=(Repository.namespace_user == Namespace.id))
|
||||||
.where(Namespace.username == namespace_name, Repository.name == repository_name)
|
.where(Namespace.username == namespace_name, Repository.name == repository_name))
|
||||||
.get())
|
if for_update:
|
||||||
|
query = db_for_update(query)
|
||||||
|
|
||||||
|
return query.get()
|
||||||
|
|
||||||
|
|
||||||
def hash_password(password, salt=None):
|
def hash_password(password, salt=None):
|
||||||
|
@ -1534,12 +1537,27 @@ def list_repository_tags(namespace_name, repository_name):
|
||||||
.join(Namespace, on=(Repository.namespace_user == Namespace.id))
|
.join(Namespace, on=(Repository.namespace_user == Namespace.id))
|
||||||
.switch(RepositoryTag)
|
.switch(RepositoryTag)
|
||||||
.join(Image)
|
.join(Image)
|
||||||
.where(Repository.name == repository_name, Namespace.username == namespace_name))
|
.where(Repository.name == repository_name, Namespace.username == namespace_name,
|
||||||
|
RepositoryTag.lifetime_end >> None))
|
||||||
|
|
||||||
|
|
||||||
|
def _garbage_collect_tags(namespace_name, repository_name):
|
||||||
|
with config.app_config['DB_TRANSACTION_FACTORY'](db):
|
||||||
|
repo = _get_repository(namespace_name, repository_name)
|
||||||
|
collect_time = (datetime.utcnow() -
|
||||||
|
timedelta(seconds=config.app_config['TIME_MACHINE_DELTA_SECONDS']))
|
||||||
|
|
||||||
|
(RepositoryTag
|
||||||
|
.delete()
|
||||||
|
.where(RepositoryTag.repository == repo, RepositoryTag.lifetime_end < collect_time)
|
||||||
|
.execute())
|
||||||
|
|
||||||
|
|
||||||
def garbage_collect_repository(namespace_name, repository_name):
|
def garbage_collect_repository(namespace_name, repository_name):
|
||||||
storage_id_whitelist = {}
|
storage_id_whitelist = {}
|
||||||
|
|
||||||
|
_garbage_collect_tags(namespace_name, repository_name)
|
||||||
|
|
||||||
with config.app_config['DB_TRANSACTION_FACTORY'](db):
|
with config.app_config['DB_TRANSACTION_FACTORY'](db):
|
||||||
# TODO (jake): We could probably select this and all the images in a single query using
|
# TODO (jake): We could probably select this and all the images in a single query using
|
||||||
# a different kind of join.
|
# a different kind of join.
|
||||||
|
@ -1573,12 +1591,12 @@ def garbage_collect_repository(namespace_name, repository_name):
|
||||||
|
|
||||||
if len(to_remove) > 0:
|
if len(to_remove) > 0:
|
||||||
logger.info('Garbage collecting storage for images: %s', to_remove)
|
logger.info('Garbage collecting storage for images: %s', to_remove)
|
||||||
garbage_collect_storage(storage_id_whitelist)
|
_garbage_collect_storage(storage_id_whitelist)
|
||||||
|
|
||||||
return len(to_remove)
|
return len(to_remove)
|
||||||
|
|
||||||
|
|
||||||
def garbage_collect_storage(storage_id_whitelist):
|
def _garbage_collect_storage(storage_id_whitelist):
|
||||||
if len(storage_id_whitelist) == 0:
|
if len(storage_id_whitelist) == 0:
|
||||||
return
|
return
|
||||||
|
|
||||||
|
@ -1710,6 +1728,8 @@ def get_parent_images(namespace_name, repository_name, image_obj):
|
||||||
|
|
||||||
def create_or_update_tag(namespace_name, repository_name, tag_name,
|
def create_or_update_tag(namespace_name, repository_name, tag_name,
|
||||||
tag_docker_image_id):
|
tag_docker_image_id):
|
||||||
|
|
||||||
|
with config.app_config['DB_TRANSACTION_FACTORY'](db):
|
||||||
try:
|
try:
|
||||||
repo = _get_repository(namespace_name, repository_name)
|
repo = _get_repository(namespace_name, repository_name)
|
||||||
except Repository.DoesNotExist:
|
except Repository.DoesNotExist:
|
||||||
|
@ -1720,35 +1740,45 @@ def create_or_update_tag(namespace_name, repository_name, tag_name,
|
||||||
except Image.DoesNotExist:
|
except Image.DoesNotExist:
|
||||||
raise DataModelException('Invalid image with id: %s' % tag_docker_image_id)
|
raise DataModelException('Invalid image with id: %s' % tag_docker_image_id)
|
||||||
|
|
||||||
|
now = datetime.utcnow()
|
||||||
|
|
||||||
try:
|
try:
|
||||||
tag = RepositoryTag.get(RepositoryTag.repository == repo, RepositoryTag.name == tag_name)
|
# When we move a tag, we really end the timeline of the old one and create a new one
|
||||||
tag.image = image
|
tag = RepositoryTag.get(RepositoryTag.repository == repo, RepositoryTag.name == tag_name,
|
||||||
|
RepositoryTag.lifetime_end >> None)
|
||||||
|
tag.lifetime_end = now
|
||||||
tag.save()
|
tag.save()
|
||||||
except RepositoryTag.DoesNotExist:
|
except RepositoryTag.DoesNotExist:
|
||||||
tag = RepositoryTag.create(repository=repo, image=image, name=tag_name)
|
# No tag that needs to be ended
|
||||||
|
pass
|
||||||
|
|
||||||
|
tag = RepositoryTag.create(repository=repo, image=image, name=tag_name, lifetime_start=now)
|
||||||
|
|
||||||
return tag
|
return tag
|
||||||
|
|
||||||
|
|
||||||
def delete_tag(namespace_name, repository_name, tag_name):
|
def delete_tag(namespace_name, repository_name, tag_name):
|
||||||
|
with config.app_config['DB_TRANSACTION_FACTORY'](db):
|
||||||
try:
|
try:
|
||||||
found = (RepositoryTag
|
query = (RepositoryTag
|
||||||
.select()
|
.select(RepositoryTag, Repository)
|
||||||
.join(Repository)
|
.join(Repository)
|
||||||
.join(Namespace, on=(Repository.namespace_user == Namespace.id))
|
.join(Namespace, on=(Repository.namespace_user == Namespace.id))
|
||||||
.where(Repository.name == repository_name, Namespace.username == namespace_name,
|
.where(Repository.name == repository_name, Namespace.username == namespace_name,
|
||||||
RepositoryTag.name == tag_name)
|
RepositoryTag.name == tag_name, RepositoryTag.lifetime_end >> None))
|
||||||
.get())
|
found = db_for_update(query).get()
|
||||||
|
|
||||||
except RepositoryTag.DoesNotExist:
|
except RepositoryTag.DoesNotExist:
|
||||||
msg = ('Invalid repository tag \'%s\' on repository \'%s/%s\'' %
|
msg = ('Invalid repository tag \'%s\' on repository \'%s/%s\'' %
|
||||||
(tag_name, namespace_name, repository_name))
|
(tag_name, namespace_name, repository_name))
|
||||||
raise DataModelException(msg)
|
raise DataModelException(msg)
|
||||||
|
|
||||||
found.delete_instance()
|
found.lifetime_end = datetime.utcnow()
|
||||||
|
found.save()
|
||||||
|
|
||||||
|
|
||||||
def delete_all_repository_tags(namespace_name, repository_name):
|
def purge_all_repository_tags(namespace_name, repository_name):
|
||||||
|
""" Immediately purge all repository tags without respecting the lifeline procedure """
|
||||||
try:
|
try:
|
||||||
repo = _get_repository(namespace_name, repository_name)
|
repo = _get_repository(namespace_name, repository_name)
|
||||||
except Repository.DoesNotExist:
|
except Repository.DoesNotExist:
|
||||||
|
@ -1863,7 +1893,7 @@ def set_team_repo_permission(team_name, namespace_name, repository_name,
|
||||||
|
|
||||||
def purge_repository(namespace_name, repository_name):
|
def purge_repository(namespace_name, repository_name):
|
||||||
# Delete all tags to allow gc to reclaim storage
|
# Delete all tags to allow gc to reclaim storage
|
||||||
delete_all_repository_tags(namespace_name, repository_name)
|
purge_all_repository_tags(namespace_name, repository_name)
|
||||||
|
|
||||||
# Gc to remove the images and storage
|
# Gc to remove the images and storage
|
||||||
garbage_collect_repository(namespace_name, repository_name)
|
garbage_collect_repository(namespace_name, repository_name)
|
||||||
|
|
Binary file not shown.
|
@ -207,7 +207,7 @@ class TestGarbageColection(unittest.TestCase):
|
||||||
self.assertNotDeleted(repository,'i1', 'i2', 'i3', 't1', 't2', 't3', 'f1', 'f2')
|
self.assertNotDeleted(repository,'i1', 'i2', 'i3', 't1', 't2', 't3', 'f1', 'f2')
|
||||||
|
|
||||||
def test_gc_storage_empty(self):
|
def test_gc_storage_empty(self):
|
||||||
model.garbage_collect_storage(set())
|
model._garbage_collect_storage(set())
|
||||||
|
|
||||||
if __name__ == '__main__':
|
if __name__ == '__main__':
|
||||||
unittest.main()
|
unittest.main()
|
||||||
|
|
Reference in a new issue