Use a consistent concept of tag liveness everywhere. Fix the tests.
This commit is contained in:
parent
90c0a9c1e0
commit
f32bd748e4
2 changed files with 58 additions and 54 deletions
|
@ -1530,15 +1530,20 @@ def get_repository_images(namespace_name, repository_name):
|
||||||
return _get_repository_images_base(namespace_name, repository_name, lambda q: q)
|
return _get_repository_images_base(namespace_name, repository_name, lambda q: q)
|
||||||
|
|
||||||
|
|
||||||
|
def _tag_alive(query):
|
||||||
|
return query.where((RepositoryTag.lifetime_end >> None) |
|
||||||
|
(RepositoryTag.lifetime_end > datetime.utcnow()))
|
||||||
|
|
||||||
|
|
||||||
def list_repository_tags(namespace_name, repository_name):
|
def list_repository_tags(namespace_name, repository_name):
|
||||||
return (RepositoryTag
|
return _tag_alive(RepositoryTag
|
||||||
.select(RepositoryTag, Image)
|
.select(RepositoryTag, Image)
|
||||||
.join(Repository)
|
.join(Repository)
|
||||||
.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,
|
||||||
RepositoryTag.lifetime_end >> None))
|
Namespace.username == namespace_name))
|
||||||
|
|
||||||
|
|
||||||
def _garbage_collect_tags(namespace_name, repository_name):
|
def _garbage_collect_tags(namespace_name, repository_name):
|
||||||
|
@ -1688,7 +1693,7 @@ def _garbage_collect_storage(storage_id_whitelist):
|
||||||
|
|
||||||
def get_tag_image(namespace_name, repository_name, tag_name):
|
def get_tag_image(namespace_name, repository_name, tag_name):
|
||||||
def limit_to_tag(query):
|
def limit_to_tag(query):
|
||||||
return (query
|
return _tag_alive(query
|
||||||
.switch(Image)
|
.switch(Image)
|
||||||
.join(RepositoryTag)
|
.join(RepositoryTag)
|
||||||
.where(RepositoryTag.name == tag_name))
|
.where(RepositoryTag.name == tag_name))
|
||||||
|
@ -1744,8 +1749,10 @@ def create_or_update_tag(namespace_name, repository_name, tag_name,
|
||||||
|
|
||||||
try:
|
try:
|
||||||
# When we move a tag, we really end the timeline of the old one and create a new one
|
# 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,
|
query = _tag_alive(RepositoryTag
|
||||||
RepositoryTag.lifetime_end >> None)
|
.select()
|
||||||
|
.where(RepositoryTag.repository == repo, RepositoryTag.name == tag_name))
|
||||||
|
tag = query.get()
|
||||||
tag.lifetime_end = now
|
tag.lifetime_end = now
|
||||||
tag.save()
|
tag.save()
|
||||||
except RepositoryTag.DoesNotExist:
|
except RepositoryTag.DoesNotExist:
|
||||||
|
@ -1760,12 +1767,13 @@ def create_or_update_tag(namespace_name, repository_name, tag_name,
|
||||||
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):
|
with config.app_config['DB_TRANSACTION_FACTORY'](db):
|
||||||
try:
|
try:
|
||||||
query = (RepositoryTag
|
query = _tag_alive(RepositoryTag
|
||||||
.select(RepositoryTag, Repository)
|
.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,
|
||||||
RepositoryTag.name == tag_name, RepositoryTag.lifetime_end >> None))
|
Namespace.username == namespace_name,
|
||||||
|
RepositoryTag.name == tag_name))
|
||||||
found = db_for_update(query).get()
|
found = db_for_update(query).get()
|
||||||
|
|
||||||
except RepositoryTag.DoesNotExist:
|
except RepositoryTag.DoesNotExist:
|
||||||
|
|
|
@ -1,8 +1,5 @@
|
||||||
import unittest
|
import unittest
|
||||||
import json as py_json
|
|
||||||
|
|
||||||
from flask import url_for
|
|
||||||
from endpoints.api import api
|
|
||||||
from app import app, storage
|
from app import app, storage
|
||||||
from initdb import setup_database_for_testing, finished_database_for_testing
|
from initdb import setup_database_for_testing, finished_database_for_testing
|
||||||
from data import model, database
|
from data import model, database
|
||||||
|
@ -14,12 +11,16 @@ REPO = 'somerepo'
|
||||||
|
|
||||||
class TestGarbageColection(unittest.TestCase):
|
class TestGarbageColection(unittest.TestCase):
|
||||||
def setUp(self):
|
def setUp(self):
|
||||||
|
self._old_tm_expiration = app.config['TIME_MACHINE_DELTA_SECONDS']
|
||||||
|
app.config['TIME_MACHINE_DELTA_SECONDS'] = 0
|
||||||
|
|
||||||
setup_database_for_testing(self)
|
setup_database_for_testing(self)
|
||||||
self.app = app.test_client()
|
self.app = app.test_client()
|
||||||
self.ctx = app.test_request_context()
|
self.ctx = app.test_request_context()
|
||||||
self.ctx.__enter__()
|
self.ctx.__enter__()
|
||||||
|
|
||||||
def tearDown(self):
|
def tearDown(self):
|
||||||
|
app.config['TIME_MACHINE_DELTA_SECONDS'] = self._old_tm_expiration
|
||||||
finished_database_for_testing(self)
|
finished_database_for_testing(self)
|
||||||
self.ctx.__exit__(True, None, None)
|
self.ctx.__exit__(True, None, None)
|
||||||
|
|
||||||
|
@ -77,19 +78,21 @@ class TestGarbageColection(unittest.TestCase):
|
||||||
model.garbage_collect_repository(repository.namespace_user.username, repository.name)
|
model.garbage_collect_repository(repository.namespace_user.username, repository.name)
|
||||||
|
|
||||||
def moveTag(self, repository, tag, docker_image_id):
|
def moveTag(self, repository, tag, docker_image_id):
|
||||||
model.create_or_update_tag(repository.namespace_user.username, repository.name, tag, docker_image_id)
|
model.create_or_update_tag(repository.namespace_user.username, repository.name, tag,
|
||||||
|
docker_image_id)
|
||||||
model.garbage_collect_repository(repository.namespace_user.username, repository.name)
|
model.garbage_collect_repository(repository.namespace_user.username, repository.name)
|
||||||
|
|
||||||
def assertNotDeleted(self, repository, *args):
|
def assertNotDeleted(self, repository, *args):
|
||||||
for docker_image_id in args:
|
for docker_image_id in args:
|
||||||
self.assertTrue(bool(model.get_image_by_id(repository.namespace_user.username, repository.name, docker_image_id)))
|
self.assertTrue(bool(model.get_image_by_id(repository.namespace_user.username,
|
||||||
|
repository.name, docker_image_id)))
|
||||||
|
|
||||||
def assertDeleted(self, repository, *args):
|
def assertDeleted(self, repository, *args):
|
||||||
for docker_image_id in args:
|
for docker_image_id in args:
|
||||||
try:
|
try:
|
||||||
# Verify the image is missing when accessed by the repository.
|
# Verify the image is missing when accessed by the repository.
|
||||||
model.get_image_by_id(repository.namespace_user.username, repository.name, docker_image_id)
|
model.get_image_by_id(repository.namespace_user.username, repository.name, docker_image_id)
|
||||||
except model.DataModelException as ex:
|
except model.DataModelException:
|
||||||
return
|
return
|
||||||
|
|
||||||
self.fail('Expected image %s to be deleted' % docker_image_id)
|
self.fail('Expected image %s to be deleted' % docker_image_id)
|
||||||
|
@ -98,13 +101,13 @@ class TestGarbageColection(unittest.TestCase):
|
||||||
def test_one_tag(self):
|
def test_one_tag(self):
|
||||||
""" Create a repository with a single tag, then remove that tag and verify that the repository
|
""" Create a repository with a single tag, then remove that tag and verify that the repository
|
||||||
is now empty. """
|
is now empty. """
|
||||||
repository = self.createRepository(latest = ['i1', 'i2', 'i3'])
|
repository = self.createRepository(latest=['i1', 'i2', 'i3'])
|
||||||
self.deleteTag(repository, 'latest')
|
self.deleteTag(repository, 'latest')
|
||||||
self.assertDeleted(repository, 'i1', 'i2', 'i3')
|
self.assertDeleted(repository, 'i1', 'i2', 'i3')
|
||||||
|
|
||||||
def test_two_tags_unshared_images(self):
|
def test_two_tags_unshared_images(self):
|
||||||
""" Repository has two tags with no shared images between them. """
|
""" Repository has two tags with no shared images between them. """
|
||||||
repository = self.createRepository(latest = ['i1', 'i2', 'i3'], other = ['f1', 'f2'])
|
repository = self.createRepository(latest=['i1', 'i2', 'i3'], other=['f1', 'f2'])
|
||||||
self.deleteTag(repository, 'latest')
|
self.deleteTag(repository, 'latest')
|
||||||
self.assertDeleted(repository, 'i1', 'i2', 'i3')
|
self.assertDeleted(repository, 'i1', 'i2', 'i3')
|
||||||
self.assertNotDeleted(repository, 'f1', 'f2')
|
self.assertNotDeleted(repository, 'f1', 'f2')
|
||||||
|
@ -113,7 +116,7 @@ class TestGarbageColection(unittest.TestCase):
|
||||||
""" Repository has two tags with shared images. Deleting the tag should only remove the
|
""" Repository has two tags with shared images. Deleting the tag should only remove the
|
||||||
unshared images.
|
unshared images.
|
||||||
"""
|
"""
|
||||||
repository = self.createRepository(latest = ['i1', 'i2', 'i3'], other = ['i1', 'f1'])
|
repository = self.createRepository(latest=['i1', 'i2', 'i3'], other=['i1', 'f1'])
|
||||||
self.deleteTag(repository, 'latest')
|
self.deleteTag(repository, 'latest')
|
||||||
self.assertDeleted(repository, 'i2', 'i3')
|
self.assertDeleted(repository, 'i2', 'i3')
|
||||||
self.assertNotDeleted(repository, 'i1', 'f1')
|
self.assertNotDeleted(repository, 'i1', 'f1')
|
||||||
|
@ -122,8 +125,8 @@ class TestGarbageColection(unittest.TestCase):
|
||||||
""" Two repositories with different images. Removing the tag from one leaves the other's
|
""" Two repositories with different images. Removing the tag from one leaves the other's
|
||||||
images intact.
|
images intact.
|
||||||
"""
|
"""
|
||||||
repository1 = self.createRepository(latest = ['i1', 'i2', 'i3'], name = 'repo1')
|
repository1 = self.createRepository(latest=['i1', 'i2', 'i3'], name='repo1')
|
||||||
repository2 = self.createRepository(latest = ['j1', 'j2', 'j3'], name = 'repo2')
|
repository2 = self.createRepository(latest=['j1', 'j2', 'j3'], name='repo2')
|
||||||
|
|
||||||
self.deleteTag(repository1, 'latest')
|
self.deleteTag(repository1, 'latest')
|
||||||
|
|
||||||
|
@ -134,8 +137,8 @@ class TestGarbageColection(unittest.TestCase):
|
||||||
""" Two repositories with shared images. Removing the tag from one leaves the other's
|
""" Two repositories with shared images. Removing the tag from one leaves the other's
|
||||||
images intact.
|
images intact.
|
||||||
"""
|
"""
|
||||||
repository1 = self.createRepository(latest = ['i1', 'i2', 'i3'], name = 'repo1')
|
repository1 = self.createRepository(latest=['i1', 'i2', 'i3'], name='repo1')
|
||||||
repository2 = self.createRepository(latest = ['i1', 'i2', 'j1'], name = 'repo2')
|
repository2 = self.createRepository(latest=['i1', 'i2', 'j1'], name='repo2')
|
||||||
|
|
||||||
self.deleteTag(repository1, 'latest')
|
self.deleteTag(repository1, 'latest')
|
||||||
|
|
||||||
|
@ -146,8 +149,8 @@ class TestGarbageColection(unittest.TestCase):
|
||||||
""" Two repositories under different namespaces should result in the images being deleted
|
""" Two repositories under different namespaces should result in the images being deleted
|
||||||
but not completely removed from the database.
|
but not completely removed from the database.
|
||||||
"""
|
"""
|
||||||
repository1 = self.createRepository(namespace = ADMIN_ACCESS_USER, latest = ['i1', 'i2', 'i3'])
|
repository1 = self.createRepository(namespace=ADMIN_ACCESS_USER, latest=['i1', 'i2', 'i3'])
|
||||||
repository2 = self.createRepository(namespace = PUBLIC_USER, latest = ['i1', 'i2', 'i3'])
|
repository2 = self.createRepository(namespace=PUBLIC_USER, latest=['i1', 'i2', 'i3'])
|
||||||
|
|
||||||
self.deleteTag(repository1, 'latest')
|
self.deleteTag(repository1, 'latest')
|
||||||
self.assertDeleted(repository1, 'i1', 'i2', 'i3')
|
self.assertDeleted(repository1, 'i1', 'i2', 'i3')
|
||||||
|
@ -158,34 +161,33 @@ class TestGarbageColection(unittest.TestCase):
|
||||||
""" Repository has multiple tags with shared images. Selectively deleting the tags, and
|
""" Repository has multiple tags with shared images. Selectively deleting the tags, and
|
||||||
verifying at each step.
|
verifying at each step.
|
||||||
"""
|
"""
|
||||||
repository = self.createRepository(
|
repository = self.createRepository(latest=['i1', 'i2', 'i3'],
|
||||||
latest = ['i1', 'i2', 'i3'],
|
other=['i1', 'f1', 'f2'],
|
||||||
other = ['i1', 'f1', 'f2'],
|
third=['t1', 't2', 't3'],
|
||||||
third = ['t1', 't2', 't3'],
|
fourth=['i1', 'f1'])
|
||||||
fourth = ['i1', 'f1'])
|
|
||||||
|
|
||||||
# Delete tag other. Should delete f2, since it is not shared.
|
# Delete tag other. Should delete f2, since it is not shared.
|
||||||
self.deleteTag(repository, 'other')
|
self.deleteTag(repository, 'other')
|
||||||
self.assertDeleted(repository, 'f2')
|
self.assertDeleted(repository, 'f2')
|
||||||
self.assertNotDeleted(repository,'i1', 'i2', 'i3', 't1', 't2', 't3', 'f1')
|
self.assertNotDeleted(repository, 'i1', 'i2', 'i3', 't1', 't2', 't3', 'f1')
|
||||||
|
|
||||||
# Move tag fourth to i3. This should remove f1 since it is no longer referenced.
|
# Move tag fourth to i3. This should remove f1 since it is no longer referenced.
|
||||||
self.moveTag(repository, 'fourth', 'i3')
|
self.moveTag(repository, 'fourth', 'i3')
|
||||||
self.assertDeleted(repository, 'f1')
|
self.assertDeleted(repository, 'f1')
|
||||||
self.assertNotDeleted(repository,'i1', 'i2', 'i3', 't1', 't2', 't3')
|
self.assertNotDeleted(repository, 'i1', 'i2', 'i3', 't1', 't2', 't3')
|
||||||
|
|
||||||
# Delete tag 'latest'. This should do nothing since fourth is on the same branch.
|
# Delete tag 'latest'. This should do nothing since fourth is on the same branch.
|
||||||
self.deleteTag(repository, 'latest')
|
self.deleteTag(repository, 'latest')
|
||||||
self.assertNotDeleted(repository,'i1', 'i2', 'i3', 't1', 't2', 't3')
|
self.assertNotDeleted(repository, 'i1', 'i2', 'i3', 't1', 't2', 't3')
|
||||||
|
|
||||||
# Delete tag 'third'. This should remove t1->t3.
|
# Delete tag 'third'. This should remove t1->t3.
|
||||||
self.deleteTag(repository, 'third')
|
self.deleteTag(repository, 'third')
|
||||||
self.assertDeleted(repository, 't1', 't2', 't3')
|
self.assertDeleted(repository, 't1', 't2', 't3')
|
||||||
self.assertNotDeleted(repository,'i1', 'i2', 'i3')
|
self.assertNotDeleted(repository, 'i1', 'i2', 'i3')
|
||||||
|
|
||||||
# Add tag to i1.
|
# Add tag to i1.
|
||||||
self.moveTag(repository, 'newtag', 'i1')
|
self.moveTag(repository, 'newtag', 'i1')
|
||||||
self.assertNotDeleted(repository,'i1', 'i2', 'i3')
|
self.assertNotDeleted(repository, 'i1', 'i2', 'i3')
|
||||||
|
|
||||||
# Delete tag 'fourth'. This should remove i2 and i3.
|
# Delete tag 'fourth'. This should remove i2 and i3.
|
||||||
self.deleteTag(repository, 'fourth')
|
self.deleteTag(repository, 'fourth')
|
||||||
|
@ -197,17 +199,11 @@ class TestGarbageColection(unittest.TestCase):
|
||||||
self.assertDeleted(repository, 'i1')
|
self.assertDeleted(repository, 'i1')
|
||||||
|
|
||||||
def test_empty_gc(self):
|
def test_empty_gc(self):
|
||||||
repository = self.createRepository(
|
repository = self.createRepository(latest=['i1', 'i2', 'i3'], other=['i1', 'f1', 'f2'],
|
||||||
latest = ['i1', 'i2', 'i3'],
|
third=['t1', 't2', 't3'], fourth=['i1', 'f1'])
|
||||||
other = ['i1', 'f1', 'f2'],
|
|
||||||
third = ['t1', 't2', 't3'],
|
|
||||||
fourth = ['i1', 'f1'])
|
|
||||||
|
|
||||||
self.gcNow(repository)
|
self.gcNow(repository)
|
||||||
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):
|
|
||||||
model._garbage_collect_storage(set())
|
|
||||||
|
|
||||||
if __name__ == '__main__':
|
if __name__ == '__main__':
|
||||||
unittest.main()
|
unittest.main()
|
||||||
|
|
Reference in a new issue