From 90c0a9c1e01dd0e4102422962262d6666f6cfb2e Mon Sep 17 00:00:00 2001 From: Jake Moshenko Date: Wed, 11 Feb 2015 11:54:30 -0500 Subject: [PATCH] First stab at time machine using fixed two week expiration policy. --- config.py | 3 ++ data/database.py | 4 +- data/model/legacy.py | 108 +++++++++++++++++++++++++++---------------- endpoints/api/tag.py | 4 +- test/data/test.db | Bin 684032 -> 708608 bytes test/test_gc.py | 2 +- 6 files changed, 78 insertions(+), 43 deletions(-) diff --git a/config.py b/config.py index 39a257b15..47f836587 100644 --- a/config.py +++ b/config.py @@ -185,5 +185,8 @@ class DefaultConfig(object): LOG_ARCHIVE_LOCATION = 'local_us' LOG_ARCHIVE_PATH = 'logarchive/' + # Number of revisions to keep expired tags + TIME_MACHINE_DELTA_SECONDS = 14 * 24 * 60 * 60 + # For enterprise: MAXIMUM_REPOSITORY_USAGE = 20 diff --git a/data/database.py b/data/database.py index eb804688e..7d027aa71 100644 --- a/data/database.py +++ b/data/database.py @@ -456,12 +456,14 @@ class RepositoryTag(BaseModel): name = CharField() image = ForeignKeyField(Image) repository = ForeignKeyField(Repository) + lifetime_start = DateTimeField(default=datetime.utcnow) + lifetime_end = DateTimeField(null=True) class Meta: database = db read_slaves = (read_slave,) indexes = ( - (('repository', 'name'), True), + (('repository', 'name'), False), ) diff --git a/data/model/legacy.py b/data/model/legacy.py index 9bae8cd32..6d3809ccb 100644 --- a/data/model/legacy.py +++ b/data/model/legacy.py @@ -106,12 +106,15 @@ class TooManyLoginAttemptsException(Exception): self.retry_after = retry_after -def _get_repository(namespace_name, repository_name): - return (Repository - .select(Repository, Namespace) - .join(Namespace, on=(Repository.namespace_user == Namespace.id)) - .where(Namespace.username == namespace_name, Repository.name == repository_name) - .get()) +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): @@ -1534,12 +1537,27 @@ def list_repository_tags(namespace_name, repository_name): .join(Namespace, on=(Repository.namespace_user == Namespace.id)) .switch(RepositoryTag) .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): 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. @@ -1573,12 +1591,12 @@ def garbage_collect_repository(namespace_name, repository_name): if len(to_remove) > 0: 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) -def garbage_collect_storage(storage_id_whitelist): +def _garbage_collect_storage(storage_id_whitelist): if len(storage_id_whitelist) == 0: return @@ -1710,45 +1728,57 @@ def get_parent_images(namespace_name, repository_name, image_obj): def create_or_update_tag(namespace_name, repository_name, tag_name, tag_docker_image_id): - try: - repo = _get_repository(namespace_name, repository_name) - except Repository.DoesNotExist: - raise DataModelException('Invalid repository %s/%s' % (namespace_name, repository_name)) - 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) + with config.app_config['DB_TRANSACTION_FACTORY'](db): + try: + repo = _get_repository(namespace_name, repository_name) + except Repository.DoesNotExist: + raise DataModelException('Invalid repository %s/%s' % (namespace_name, repository_name)) - try: - tag = RepositoryTag.get(RepositoryTag.repository == repo, RepositoryTag.name == tag_name) - tag.image = image - tag.save() - except RepositoryTag.DoesNotExist: - tag = RepositoryTag.create(repository=repo, image=image, name=tag_name) + 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 tag + now = datetime.utcnow() + + try: + # When we move a tag, we really end the timeline of the old one and create a new one + tag = RepositoryTag.get(RepositoryTag.repository == repo, RepositoryTag.name == tag_name, + RepositoryTag.lifetime_end >> None) + tag.lifetime_end = now + tag.save() + except RepositoryTag.DoesNotExist: + # No tag that needs to be ended + pass + + tag = RepositoryTag.create(repository=repo, image=image, name=tag_name, lifetime_start=now) + + return tag def delete_tag(namespace_name, repository_name, tag_name): - try: - found = (RepositoryTag - .select() - .join(Repository) - .join(Namespace, on=(Repository.namespace_user == Namespace.id)) - .where(Repository.name == repository_name, Namespace.username == namespace_name, - RepositoryTag.name == tag_name) - .get()) + with config.app_config['DB_TRANSACTION_FACTORY'](db): + try: + query = (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, RepositoryTag.lifetime_end >> None)) + 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) + except RepositoryTag.DoesNotExist: + msg = ('Invalid repository tag \'%s\' on repository \'%s/%s\'' % + (tag_name, namespace_name, repository_name)) + 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: repo = _get_repository(namespace_name, repository_name) 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): # 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 garbage_collect_repository(namespace_name, repository_name) diff --git a/endpoints/api/tag.py b/endpoints/api/tag.py index 581e02a60..21972fc19 100644 --- a/endpoints/api/tag.py +++ b/endpoints/api/tag.py @@ -54,8 +54,8 @@ class RepositoryTag(RepositoryParamResource): username = get_authenticated_user().username log_action('move_tag' if original_image_id else 'create_tag', namespace, - { 'username': username, 'repo': repository, 'tag': tag, - 'image': image_id, 'original_image': original_image_id }, + {'username': username, 'repo': repository, 'tag': tag, + 'image': image_id, 'original_image': original_image_id}, repo=model.get_repository(namespace, repository)) return 'Updated', 201 diff --git a/test/data/test.db b/test/data/test.db index c37aec119f0b2d599ca2fc5db9c1e1660280afa0..1e810ea24930671a0bac6571e5c2fe3ddc4ba91d 100644 GIT binary patch delta 15558 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 delta 12907 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 VpZWWv_}&vBFzSi@NJ})u|1S)Tu|)s? diff --git a/test/test_gc.py b/test/test_gc.py index 0ad8ce7bb..5a5444f2a 100644 --- a/test/test_gc.py +++ b/test/test_gc.py @@ -207,7 +207,7 @@ class TestGarbageColection(unittest.TestCase): self.assertNotDeleted(repository,'i1', 'i2', 'i3', 't1', 't2', 't3', 'f1', 'f2') def test_gc_storage_empty(self): - model.garbage_collect_storage(set()) + model._garbage_collect_storage(set()) if __name__ == '__main__': unittest.main()