2015-11-19 21:01:36 +00:00
|
|
|
import logging
|
|
|
|
|
2015-07-15 21:25:41 +00:00
|
|
|
from uuid import uuid4
|
|
|
|
|
2016-08-29 15:58:18 +00:00
|
|
|
from peewee import IntegrityError
|
2015-08-12 20:39:32 +00:00
|
|
|
from data.model import (image, db_transaction, DataModelException, _basequery,
|
2016-08-29 15:58:18 +00:00
|
|
|
InvalidManifestException, TagAlreadyCreatedException)
|
2015-08-12 20:39:32 +00:00
|
|
|
from data.database import (RepositoryTag, Repository, Image, ImageStorage, Namespace, TagManifest,
|
2016-07-18 22:20:00 +00:00
|
|
|
RepositoryNotification, Label, TagManifestLabel, get_epoch_timestamp,
|
|
|
|
db_for_update)
|
2015-07-15 21:25:41 +00:00
|
|
|
|
|
|
|
|
2015-11-19 21:01:36 +00:00
|
|
|
logger = logging.getLogger(__name__)
|
|
|
|
|
|
|
|
|
2015-07-15 21:25:41 +00:00
|
|
|
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))
|
|
|
|
|
|
|
|
|
2015-11-09 22:12:22 +00:00
|
|
|
def get_matching_tags(docker_image_id, storage_uuid, *args):
|
|
|
|
""" Returns a query pointing to all tags that contain the image with the
|
|
|
|
given docker_image_id and storage_uuid. """
|
|
|
|
image_query = image.get_repository_image_and_deriving(docker_image_id, storage_uuid)
|
|
|
|
|
2015-11-17 22:42:52 +00:00
|
|
|
return _tag_alive(RepositoryTag
|
2015-11-19 21:01:36 +00:00
|
|
|
.select(*args)
|
|
|
|
.distinct()
|
|
|
|
.join(Image)
|
|
|
|
.join(ImageStorage)
|
|
|
|
.where(Image.id << image_query, RepositoryTag.hidden == False))
|
2015-10-27 21:38:48 +00:00
|
|
|
|
|
|
|
|
2015-11-17 22:42:52 +00:00
|
|
|
def get_tags_for_image(image_id, *args):
|
|
|
|
return _tag_alive(RepositoryTag
|
|
|
|
.select(*args)
|
|
|
|
.distinct()
|
|
|
|
.where(RepositoryTag.image == image_id,
|
|
|
|
RepositoryTag.hidden == False))
|
|
|
|
|
|
|
|
|
|
|
|
def filter_tags_have_repository_event(query, event):
|
|
|
|
return (query
|
|
|
|
.switch(RepositoryTag)
|
|
|
|
.join(Repository)
|
|
|
|
.join(RepositoryNotification)
|
|
|
|
.where(RepositoryNotification.event == event))
|
|
|
|
|
2015-07-15 21:25:41 +00:00
|
|
|
def list_repository_tags(namespace_name, repository_name, include_hidden=False,
|
|
|
|
include_storage=False):
|
|
|
|
to_select = (RepositoryTag, Image)
|
|
|
|
if include_storage:
|
|
|
|
to_select = (RepositoryTag, Image, ImageStorage)
|
|
|
|
|
|
|
|
query = _tag_alive(RepositoryTag
|
|
|
|
.select(*to_select)
|
|
|
|
.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
|
|
|
|
|
|
|
|
|
2015-08-12 20:39:32 +00:00
|
|
|
def create_or_update_tag(namespace_name, repository_name, tag_name, tag_docker_image_id,
|
|
|
|
reversion=False):
|
2015-07-15 21:25:41 +00:00
|
|
|
try:
|
|
|
|
repo = _basequery.get_existing_repository(namespace_name, repository_name)
|
|
|
|
except Repository.DoesNotExist:
|
|
|
|
raise DataModelException('Invalid repository %s/%s' % (namespace_name, repository_name))
|
|
|
|
|
|
|
|
now_ts = get_epoch_timestamp()
|
|
|
|
|
|
|
|
with db_transaction():
|
|
|
|
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_obj = 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)
|
|
|
|
|
2016-08-29 15:58:18 +00:00
|
|
|
try:
|
|
|
|
return RepositoryTag.create(repository=repo, image=image_obj, name=tag_name,
|
|
|
|
lifetime_start_ts=now_ts, reversion=reversion)
|
|
|
|
except IntegrityError:
|
|
|
|
msg = 'Tag with name %s and lifetime start %s under repository %s/%s already exists'
|
|
|
|
raise TagAlreadyCreatedException(msg % (tag_name, now_ts, namespace_name, repository_name))
|
2015-07-15 21:25:41 +00:00
|
|
|
|
|
|
|
|
|
|
|
def create_temporary_hidden_tag(repo, image_obj, 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_obj, name=tag_name, lifetime_start_ts=now_ts,
|
|
|
|
lifetime_end_ts=expire_ts, hidden=True)
|
|
|
|
return tag_name
|
|
|
|
|
|
|
|
|
|
|
|
def delete_tag(namespace_name, repository_name, tag_name):
|
|
|
|
now_ts = get_epoch_timestamp()
|
|
|
|
with db_transaction():
|
|
|
|
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()
|
|
|
|
|
|
|
|
|
2015-06-19 18:55:30 +00:00
|
|
|
def garbage_collect_tags(repo):
|
2016-08-26 18:48:39 +00:00
|
|
|
""" Remove all of the tags that have gone past their garbage collection
|
|
|
|
expiration window, and return a set of image ids which *may* have been
|
|
|
|
orphaned.
|
|
|
|
"""
|
|
|
|
def add_expiration_data(base_query):
|
|
|
|
expired_clause = get_epoch_timestamp() - Namespace.removed_tag_expiration_s
|
|
|
|
return (base_query
|
|
|
|
.switch(RepositoryTag)
|
|
|
|
.join(Repository)
|
|
|
|
.join(Namespace, on=(Repository.namespace_user == Namespace.id))
|
|
|
|
.where(~(RepositoryTag.lifetime_end_ts >> None),
|
|
|
|
RepositoryTag.lifetime_end_ts <= expired_clause))
|
|
|
|
return _delete_tags(repo, add_expiration_data)
|
|
|
|
|
|
|
|
def purge_all_tags(repo):
|
|
|
|
""" Remove all tags from the repository, and return a set of all of the images
|
|
|
|
ids which are now orphaned.
|
|
|
|
"""
|
|
|
|
return _delete_tags(repo)
|
|
|
|
|
|
|
|
def _delete_tags(repo, query_modifier=None):
|
|
|
|
""" Garbage collect the tags for a repository and return a set of the image
|
|
|
|
ids which may now be orphaned.
|
|
|
|
"""
|
|
|
|
tags_to_delete_q = (RepositoryTag
|
|
|
|
.select(RepositoryTag.id, Image.ancestors, Image.id)
|
|
|
|
.join(Image)
|
|
|
|
.where(RepositoryTag.repository == repo))
|
|
|
|
|
|
|
|
if query_modifier is not None:
|
|
|
|
tags_to_delete_q = query_modifier(tags_to_delete_q)
|
|
|
|
|
|
|
|
tags_to_delete = list(tags_to_delete_q)
|
|
|
|
|
|
|
|
if len(tags_to_delete) == 0:
|
|
|
|
return set()
|
|
|
|
|
|
|
|
with db_transaction():
|
|
|
|
manifests_to_delete = list(TagManifest
|
|
|
|
.select(TagManifest.id)
|
|
|
|
.join(RepositoryTag)
|
|
|
|
.where(RepositoryTag.id << tags_to_delete))
|
|
|
|
|
|
|
|
num_deleted_manifests = 0
|
|
|
|
if len(manifests_to_delete) > 0:
|
|
|
|
# Find the set of IDs for all the labels to delete.
|
|
|
|
manifest_labels_query = (TagManifestLabel
|
|
|
|
.select()
|
|
|
|
.where(TagManifestLabel.repository == repo,
|
|
|
|
TagManifestLabel.annotated << manifests_to_delete))
|
|
|
|
|
|
|
|
label_ids = [manifest_label.label_id for manifest_label in manifest_labels_query]
|
|
|
|
if label_ids:
|
|
|
|
# Delete all the mapping entries.
|
|
|
|
(TagManifestLabel
|
|
|
|
.delete()
|
|
|
|
.where(TagManifestLabel.repository == repo,
|
|
|
|
TagManifestLabel.annotated << manifests_to_delete)
|
|
|
|
.execute())
|
|
|
|
|
|
|
|
# Delete all the matching labels.
|
|
|
|
Label.delete().where(Label.id << label_ids).execute()
|
|
|
|
|
|
|
|
|
|
|
|
num_deleted_manifests = (TagManifest
|
|
|
|
.delete()
|
|
|
|
.where(TagManifest.id << manifests_to_delete)
|
|
|
|
.execute())
|
|
|
|
|
|
|
|
num_deleted_tags = (RepositoryTag
|
|
|
|
.delete()
|
|
|
|
.where(RepositoryTag.id << tags_to_delete)
|
|
|
|
.execute())
|
|
|
|
|
|
|
|
logger.debug('Removed %s tags with %s manifests', num_deleted_tags, num_deleted_manifests)
|
|
|
|
|
|
|
|
ancestors = reduce(lambda r, l: r | l,
|
|
|
|
(set(tag.image.ancestor_id_list()) for tag in tags_to_delete))
|
|
|
|
direct_referenced = {tag.image.id for tag in tags_to_delete}
|
|
|
|
return ancestors | direct_referenced
|
2015-07-15 21:25:41 +00:00
|
|
|
|
2016-06-02 20:36:38 +00:00
|
|
|
|
|
|
|
def _get_repo_tag_image(tag_name, include_storage, modifier):
|
|
|
|
query = Image.select().join(RepositoryTag)
|
|
|
|
|
|
|
|
if include_storage:
|
2016-08-26 18:47:59 +00:00
|
|
|
query = (Image
|
|
|
|
.select(Image, ImageStorage)
|
|
|
|
.join(ImageStorage)
|
|
|
|
.switch(Image)
|
|
|
|
.join(RepositoryTag))
|
2016-06-02 20:36:38 +00:00
|
|
|
|
|
|
|
images = _tag_alive(modifier(query.where(RepositoryTag.name == tag_name)))
|
2015-07-15 21:25:41 +00:00
|
|
|
if not images:
|
|
|
|
raise DataModelException('Unable to find image for tag.')
|
|
|
|
else:
|
|
|
|
return images[0]
|
|
|
|
|
|
|
|
|
2016-06-02 20:36:38 +00:00
|
|
|
def get_repo_tag_image(repo, tag_name, include_storage=False):
|
|
|
|
def modifier(query):
|
|
|
|
return query.where(RepositoryTag.repository == repo)
|
|
|
|
|
|
|
|
return _get_repo_tag_image(tag_name, include_storage, modifier)
|
|
|
|
|
|
|
|
|
|
|
|
def get_tag_image(namespace_name, repository_name, tag_name, include_storage=False):
|
|
|
|
def modifier(query):
|
2016-08-26 18:47:59 +00:00
|
|
|
return (query
|
|
|
|
.switch(RepositoryTag)
|
|
|
|
.join(Repository)
|
|
|
|
.join(Namespace)
|
|
|
|
.where(Namespace.username == namespace_name, Repository.name == repository_name))
|
2016-06-02 20:36:38 +00:00
|
|
|
|
|
|
|
return _get_repo_tag_image(tag_name, include_storage, modifier)
|
|
|
|
|
|
|
|
|
2015-07-15 21:25:41 +00:00
|
|
|
def list_repository_tag_history(repo_obj, page=1, size=100, specific_tag=None):
|
|
|
|
query = (RepositoryTag
|
|
|
|
.select(RepositoryTag, Image)
|
|
|
|
.join(Image)
|
|
|
|
.where(RepositoryTag.repository == repo_obj)
|
|
|
|
.where(RepositoryTag.hidden == False)
|
2015-10-07 17:00:12 +00:00
|
|
|
.order_by(RepositoryTag.lifetime_start_ts.desc(), RepositoryTag.name)
|
2016-08-02 18:17:33 +00:00
|
|
|
.limit(size + 1)
|
|
|
|
.offset(size * (page - 1)))
|
2015-07-15 21:25:41 +00:00
|
|
|
|
|
|
|
if specific_tag:
|
|
|
|
query = query.where(RepositoryTag.name == specific_tag)
|
|
|
|
|
2016-08-02 18:17:33 +00:00
|
|
|
tags = list(query)
|
|
|
|
return tags[0:size], len(tags) > size
|
2015-07-15 21:25:41 +00:00
|
|
|
|
|
|
|
|
|
|
|
def revert_tag(repo_obj, 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 == repo_obj)
|
|
|
|
.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(repo_obj.namespace_user.username, repo_obj.name, tag_name,
|
|
|
|
docker_image_id, reversion=True)
|
|
|
|
|
2015-08-12 20:39:32 +00:00
|
|
|
|
|
|
|
def store_tag_manifest(namespace, repo_name, tag_name, docker_image_id, manifest_digest,
|
|
|
|
manifest_data):
|
2016-07-18 22:20:00 +00:00
|
|
|
""" Stores a tag manifest for a specific tag name in the database. Returns the TagManifest
|
2016-08-29 15:58:18 +00:00
|
|
|
object, as well as a boolean indicating whether the TagManifest was created.
|
2016-07-18 22:20:00 +00:00
|
|
|
"""
|
2015-08-25 19:34:49 +00:00
|
|
|
with db_transaction():
|
|
|
|
tag = create_or_update_tag(namespace, repo_name, tag_name, docker_image_id)
|
|
|
|
|
|
|
|
try:
|
|
|
|
manifest = TagManifest.get(digest=manifest_digest)
|
|
|
|
manifest.tag = tag
|
|
|
|
manifest.save()
|
2016-08-29 15:58:18 +00:00
|
|
|
return manifest, False
|
2015-08-25 19:34:49 +00:00
|
|
|
except TagManifest.DoesNotExist:
|
2016-07-18 22:20:00 +00:00
|
|
|
return TagManifest.create(tag=tag, digest=manifest_digest, json_data=manifest_data), True
|
2015-08-12 20:39:32 +00:00
|
|
|
|
|
|
|
|
2015-12-03 21:04:17 +00:00
|
|
|
def get_active_tag(namespace, repo_name, tag_name):
|
2015-08-12 20:39:32 +00:00
|
|
|
return _tag_alive(RepositoryTag
|
|
|
|
.select()
|
|
|
|
.join(Repository)
|
|
|
|
.join(Namespace, on=(Repository.namespace_user == Namespace.id))
|
|
|
|
.where(RepositoryTag.name == tag_name, Repository.name == repo_name,
|
|
|
|
Namespace.username == namespace)).get()
|
|
|
|
|
|
|
|
|
|
|
|
def associate_generated_tag_manifest(namespace, repo_name, tag_name, manifest_digest,
|
|
|
|
manifest_data):
|
2015-12-03 21:04:17 +00:00
|
|
|
tag = get_active_tag(namespace, repo_name, tag_name)
|
2015-08-12 20:39:32 +00:00
|
|
|
return TagManifest.create(tag=tag, digest=manifest_digest, json_data=manifest_data)
|
|
|
|
|
|
|
|
|
|
|
|
def load_tag_manifest(namespace, repo_name, tag_name):
|
|
|
|
try:
|
|
|
|
return (_load_repo_manifests(namespace, repo_name)
|
|
|
|
.where(RepositoryTag.name == tag_name)
|
|
|
|
.get())
|
|
|
|
except TagManifest.DoesNotExist:
|
|
|
|
msg = 'Manifest not found for tag {0} in repo {1}/{2}'.format(tag_name, namespace, repo_name)
|
|
|
|
raise InvalidManifestException(msg)
|
|
|
|
|
|
|
|
|
|
|
|
def load_manifest_by_digest(namespace, repo_name, digest):
|
|
|
|
try:
|
|
|
|
return (_load_repo_manifests(namespace, repo_name)
|
|
|
|
.where(TagManifest.digest == digest)
|
|
|
|
.get())
|
|
|
|
except TagManifest.DoesNotExist:
|
|
|
|
msg = 'Manifest not found with digest {0} in repo {1}/{2}'.format(digest, namespace, repo_name)
|
|
|
|
raise InvalidManifestException(msg)
|
|
|
|
|
|
|
|
|
|
|
|
def _load_repo_manifests(namespace, repo_name):
|
2016-06-15 18:49:03 +00:00
|
|
|
return _tag_alive(TagManifest
|
|
|
|
.select(TagManifest, RepositoryTag)
|
|
|
|
.join(RepositoryTag)
|
|
|
|
.join(Image)
|
|
|
|
.join(Repository)
|
|
|
|
.join(Namespace, on=(Namespace.id == Repository.namespace_user))
|
|
|
|
.where(Repository.name == repo_name, Namespace.username == namespace))
|